实现
条款26:尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible.)
1)“尽可能延后”的真正意义:不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
条款27:尽量少做转型动作(Minimize casting.)
1)C风格的转型(旧式转型,old-style casts):
(T)expression //将expression转型为T
T(expression) //将expression转型为T
2)C++还提供四种新式转型(常常被称为new-style或C++-style casts):
const_cast<T>( expression )
dynamic_cast<T>( expression )
reinterpret_cast<T>( expression )
static_cast<T>( expression )
(1)const_cast通常被用来将对象的常量性移除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符。
(2)dynamic_cast主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。
(3)reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。如将一个pointer to int转型为一个int。
(4)static_cast用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象,或将int转为double等等。
3)dynamic_cast的许多实现版本执行速度相当慢。
4)替代dynamic_cast的两种方案:
(1)使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要,简称使用类型安全容器。
(2)在base class内提供virtual函数做你想对各种base class派生类做的事,也就是将virtual函数往继承体系上方移动。
5)如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
6)如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
7)宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
条款28:避免返回handles指向对象内部成分(Avoid returning “handles” to object internals.)
1)references、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。
2)返回handle可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。
3)避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
条款29:为“异常安全”而努力是值得的(Strive for exception-safe code.)
假设有个class用来表现夹带背景图案的GUI菜单。这个class希望用于多线程环境,所以它有个互斥器(mutex)作为并发控制(concurrency control)之用。
class PrettyMenu{
public:
...
void changeBackground(std::istream& imgSrc); //改变背景图像
...
private:
Mutex mutex; //互斥器
Image* bgImage; //目前的背景图像
int imageChanges; //背景图像被改变的次数
};
下面是changeBackground函数的一个可能实现:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex); //取得互斥器
delete bgImage; //摆脱旧的背景图像
++imageChanges; //修改图像变更次数
bgImage = new Image(imgSrc); //安装新的背景图像
unlock(&mutex); //释放互斥器
}
1)“异常安全”有两个条件:
(1)不泄露任何资源。上述代码没有做到这一点,因为一旦“new Image(imgSrc)”导致异常,对unlock的调用就绝不会执行,于是互斥器就永远被把持住了。
(2)不允许数据败坏。如果“new Image(imgSrc)”抛出异常,bgImage就是指向一个已被删除的对象,imageChanges也已被累加,而其实并没有新的图像被成功安装起来。
可以以对象管理资源方式解决资源泄露问题,使用Lock class作为一种“确保互斥器被及时释放”的方法。
2)异常安全函数提供的三个保证:
(1)基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一直的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态恐怕不可预料。
(2)强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会恢复到“调用函数之前”的状态。
(3)不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。
异常安全码(Exception-safe code)必须提供上述三种保证之一。
对changeBackground而言,提供强烈保证几乎不困难。首先改变PrettyMenu的不过Image成员变量的类型,从一个类型为Image*的内置指针改为一个“用于资源管理”的智能指针。
第二,重新排列changeBackground内的语句次序,使得在更换图像之后才累加imageChanges。
class PrettyMenu{
...
std::tr1::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc)); //以“new Image”的执行结果设定bgImage内部指针
++imageChanges;
}
3)copy and swap策略:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
4)异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
5)函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解inlining的里里外外(Understand the ins and outs of inlining.)
1)如果inline函数的本体很小,编译器针对“函数本体”所产出的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码(object code)和较高的指令高速缓存装置击中率。
2)inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式就是将函数定义于class定义式内。明确声明inline函数的做法则是在定义式前加上关键字inline。
3)大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining,而所有对virtual函数的调用(除非是最平淡无奇的)也都会使inlining落空。因为virtual意味“等待,直到运行期才确定调用哪个函数”,而inline意味“执行前,先将调用动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,就很难责备它们拒绝将函数本体inlining。
4)一个表面上看似inline的函数是否真的是inline,取决于你的建置环境,主要取决于编译器。
5)有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。编译器通常不对”通过函数指针而进行的调用“实施inlining,这意味对inline函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式。
6)构造函数和析构函数往往是inlining的糟糕候选人。
7)inline函数无法随着程序库的升级而升级。如果f是程序库内的一个inline函数,客户将”f函数本体“编进其程序中,一旦程序设计者决定改变f,所有用到f的客户端程序都必须重新编译。然而如果f是non-inline函数,一旦它有任何修改,客户端只需重新连接就好。
8)大部分调试器面对inline函数都束手无策,不支持对inlined函数的调试。
9)80-20经验法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上头。
10)将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
11)不要只因为function templates出现在头文件,就将它们声明为inline。
条款31:将文件间的编译依存关系降至最低(Minimize compilation dependencies between files.)
1)针对Person可以这样做:把Person分割为两个classes,一个只提供接口,另一个负责实现该接口。如果负责实现的那个所谓implementation class取名为PersonImpl,Person将定义如下:
#include<string>
#include<memory>
class PersonImpl;
class Date;
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
};
main class(Person)只内含一个指针成员,指向其实现类(PersonImpl)。这般设计常被称为pimpl idiom。这样设计之下,Person的客户就完全与Dates,Addresses以及Persons的实现细目分离了。
这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正式编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。
2)“接口与实现分离”的设计策略:
(1)如果使用object references或object pointers可以完成任务,就不要使用objects。如果定义某类型的objects,就需要用到该类型的定义式。
(2)如果能够,尽量以class声明式替换class定义式。如果能够将“提供class定义式”(通过#include完成)的义务从“函数声明所在”之头文件转移到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性去除掉。
(3)为声明式和定义式提供不同的头文件。
3)像Person这样使用pimpl idiom的classes,往往被称为Handle classes。这样的classes如何真正做点事情。办法之一是将它们的所有函数转交给相应的实现类(implementation classes)并由后者完成实际工作。
另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class(抽象基类),称为interface class。这种class的目的是详细一一描述derived classes的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。
4)Handle classes和Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies)。
5)支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。