【Effective C++】第五章 实现(下)

条款29:为“异常安全”而努力是值得的

当异常抛出时,带有异常安全的函数会:

  • 不泄露任何资源(条款13:资源管理类)

  • 不允许数据败坏

异常安全函数提供以下三个保证之一:

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。然而程序的现实状态不可预料。

  • 强烈保证:如果异常被抛出,程序状态不改变。

  • 不抛掷保证:承诺绝不抛出异常,因为他们总能完成它们原先承诺的功能。

copy and swap会导致强烈保证。原则是为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。

请记住:

  • 异常安全函数(Exception-safefunctions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。

  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款30:彻透了解inlining的里里外外

  1. inline函数,看起来像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。

  2. inline函数背后整体的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。这样做可能增加目标码的大小。在一台内存有限的的机器上,过度热衷inlining会造成程序体积太大,即使有虚拟内存,inline造成的代码膨胀也会导致额外换页行为,降低指令高速缓存装置的击中率,以及伴随这些而来的效率损失。

  3. inline只是对编译器的一个申请,不是强制指令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内:

      
      class Person{
      public:
          ...
          int age() const { return theAge; } //隐喻申请
          ...
      private:
          int theAge
      };

    这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline。

    明确声明inline函数的做法则是在其定义式钱加上关键字inline。例如标准的max template:

      
      template<typename T>
      inline const T& std::max(const T& a, const T& b){
          return a < b ? b : a;
      }
  4. 大部分编译器拒绝将过于复杂(例如带有循环或递归)的函数inlining,而所有对虚函数的调用也都会使inlining落空。因为虚函数直到运行期才确定调用哪个函数,而内联函数意味执行前先将调用动作替换为被调用函数的本体。

  5. 一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。编译器通常不对“通过函数指针而进行的调用”实施inlining。

  6. 构造函数和析构函数往往是inlining的糟糕候选人。(由编译器于编译期代为产生并安插在程序中的代码,可能存在于构造函数和析构函数中)。

  7. 程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。例如f是程序库内的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序必须重新编译。但如果f是non-inline函数,客户端只需重新连接即可;如果是动态链接库,升级版函数甚至可以不知不觉地被应用程序吸纳。

  8. 大部分调试器面对inline函数都束手无策,因为你不能在一个不存在的函数内设立断点。

请记住:

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

  • 不要只因为function templates出现在头文件,就将它们声明为inline。

条款31:将文件的编译依存关系降至最低

如果没有取得其实现代码所用到的class string,Date和Address的定义式,那么class Person无法通过编译。

  
  class Person{
  public:
      Person(const std::string& name, const Date& birthday,
          const Address& addr);
      std::string name() const;
      std::string birthday() const;
      std::string address() const;
      ...
  private:
      std::string theName; //实现细节
      Date theBirthday;    //实现细节
      Address theAddress;  //实现细节
  };
  

所以Person定义文件的最上方可能存在:

  
  #include<string>
  #include"date.h"
  #include"address.h"

这么一来使得Person定义文件和其含入文件之间形成了一种编译依赖关系。如果这些头文件中有任何一个被改变,或者这些头文件依赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。

Handle classes可以解除接口和实现之间的耦合关系,从而降低文件间的编译依存性。

解除接口和实现之间的耦合关系,从而降低文件间的编译依存性的方法还有Interface classes。

请记住:

  • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。

  • 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。

猜你喜欢

转载自blog.csdn.net/u012940886/article/details/80858208