尽可能延后变量定义式的出现时间

  1. 变量定义时我们需要付出构造成本,变量离开作用域时我们需要承担析构成本。因此如果不是确定使用变量就将变量定义式往后放。
  2. 我们不仅需要延后变量的定义,直到非得使用该变量的前一刻,还应该尝试延后这份定义直到能够给它初值实参为止。这样不仅可以避免构造和析构非必要对象,还可以避免无意义的default构造行为。
  3. 如果我们知道赋值成本比“构造+析构”成本低,并且正在处理代码中效率高度敏感的部分,那我们可以将变量定义在循环外,否则我们应该将其定义在循环内。

尽量少做转型动作

  1. C风格的转型动作:(T)expression //将expression转为T
    函数风格的转型动作:T(expression) //将expression转型为T
  2. C++的转型操作:const_cast、dynamic_cast、static_cast、reinterpret_cast。新式转型比较受欢迎原因在于:很容易被辨识并且各个转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。
  3. 转型后得到的是新的副本而不是对原来的对象就行修改,并且转型操作的实现执行速度可能会很慢,效率不高。因此如果可以,尽量避免转型,特别是在注重效率的代码中避免使用dynamic_cast。
  4. 如果转型是必要的,试着将它隐藏在某个函数的背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码中。
  5. 宁可使用C++新式转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职责。

避免返回handles指向对象内部成分

  1. 引用、指针和迭代器统统都是所谓的handles(用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的就是“降低对象封装性”的风险。
  2. 如果返回一个handle,用户就可以取得一个指针、引用或迭代器指向我们封装内的成员,然后通过handle调用它。
  3. 避免返回handles(包括引用、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生空悬指针、空悬引用以及空悬迭代器的可能性降至最低。

为“异常安全”而努力是值得的

  1. 当异常被抛出时,带有异常安全性的函数会:
    (1)不泄露任何资源,比如内存或mutex资源未释放
    (2)不允许数据败坏,比如将指针指向new异常的对象
  2. 解决资源泄露的问题我们可以使用资源管理类,利用C++中对象生命周期结束会自动调用析构函数的特性来释放资源,因此主要问题在于数据败坏的问题。
  3. 异常安全函数提供以下三个保证之一:
    (1)基本承诺:异常被抛出时程序内的任何事物仍然保持在有效状态下,没有对象或数据结构会因此而败坏,但是程序的现实状态恐怕不可预料
    (2)强烈保证:异常被抛出,程序状态不改变,调用函数时如果调用失败就回到调用函数之前的状态
    (3)不抛掷保证:承诺绝不发生异常,作用于内置类型(比如int,指针等)身上的操作都提供nothrow保证。
  4. 我们自己编写的代码特别是运用到动态内存的地方很难达到不抛掷保证。copy and swap策略很典型导致强烈保证,原则很简单:为你打算修改的对象做出一份副本,然后在副本上做一切必要的修改,若有任何异常抛出,此时对象状态也未改变,待改变成功后再将对象与修改完的副本在一个不抛出异常的操作中置换(swap)。
  5. 如果函数只操作局部性状态,便相对容易地提供强烈保证,但是当函数对“非局部性数据”有连带影响时,提供困难保证就困难得多。
  6. copy and swap问题在于为每个需要修改的对象作出一个副本,可能会带来比较大的空间和时间开销,因此当强烈保证不切实际时,我们必须提供“基本保证”。
  7. 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

透彻了解inlining的里里外外

  1. 当我们inline某个函数,或许编译器就因此有能力对函数本体执行语境相关最优化,而大部分编译器绝不会对非inline函数调用执行最优化。
  2. inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之,这样做可能增加我们的目标码(object code)大小,在内存有限的机器上,过度热衷inlining会造成程序体积太大,即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为。
  3. 如果inline函数的本体很小,编译器针对“函数本体”所产出的代码可能比针对“函数调用”所产出的码更小。
  4. inline只是对编译器的一个申请,不是强制命令,这项申请可以隐喻提出,也可以明确提出,隐喻方式是将函数定义于class定义式内,这样的函数通常是成员函数,friend函数定义在class内也会被隐喻声明为inline。
  5. 明确声明inline函数的做法则是在其定义式前加上关键字inline,inline函数通常一定被置于头文件内,因为编译器需要知道函数长啥样。inlining在大多数C++程序中是编译期行为。template通常也被置于头文件中,因为编译器也需要知道它长啥样。
  6. template的具现化与inlining无关,如果我们认为一个template所具现出的函数都应该是inlined,就将template声明为inline,否则就不要将其声明为inline。
  7. 大部分编译器将拒绝将太过复杂(例如带有循环或递归)的函数inlining,而对所有virtual函数的调用也会使inlining落空因为virtual函数需要在运行期确定调用哪个函数,而inline函数则需要在编译期确定调用哪个函数并将其替换为函数本体。
  8. 如果编译器无法将我们所要求的函数inline化,会给我们警告信息。
  9. 编译器有时也会为inline函数生成一个outlined函数本体,比如程序需要取某个inline函数的地址,编译器通常需要为此函数生成一个outlined函数本体,毕竟没有本体如何取地址呢。因此编译器通常不对“通过函数指针而进行的调用”实施inlining,因此inline函数调用是被inlined或是不inlined取决于该调用的实施方式。
  10. 构造函数和析构函数通常是inlining的糟糕候选人,因为编译器为构造函数和析构函数增加了很多保证进行异常处理,代码体量也很大。
  11. inline函数还存在一个问题在于inline函数无法随着程序库的升级,因为如果内联函数进行改变,所有用到这个函数的客户端程序必须重新编译,但如果是非inline函数则客户端只需要重新连接就好了,远比重新编译的负担少很多。
  12. 大部分编译器无法对inline函数进行调试,我们需要慎重使用inline函数。
  13. 不要只因为函数模板出现在头文件就将它们声明为inline。

将文件间的编译依存关系降至最低

  1. 类的声明头文件或者这些头文件所依赖的其他头文件有任何一个被改变,那么每一个含入这个类的文件就得重新编译,任何使用这个类的文件也必须重新编译。这样的连串编译依存关系会造成很大的开销。
  2. “前置声明每一件东西”的困难一是在于有些类型不是class,可能只是typedef,比如string,二是在于编译器必须在编译期间知道对象的大小,从而可以分配内存。
  3. 我们可以使用pimpl模式,分为实现类和接口类,接口类中包含指向实现类的指针,这样一来实现类中的包含关系与接口类就毫无关系,这就是接口与实现的分离
  4. pimpl分离的关键在于以“声明的依存性”替换“定义的依存性”,这正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。
  5. 如果使用对象的引用或者对象的指针可以完成任务,就不要使用对象的值,因为前两者只需要对象声明式即可,后者则需要对象定义式。
  6. 如果能够,尽量以class声明式替换class定义式,注意,当我们声明一个函数而它用到某个class时,我们并不需要该class的定义,纵使函数以传值的方式传递参数或返回值时也一样,不过在调用该函数时需要为class提供其定义。
  7. 为声明式和定义式提供不同的头文件,在许多建置环境中template定义式通常定义于头文件中,但也有些建置环境允许template定义式放在非头文件中。
  8. 使用pimpl模式的接口类通常被称为handle class,让一个类变为handle class不会改变它所完成的功能,只会改变它完成功能的方式,需要调用其体内的实现类指针。
  9. 另一个制作handle class的办法是,令这个类成为一种特殊的抽象基类,称为interface class,这种类通常不带成员变量,也没有构造函数,只有一个虚析构函数和一组纯虚函数。因为无法为抽象基类创建实体,我们只能通过类的引用或指针来使用。这种类通常会使用自身内部的工厂函数或虚析构函数返回指针或智能指针指向动态分配的对象,此函数扮演真正的继承与抽象基类的实现类的构造函数的作用。
  10. handle classes和interface classes解除了接口和实现之间的耦合关系,从而降低了文件间的编译依存性。但是handle classes使用指针会增加访问的开销,并且使用动态内存也会带来额外开销和内存不足异常的可能性。
  11. interface classes由于每个函数都是虚函数,所以我们必须为每次函数调用付出一个间接跳跃成本,此外由这个interface class派生出来的对象内必须包含一个vptr,也会增加额外开销。
  12. 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是handle classes和interface classes。
  13. 程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不仅是否涉及templates都适用。