良好的代码架构能够使得后续编码工作变的简单。尤其在OOP的世界中,如何能够设计良好的C++接口?我们的目标是高效,易用,易拓展。
18 让接口容易被使用,不易被误用
首先,考虑客户可能犯什么错误。书中提到可以构建类型系统防范客户输入不合理的数据。同时限制什么可以做,什么不可以做。例如加入const
限定修饰符。
其次,尽量使接口与内建类型等保持一致。例如STL中统一使用size()
方法获取容器的大小。
任何接口如果强制客户记得某件事情,那么就会有犯错的危险。较佳的方法是先发制人,例如预定函数的返回值为智能指针,防止客户接触裸指针。
19 设计class
犹如设计type
设计自定义的class
要慎重,就好比语言设计者小心翼翼地设计语言的内置类型。一般有如下考虑:
- 新的对象如何创建和销毁?这关系到构造和析构函数。
- 对象初始化和赋值有何区别?这关系到构造函数和赋值运算符。
- 新的对象如何以pass-by-value方式传递,意味着什么?这关系到copying函数的实现。
- 什么是新类型的合法值?可能需要对
setter()
函数进行参数检查。 - 新类型在继承图中的位置?这关系到虚函数,以及析构函数是否为虚函数。
- 新类型需要什么样的转换?只能显式构造还是允许隐式转换(意味着你需要自己实现隐式转换函数)。
- 什么样的操作符和函数对此类型是合理的?这涉及到访问权限,以及新类型与外界的交互。
- 什么样的标准函数应该驳回?是否要禁止编译器生成默认函数。
- 谁该取用成员?这决定了成员的访问权限,以及某些类或函数是否为
friend
。 - 什么事新类型的未声明接口?
- 新类型有多一般化?是否建立
template class
更好? - 新的类型真的必要吗?如果是要定义新的派生类来为已经存在的类添加功能,也许使用non-member函数或者模板技术是更好的选择。
20 宁以pass-by-reference-to-const替换pass-by-value
对于较大对象,pass-by-value有可能成为费时的操作。而且,如果以派生类对象实参传入一个以基类为形参的函数,会导致切片发生,也就是函数内部可见的仍然是基类对象,无法实现多态。
总而言之,当按照值传递方法传入参数时,请再三考虑是否传入常值引用是更好的选择。但该条款不适用于内建类型和STL中的迭代器和函数对象。对它们而言,值传递通常更为恰当。
21 必须返回对象时,不要妄想返回其reference
函数返回时,存在局部对象析构和返回值的构造,不要妄图对此优化,返回局部non-static对象的引用几乎必然导致失败!
C++11中引入的移动构造也许是解决这个问题的可行之道,以后总结。
22 将成员变量声明为private
封装,封装,还是封装!
而且,请记住,其实只有两种访问权限:private
(提供了封装)和其他(包括protected
,不提供封装)。
23 宁以non-member和non-friend函数替换member函数
对于类中的数据进行操作时,常常可以使用成员函数的方法,也可以编写一个non-member函数,通过调用类的公开方法实现目的。作者认为应偏向后者。原因有三:
- 封装性。我们以能够获取类私有成员变量的代码多少进行封装性的量度。如果引入类的成员函数,这个函数可以肆无忌惮地访问类内的所有成员,这使得封装被破坏。
- 代码设计的弹性。使用成员函数需要对类进行修改,而使用后者,我们可以借助C++中的名字空间,将相似功能的函数组织在不同的hpp和cpp文件中。需要的时候可以随时添加(因为C++的名字空间支持跨文件,而类声明并不是)。
- 编译开销。每次都要修改类的话,还要重新编译。而使用non-member函数,可以不断做加法,编译时完全可以只处理新文件。
24 若所有参数均需要类型转换,请为此采用non-member函数
作者举出自定义的有理数类与整型数做乘法的例子。首先,我们不将构造函数声明为explicit
,可以完成整形到有理数类的隐式类型转换。
重载乘法的运算符可以被声明为有理数类的成员函数,如下所示:1
2
3
4
5
6class Rational {
// ...
public:
Rational(int numerator=0, int denominator=1);
const Rational operator*(const Rational& rhs) const;
};
然而,这样做的话,auto res = 2*Rational(4,5)
就无法通过编译,因为int
并没有实现operator*(const Rational&)
操作。
更好的方法是将其作为non-member函数,1
2
3const Rational operator*(const Rational& lhs, const Rational& rhs) {
//...
}
25 考虑写出一个不抛出异常的swap()
函数
这一条款更像是模板特化规则的大杂烩。
STL中的swap()
函数是交换两个对象内容的不错选择。它的实现大致如下(平淡无奇):1
2
3
4
5
6
7
8namespace std {
template <typename T>
void swap(T& a, T&b) {
T tmp(a);
a = b;
b = tmp;
}
}
但是对于某些pImpl(pointer to implementation)手法的类(指类的数据成员实际死一个指针,而不是数据成员的实在值),标准库的这一实现未免效率较低,因为我们实际上一般只需要交换两个对象的指针即可。
如何对我们的对象Widget
实现特化?
如果Widget
不是模板类,那么我们需要进行全特化。加入以下:1
2
3
4
5
6namespace std {
template <>
void swap<Widget>(Widget& a, Widget& b) {
swap(pImpl, b.pImpl);
}
}
更好的解决方法是先将swap()
定义为Widget
类的公共成员函数,然后再全特化标准库的swap()
方法时调用。这样与STL的约定保持一致。STL中vector
等容器即是这样的。一方面提供了公开方法进行交换,另一方面特化了std
名字空间的swap()
方法。
当Widget
是模板类时,需要进行偏特化。也许看上去是这样:1
2
3
4
5
6namespace std {
template <typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
但是程序员可以全特化std
中的模板,却不能加入新的类或函数进入std
中。在实际中,这样写出的程序一般仍然能够编译运行,但是这种行为确实是未定义的。所以最好不要这样做。
所以,可以在Widget
存在的名字空间内定义swap()
(而不是加入std
),这里涉及到C++中的模板实例化查找规则,不再多说了。作者在条款末尾总结了一般规则:
- 一般使用标准库中的
swap()
即可。 - 如果自己实现,首先提供一个
public
的swap()
成员函数,注意这儿函数决不能抛出异常。 - 在类或者模板在的名字空间中提供一个non-member的
swap()
函数,并令它调用上述的swap()
成员函数。 - 如果是类,而不是模板,那么特化
std::swap()
,并令它调用上述swap()
成员函数。 - 在客户端代码调用
swap()
时,确定包含一个using
声明式,以便让std::swap()
在你的函数内可见,然后不加任何名字空间修饰符,赤裸裸调用swap()
。