- 条款18:让接口容易被正确使用,不易被误用
- 条款19:设计class犹如设计type
- 条款20:宁以pass-by-reference-to-const替换pass-by-value
- 条款21:必须返回对象时,别妄想返回其reference
- 条款22:将成员变量声明未private
- 条款23:宁以non-member、non-friend替换member函数
- 条款24:若所有参数皆需类型转换,请为此采用non-member函数
- 条款25:考虑写出一个不抛出异常的swap函数
条款18:让接口容易被正确使用,不易被误用
- 除非有好理由,否则应该尽量令你的types的行为与内置types一致。“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除用户的资源管理责任。
- std::shared_ptr支持定制型删除其(custom deleter)。这可防范DLL问题(在这个dll创建对象,在另一边dll删除),可被用来自动解除互斥锁等等。
条款19:设计class犹如设计type
- C++程序员许多时间主要用来扩张类型系统(type system)。这意味着他不仅是class设计者,还是type设计者。包括重载(overloading)函数和操作符、控制内存的分配和归还、定义对象的初始化和终结……所以应该带着和“语言设计者当初设计语言内置类型时”一样的谨慎来研讨class设计。
- 设计高效的classes需要考虑:
- 新type的对象应该如何被创建和销毁?(operator new,operator new[],operator delete和operator delete[]的设计);
- 对象的初始化和对象的复制有什么样的差别?(构造函数和复制(assigment)操作符的行为;
- 新type的对象如果被passed by value(以值传递),意味着什么?
- 什么是新type的“合法值“? class必须维护的约束条件(invariants)(构造函数、赋值操作符和”setter“函数需要的错误检查工作。它影响函数的异常抛出、以及函数异常明细列(exception specifications)。
- 新type需要配合某个继承图系(inheritance graph)吗? (必然受到那些classes设计的束缚)
- 新type需要什么样的转换? (当需要隐式转换,必须写一个类型转换函数或者写一个non-explicit-one-argument(可被单一实参调用)构造函数)
- 什么样的操作符和函数对此新type而言是合理的?
- 什么样的标准函数应该驳回? (private)
- 谁该取用新type的成员? (public、protected、private、friends)
- 什么是新type的”未声明接口“(undeclared interface)?
- 新type有多么一般化? 如果是定义一整个types家族,就需要定义一个class template)
- 真的需要一个新type吗?
条款20:宁以pass-by-reference-to-const替换pass-by-value
- 当一个函数以pass by value方式接受对象,它的成本是很高的,很短的声明周期使它调用构造函数和析构函数。以by reference方式传递参数可以避免slicing(对象切割)问题(derived class对于base class来说的特化信息丢失)。
- 当使用内置类型有机会选择采用pass-by-value或pass-by-reference-to-const时,by value方式可能会效率高些,因为内置类型都相当小。一般可以合理假设”pass-by-value并不昂贵“的唯一对象就是内置类型和STL的迭代器和函数对象。
条款21:必须返回对象时,别妄想返回其reference
- 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个eap-allocated对象,或返回pointer或reference指向一个local sattic对象而有可能同时需要多个这样的对象。
条款22:将成员变量声明未private
- 成员变量的封装性与”成员变量的内容改变时所破坏的代码数量“成反比。取消一个public成员变量,所有使用它的客户码都会被破坏,;取消一个protected成员变量,所有使用它的derived classes都会被破坏。从封装的角度来看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
- 将成员变量声明为private。可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
- protected并不比public更具封装性。
条款23:宁以non-member、non-friend替换member函数
- 如果某些东西被封装,越多东西被封装,就越多的弹性去改变它。
- 如果在一个member函数和一个non-member,non-friend函数之间做抉择,两者提供相同机能,那么较大封装性的是non-member non-friend函数,因为它不增加访问private成分的函数数量。
- 标准程序库并不是拥有单一、整体、庞大的<C++StandardLibrary>头文件并在其中包含std命名空间内的每一样东西,而是有数十个头文件,每个头文件声明std的某些机能。
- 将所有便利函数放在多个头文件但隶属同一个ing名空间,意味着客户可以轻松扩展这一组便利函数。它需要做的就是添加更多non-member non-friend函数到此命名空间内。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
1 | class Rational |
当尝试混合式算式,只有一般行的通;
1 | Rational oneEighth(1, 8); |
oneHalf
是一个内含operator *
函数的class对象,而整数2并没有相应的class,也就没有operator*
成员函数。但为什么第二个参数是2时可被接受?这里发生了隐式类型转换(implicit type conversion)。
1 | const Rational temp(2); |
而实际想要支持混合式算数运算,就让operator*
称为一个non-member函数,允许编译器在每一个实参身上执行隐式类型转换:
1 | const Rational operator*(const Rational &lhs, const Rational &rhs) |
因此结论:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member。
条款25:考虑写出一个不抛出异常的swap函数
- 一旦要置换两个类对象值,唯一需要做的就是置换其pImpl指针,但default swap算法不知道这点。确切实践思路的一个做法是将std::swap针对该class对象特化(total template specialization):假设
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Widge
{
public:
...
void swap(Widget &other)
{
using std::swap;
swap(pImpl, other.pImpl); // 置换指针
}
};
namespace std
{
temlate<>
void swap<Widget>(Widget &a, Widget &b)
{
a.swap(b);
}
}Widget
和WidgetImpl
都是class templates而非classes,可以尝试将它们的数据类型参数化:在类内放个swap成员函数很简单,却在特化std::swap时遇上乱流:1
2
3
4
5template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };看起来合理但不合法,这是企图偏特化(partially specialize)一个function template,但C++只允许对class template偏特化。有时候std的内容是标准委员会决定,如果希望软件有预期行为,最好不加新东西到std里头。为此,还是声明non-member swap让它调用member swap,但不再将non-member swap声明为std::swap特别版本或重载版本。1
2
3
4
5
6
7namespace std
{
// error
template<typename T>
void swap< Widget<T> >(Widget<T> &a, Widget<T> &b)
{ a.swap(b); }
}
顺带一提,任何地点的任何代码打算置换两个Widget对象,因而调用swap,C++的名称查找法则(name lookup rules,或argument-dependent lookup和koenig lookup法则)会找到专属版本,那正是我们需要的:查找法则首先寻找global作用域或T所在命名空间内的T专属swap,找不到才使用std内的swap,这得感谢using声明式在函数内曝光。1
2
3
4
5
6
7
8template<typename T>
void doSomething(T &obj1, T &obj2)
{
using std::swap; // 令std::swap在此函数可用
...
swap(obj1, obj2); // 寻找最佳swap版本woc~
...
} - 成员版swap绝不可抛出异常,因为swap的一个最好的应用是帮助clasees或class templates提供异常安全性保障。