软件特攻队|API 设计,C++不容忽视的错误设计(1)

对于许多C ++开发人员来说,API设计可能会在其优先级列表中排名第3或第4。大多数开发人员都倾向于使用C ++来获得原始功能和控制权。因此,性能和优化的想法占据这些开发者的时间的百分之八十。


当然,每个C ++开发人员都会考虑头文件设计的各个方面,但是API设计不仅仅是头文件设计那样。事实上,我强烈建议每一个开发人员在其API的设计上,无论是面向公共还是面向内部,都给予一些帮助,因为这样可以节省你大量的维护成本,提供平滑的升级路径,并为你的客户节省麻烦。

错误#1:不将你的API放在命名空间中

因为你不知道将使用哪个代码库,特别是对于外部API。如果不将API功能限制在命名空间中,则可能导致与该系统中使用的其他API发生名称冲突。

例如:

让我们考虑一个非常简单的API和使用它的客户端类:

//API - In Location.h

class vector

{

public:

vector(double x, double y, double z);

private:

double xCoordinate;

double yCoordinate;

double zCoordinate;

};

//Client Program

#include "stdafx.h"

#include "Location.h"

#include

using namespace std;

int main()

{

vector myVector;

myVector.push_back(99);

return 0;

}


如果有人试图在同时使用std::vector的项目中使用这个类,他们会得到一个错误“error C2872: ‘vector’: ambiguous symbol”。这是因为编译器无法决定客户端代码引用的向量是std::vector还是location.h中定义的vector对象。

如何解决这个问题?

始终将API放在自定义命名空间中,例如:

//API

namespace LocationAPI

{

class vector

{

public:

vector(double x, double y, double z);

private:

double xCoordinate;

double yCoordinate;

double zCoordinate;

};

}

另一种方法是为所有公共API符号添加一个唯一的前缀。如果遵循此约定,我们将调用我们的类“lvector”而不是“vector”。此方法用于OpenGL和QT。

在我看来,如果你正在开发纯C的API,这是有道理的。确保所有公共符号符合此唯一命名约定是另一个令人头痛的问题。如果你正在使用C ++,那么你应该只在命名空间中对API功能进行分组,让编译器为你完成繁重的任务。

我还强烈建议你使用嵌套命名空间来进行功能分组或将公共API与内部API分开。一个很好的例子是Boost库,它们可以自由地使用嵌套的命名空间。例如,在根“boost”命名空间内,boost :: variant包含Boost Variant API的公共符号,boost :: detail :: variant包含该API的内部详细信息。

错误#2:在你的公共API头的全局范围中包含“using namespace”

这将导致被引用命名空间中的所有符号在全局命名空间中变得可见,并首先抵消掉使用命名空间的好处。

另外:

头文件的使用者不可能撤消命名空间包含,因此他们被迫使用决策来使用你的命名空间,这是不可取的。

它极大地增加了命名空间首先要解决的冲突的可能性。

当引入新版本的库时,程序的工作版本可能无法编译。如果新版本引入的名称与应用程序正在从另一个库使用的名称冲突,则会发生这种情况。

代码中的“using namespace”部分从包含头部的代码中出现的那一点开始生效,这意味着在此之前出现的任何代码都可能与该点之后出现的任何代码区别对待。

如何解决这个问题?

1.尽量避免在头文件中放置任何使用的命名空间声明。如果你需要一些名称空间对象来编头文件,请在头文件中使用完全限定名称(例如std :: cout,std :: string)。

//File:MyHeader.h:

class MyClass

{

private:

Microsoft::WRL::ComPtr _parent;

Microsoft::WRL::ComPtr _child;

}

2.如果上面的建议#1导致代码混乱太多 - 将“using namespace”用法限制在头文件中定义的类或命名空间内。另一个选择是在头文件中使用范围别名,如下所示。

//File:MyHeader.h:

class MyClass

{

namespace wrl = Microsoft::WRL; // note the aliasing here !

private:

wrl::ComPtr _parent;

wrl::ComPtr _child;

}

错误#3:无视“三法则”

什么是“三法则”?

三法则是,如果一个类定义了析构函数、复制构造函数或复制赋值运算符,那么它应该明确定义三个函数所有,而不是依赖它们的默认实现。

为什么忽略三法则是一个错误?

如果你定义它们中的任何一个,很可能你的类正在管理一个资源(内存,fileHandle,套接字等)。从而:

如果你编写/禁用复制构造函数或复制赋值运算符,您可能需要对另一个执行相同操作:如果执行“special”工作,则另一个可能也应如此,因为这两个函数应该具有相同的效果。

如果你明确地编写了复制函数,则可能需要编写析构函数:如果复制构造函数中的“special”工作是分配或复制某些资源(例如,内存,文件,套接字等),则需要在其中释放它析构函数。

如果你明确地编写了析构函数,则可能需要显式写入或禁用复制:如果必须编写一个非常重要的析构函数,通常是因为你需要手动释放该对象所持有的资源。如果是这样,那些资源可能需要仔细复制,然后你需要注意对象的复制和分配方式,或者完全禁用复制。

让我们看一个例子,在下面的API中,我们有一个由MyArray类管理的资源int *。我们为类创建了一个析构函数,因为我们知道在销毁管理类时我们必须为int *释放内存。到现在为止还挺好。

现在让我们假设你的API的客户端使用它如下所示。

int main()

{

int vals[4] = { 1, 2, 3, 4 };

MyArray a1(4, vals); // Object on stack - will call destructor once out of scope

MyArray a2(a1); // DANGER !!! - We're copyin the reference to the same object

return 0;

}


那么这里发生了什么?

客户端通过构造函数在eth堆栈上创建了类a1的实例。然后他通过从a1复制创建了另一个实例a2。当a1超出范围时,析构函数将删除底层int *的内存。但是当a2超出范围时,它会再次调用析构函数并尝试再次为int *释放内存(此问题称为双重释放),这会导致堆损坏。

由于我们没有提供复制构造函数并且没有将我们的API标记为不可复制,因此客户端无法知道他不应该复制MyArray对象。

如何解决这个问题?

我们可以这样一些事情:

为创建底层资源的深层副本的类提供复制构造函数,例如(int *)就是这种情况。

通过删除复制构造函数和复制赋值运算符使类不可复制。

最后,在API头文件中提供该信息。

这是通过提供复制构造函数和复制赋值运算符来解决问题的代码:

// File: RuleOfThree.h

class MyArray

{

private:

int size;

int* vals;

public:

~MyArray();

MyArray(int s, int* v);

MyArray(const MyArray& a); // Copy Constructor

MyArray& operator=(const MyArray& a); // Copy assignment operator

};

// Copy constructor

MyArray::MyArray(const MyArray &v)

{

size = v.size;

vals = new int[v.size];

std::copy(v.vals, v.vals + size, checked_array_iterator(vals, size));

}

// Copy Assignment operator

MyArray& MyArray::operator =(const MyArray &v)

{

if (&v != this)

{

size = v.size;

vals = new int[v.size];

std::copy(v.vals, v.vals + size, checked_array_iterator(vals, size));

}

return *this;

}


解决此问题的第二种方法是通过删除复制构造函数和复制分配运算符使类不可复制。

// File: RuleOfThree.h

class MyArray

{

private:

int size;

int* vals;

public:

~MyArray();

MyArray(int s, int* v);

MyArray(const MyArray& a) = delete;

MyArray& operator=(const MyArray& a) = delete;

};



此时,当客户端尝试复制类时,他将遇到编译错误:error C2280: ‘MyArray::MyArray(const MyArray &)’: attempting to reference a deleted function

C++ 11的附录:

“三”法则现在已转换为“五”法则,用于移动构造函数和移动赋值运算符中的因子。因此,在我们的例子中,如果要使类不可复制和不可移动,我们将标记移动构造函数和movbe赋值操作符为已删除。

class MyArray

{

private:

int size;

int* vals;

public:

~MyArray();

MyArray(int s, int* v);

//The class is Non-Copyable

MyArray(const MyArray& a) = delete;

MyArray& operator=(const MyArray& a) = delete;

// The class is non-movable

MyArray(MyArray&& a) = delete;

MyArray& operator=(MyArray&& a) = delete;

};


附加警告:如果为类定义了复制构造函数(包括将其标记为已删除),则不会为该类创建移动构造函数。因此,如果你的类只包含简单的数据类型,并且你计划使用隐式生成的移动构造函数,那么如果你定义复制构造函数则不可能。在这种情况下时,你必须显式定义移动构造函数。