对于许多C ++开发人员来说,API设计可能会在其优先级列表中排名第3或第4。大多数开发人员都倾向于使用C ++来获得原始功能和控制权。因此,性能和优化的想法占据这些开发者的时间的百分之八十。
当然,每个C ++开发人员都会考虑头文件设计的各个方面,但是API设计不仅仅是头文件设计那样。事实上,我强烈建议每一个开发人员在其API的设计上,无论是面向公共还是面向内部,都给予一些帮助,因为这样可以节省你大量的维护成本,提供平滑的升级路径,并为你的客户节省麻烦。
错误#7:不将只读数据/方法标记为constnoexcept
为什么这是一个错误?
有时,你的API会将来自客户端的一些数据结构作为输入。将方法和方法参数标记为const表示客户端将以只读模式使用该数据。相反,如果你没有将API方法和参数标记为const,那么你的客户可能倾向于向你传递数据副本,因为你没有做出此类保证。根据客户端代码调用API的频率,性能影响的结果可以从轻微到严重。
如何解决这个问题?
当你的API需要对客户端数据进行只读访问时,请将API方法和/或参数标记为const。假设你需要一个函数来只检查两个坐标是否相同。
//Don't do this:
bool AreCoordinatesSame(vector& vect1, vector& vect2);
相反,将方法标记为const,以便客户端知道你不会修改客户端传入的矢量对象。
bool AreCoordinatesSame(vector& vect1, vector& vect2) const;
Const正确性是一个很大的话题 - 请参考一本好的C ++教科书或阅读https://isocpp.org/wiki/faq/const-correctness中的FAQ部分。
错误#8:通过const引用返回API的内部
为什么这是一个错误?
从表面上看,通过const引用返回一个对象似乎是双赢的。这是因为:
避免不必要的复制。
客户端无法修改数据,因为它是const引用
但是,这可能会导致一些棘手的问题 ——即:
如果客户端API在内部解除分配后保留并使用引用,该怎么办?
什么是客户端使用const转换来抛弃对象的常量并修改它?
如何解决这个问题?
遵循三步规则:
首先,尽量不要通过更好的设计来暴露API对象的内部
如果规则1太贵,请考虑按值返回对象(创建副本)。
如果这是一个堆分配的对象,请考虑通过shared_pointer返回它,以确保即使你的核心对象被释放也可以访问该引用。
错误#9:使用隐式模板实例化时,使用模板实现细节来混淆公共头文件
在隐式实例化中,模板代码的内部必须放在头文件中。没有其他办法。但是,你可以将模板声明(你的API用户将引用)从模板实例化中分离出来,方法是将实例化放在单独的头文件中,如下所示:
// File: Stack.h ( Public interface)
#pragma once
#ifndef STACK_H
#define STACK_H
#include <vector>
template <typename T>
class Stack
{
public:
void Push(T val);
T Pop();
bool IsEmpty() const;
private:
std::vector<T> mStack;
};
typedef Stack<int> IntStack;
typedef Stack<double> DoubleStack;
typedef Stack<std::string> StringStack;
// isolate all implementation details within a separate header
#include "stack_priv.h"
#endif
// File: Stack_priv.h ( hides implementation details of the Stack class)
#pragma once
#ifndef STACK_PRIV_H
#define STACK_PRIV_H
template <typename T>
void Stack<T>::Push(T val)
{
mStack.push_back(val);
}
template <typename T>
T Stack<T>::Pop()
{
if (IsEmpty())
{
return T();
}
T val = mStack.back();
mStack.pop_back();
return val;
}
template <typename T>
bool Stack<T>::IsEmpty() const
{
return mStack.empty();
}
#endif
许多高质量的基于模板的API使用此技术,例如各种Boost头文件。它的好处是保持主要公共头文件不受实现细节的影响,同时将内部细节的必要暴露,隔离到明确指定为包含私有细节的单独头文件。
错误#10:当用例已知时,不使用显式模板实例化头文件
为什么这是一个错误?
从API设计的角度来看,隐式实例化受到以下问题的困扰:
编译器现在负责在适当的位置滞后地实例化代码,并确保只存在该代码的一个副本以防止重复符号的链接错误。这会对你的客户端的构建和链接时间造成影响。
你的代码逻辑的内部现在暴露出来,这绝不是一个好主意。
客户端可以用一些你以前没有测试过的任意类型来实例化你的模板,并且会遇到奇怪的失败。
如何解决这个问题?
如果你知道你的模板将只与int、double和string一起使用,你可以使用显式实例化为这三种类型生成模板特化。它缩短了客户端的构建时间,使你不必密封模板中未经测试的类型,并将模板代码逻辑隐藏在cpp文件中。
要做到这一点很简单 - 只需按照以下三个步骤进行:
步骤1:将堆栈模板代码的实现移动到cpp文件中:
在这一点上,让我们尝试实现并使用堆栈的push()方法:
Stack<int> myStack;
myStack.Push(31);
我们会遇到一个连接错误:
error LNK2001: unresolved external symbol "public: void __thiscall Stack<int>::Push(int)" (?Push@?$Stack@H@@QAEXH@Z)
这是链接器告诉我们它在任何地方都找不到push方法的定义。难怪,因为我们还没有实例化它。
步骤2:在cpp文件底部创建int、double和string类型的模板实例:
// explicit template instantiations
template class Stack<int>;
template class Stack<double>;
template class Stack<std::string>;
现在你可以构建和运行堆栈代码了。
步骤3:通过将以下typedef放在头文件的末尾,告诉客户端你的API支持int、double和string的三种限定类型:
typedef Stack<int> IntStack;
typedef Stack<double> DoubleStack;
typedef Stack<std::string> StringStack;
警告:如果进行显式特殊化,客户端将无法创建更多特殊化(并且编译器也无法为客户端创建隐式实例化),因为实现细节隐藏在我们的.cpp文件中。请确保这是你的API的预期用例。
欢迎关注软件特攻队!
本文暂时没有评论,来添加一个吧(●'◡'●)