Implementations
- 条款26、尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)
- 条款27、尽量少做转型动作(Minimize casting)
- 条款28、避免返回handles指向对象内部成分(Avoid returning “handles” to object internals)
- 条款29、为“异常安全”而努力是值得的(Strive for exception-safe code)
- 条款30、透彻了解inline的里里外外(Understand the ins and outs of inlining)
- 条款31、将文件间的编译依存关系降至最低(Minimize compilation dependencies between file)
条款26、尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)
- 太快定义变量可能造成效率上的拖延。特别是当存在某些控制流未被执行时,提前定义的变量有可能导致本可以避免的构造和析构成本。
- 最好的方式是延后变量定义到它要被使用的前一刻,甚至是延后到可以直接给它实参为止。这样的话不仅可以避免构造非必要对象,还可以避免毫无意义的default构造行为。
条款27、尽量少做转型动作(Minimize casting)
- 过度使用转型可能导致代码变慢且难维护。
- C风格的转型语法:T(expression)或 (T)expression。
- C++ new cast:const_cast( expression )、static_cast( expression )、dynamic_cast( expression )、reinterpret_cast( expression )。
这些转型操作符的作用如下:
const_cast( expression ):用来将对象的常量性移除(唯一有此功能的C++style转型操作符)。
static_cast( expression ):用来强迫隐式转换,常用于non-const对象转型为const对象、将void*指针转换成typed指针、将pointer-to-base转换成pointer-to-derived,以及上述多种转换的反向转换(但无法将const对象转换为non-const对象)。
dynamic_cast( expression ):用来执行“安全向下转型”,也就是用来决定某对象是否归属于继承体系中的某个类型。该转型无法由旧式转型语法完成,同时可能耗费重大运行时成本。
reinterpret_cast( expression ):意图执行低级转型,实际结果可能取决于编译器,故可移植效果不好。
- C++style转型的优点主要体现在:1、在代码中的辨识度高;2、将转型动作的目标窄化,便于编译器诊断出转型的错误运用。
- 对于单一对象而言,它可能拥有多个地址(如果有个偏移量在运行期被施行于derived*身上的话,则以“base”指针指向它时和例如以“derived”指针指向它时取得的地址不同)。
- C++代码应该尽量少的使用转型,如果转型是必要的,尽量将转型动作隐藏在某个函数内,这样可以方便客户的使用。
条款28、避免返回handles指向对象内部成分(Avoid returning “handles” to object internals)
- references、指针、迭代器等可以取得实际对象的类型都是所谓的handles,而返回一个指向对象内部数据的handle会有降低封装性的风险。
- 避免返回handle指向对象内部,可以增加封装性,帮助const成员函数的行为像个const,以及将发生“虚吊号码牌”的可能性降低。
条款29、为“异常安全”而努力是值得的(Strive for exception-safe code)
- 当异常被抛出时,带有异常安全性的函数会有如下保证:1.不泄露任何资源;2.不会导致数据败坏。
- 异常安全函数可以提供以下三个保证之一:1.基本承诺;2.强烈保证;3.不抛掷保证。
基本承诺:如果异常被抛出,程序内的任何事物依然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有的对象都处于一种前后一致的状态。
强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认知,如果函数成功,就是完全成功,如果函数失败,程序会回复到调用函数之前的状态。
不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有的操作都提供nothrow保证。
- 从异常安全性的观点来看,nothrow函数很好,但是很难在C++领域中完全没有调用任何一个可能抛出异常的函数。所以对于大部分函数而言,抉择一般会落在基本保证和强烈保证之间。
- 对于copy and swap策略,它很典型的会导致强烈保证;不过需要注意的是,强烈保证并非对所有的函数都可实现或有现实意义。
copy and swap原则:为你打算修改的对象(原件)做一个副本,然后在这个副本上做一切必要的修改。若有任何修改动作抛出异常,原对象状态并未改变。待所有的修改全部成功后,再将原对象和修改过的副本在一个不抛出异常的操作中”置换“。
事实上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象。这种手法常被称为pimpl idiom。
- copy and swap策略是对对象做出全有或者全无改变的一个很好的办法。
- 函数提供的“异常安全保证”通常最高只等于其所调用的各个函数中的“异常安全保证”最弱的那个函数。
条款30、透彻了解inline的里里外外(Understand the ins and outs of inlining)
- 将大多数inlining限制在小型、被频繁调用的函数身上,可以使日后的调试和二进制升级更容易,也可以使潜在的代码膨胀问题最小化,同时使程序的速度提升机会最大化。
- 过度使用inline函数会使程序体积增大。即使拥有虚内存,inline造成的代码膨胀问题也会导致额外的换页行为,降低指令高速缓存装置的击中率和效率损失。
- inline只是对编译器的一个申请,并不是强制命令。这个申请可以隐喻提出,也可以明确声明。隐喻提出的方法是将函数定义在class声明内。明确声明的方法是在函数定义前加上inline关键字。
- inlining在绝大多数C++程序中是编译期行为。一个申请了inlining的函数是否真正inline,取决于你的建制环境,主要是指的编译器。
- 大部分编译期拒绝太过复杂的函数inlining行为(例如带有循环或者递归),所有对virtual函数的调用(除非是最平淡无奇的函数)也都会使inlining落空。
- inline函数无法随着程序库的升级而升级。如果inline函数发生了改变,那么所有用到该函数的程序都必须重新编译。
条款31、将文件间的编译依存关系降至最低(Minimize compilation dependencies between file)
- 当一个文件包含另一个文件时,在本文件和被包含文件中会存在一种编译依存关系。如果这两个头文件中的任何一个文件有任何改变,那么包含这些文件以及使用它们的文件都必须重新编译。
- 支持“编译依存性最小化”的一般构想是:相依于声明式,而非定义式。
常见的设计策略:
- 如果使用object references或者object pointers可以完成任何,就不要使用objects。你可以只靠一个声明式就定义出指向该类型的reference或者pointer;但是如果定义某类型的object,就需要用到该类型的定义式。
- 尽量以class声明式代替class定义式。比如当你声明一个函数而它用到了某个class时,你不需要该class的定义;纵使函数以pass-by-value的方式传参。但是当调用该函数时,需要让该class的定义式先曝光。
- 为声明式和定义式提供不同的头文件。
- 基于“编译依存性最小化”思想的两个常见手段是Handle classes和Interface classes。
基于pimpl idiom手法实现的很多class都属于Handle class。
- Handle class和Interface class解除了接口和实现之间的耦合关系,降低了文件间的编译依存性。
- 对于Handle class来说,成员函数必须通过implementation pointer取得对象数据。这会为每次的访问增加一层间接性。每一个对象消耗的内存数量也会增加implementation pointer的大小。implementation pointer还需要初始化并指向一个implementation object,所以会有动态内存分配带来的额外开销,以及异常的可能性。
- 对于Interface class来说,由于每个函数都是virtual,所以每次的函数调用都会有间接跳跃成本。此外,该class派生出的对象会多出用于保存vptr的内存。
- inline和Handle class/Interface class配合使用的效果较好。
- 在程序发展过程中使用Handle class/Interface class可以使实现码变化时对其客户带来的冲击最小化。而当它们导致速度/或大小差异过大时以至于class的耦合性与之相比不明显时,应以具象类替换它们。
- 程序库头文件应该是“完全且仅有声明式”的形式存在,不论是否涉及template。