随笔-19  评论-2  文章-0  trackbacks-0

========================
Effective C++   实现
书作者:Scott Meyers
原笔记作者:Justin
========================

Item 26 : 尽可能延后变量定义式的出现时间
--------------------------------------------------
 tag:
 ·尽可能延后变量定义式的出现,以增加程序的清晰度并改善程序效率。
 
 定义变量包含了该变量对象的构造操作,如果因为某个原因(如抛出异常,条件语句未执行等)而没有真正用到这个变量,那么构造该变量所耗费的时间和资源就白费了。
 在即将使用变量前再定义它对理解代码也有好处:要想知道某个变量时做什么用的?读接下来的代码便是。

 思考题,以及答案:
 //方法A:循环外定义
 Widget w;
 for (int i = 0; i < n; ++i){
    w = some_value_dependent_on_i;      
    //..                                 
 }                                   

 //方法B:循环内定义
 for (int i = 0; i < n; ++i) {
 Widget w(some_value_dependent_on_i);
 //..
 }
  方法A调用了1次构造函数、1次析构函数、n次拷贝函数;
  方法B调用了n次析构函数、n次析构函数。
  
  当 拷贝操作的开销 比 构造-析构操作 要廉价的时候,一般来说A方法是上选。
  但是A方法中对象的作用域比B方法中更大,也就违背了代码的集中性和可维护性原则。
  因此,除非
     拷贝操作比构造-析构操作开销小,并且此部分代码对性能(performance)要求很高,(此时选择为A)
  否则B方法还是更合理。


Item 27 : 少做转型动作
--------------------------------------------------
 tag: cast 转型    const_cast    dynamic_cast    reinterpret_cast    static_cast
 
 ·尽量避免转型,特别是注重效率的代码中避免 dynamic_casts.尽量将要转型的设计转化为无需转型。
 ·若转型必须,试着将它隐藏于某个函数背后。客户随后可以随时调用该函数,而不需将转型放进他们自己的代码中。
 ·另可使用 C++ style转型,也不要使用旧式转型。前者更容易分辨。
 
 类型转换的三种形式(前面两种都是C风格的旧式类型转换):
  (T)expression
  T(expression)
  C++ Style:
   const_cast:设置或是去除对象的const属性。
   dynamic_cast:主要用于继承关系层次中的向上、向下转换,以及类之间的交叉转换。会进行转换安全性检查。
   static_cast:可用于内置类型的转换,以及继承关系层次中的向上转换。没有转换安全性检查。
   reinterpret_cast:简单的强制将一个指针转换为另外一种指针或整数类型,不做任何检查。
 
 类型转换还可能引发额外的代码运行。比如说dynamic_cast就会通过调用strcmp来比较类的名称,从而完成继承关系中不同类对象的转换,这个时候就不仅仅是简单的变变类型了。因此,说“类型转换仅是告诉编译器把一种类型的数据当成另外一种来参与计算”其实是一个理解上的误区。
 类型转换也有可能带来额外开销:比如书中用static_cast进行的继承关系的向上转换,就会自作主张地生成一个临时的对象。

 dynamic_cast 通常是在一个认定为 derived class 对象上执行 derived class操作函数,但手上只有一个“指向base”的pointer或reference。
  可以用以下两种方法来避免这个问题:
  1. 使用容器并在其中存储直接指向 derived class 对象的指针(通常为智能指针),以消除通过base class接口处理对象的需要。
  2. 在base class内提供virtual函数做你想对各个derived classes做的事。
 要避免所谓的“连串(cascading)dynamic_casts”
 
 在C++中,两个指向同一个对象的不同指针可能拥有不同的地址值。
 因此,不仅要尽可能的避免转换类型,而且在不得不使用类型转换的时候,也应该考虑将转换的代码用函数封装起来。



Item 28 : 避免返回 handles 指向对象内部成分
--------------------------------------------------
 tag: 返回值
 
 避免返回 handles(包括reference、pointer、iterator)指向对象内部。
 增加封装性,帮助const成员函数的行为像个const,并将发生“悬垂handles”的可能性降至最低。
 

 如果只需要读访问,就使用const的返回值,不要开放写的权限。
 有可能产生悬垂指针(dangling pointer)也是暴露对象内部成员handles的后果之一。
 
 一个返回对象内部成员的函数,在用户不正确使用的情况下,就有可能产生悬垂指针。
  class AClass{//..};
  class BClass{
  //..
  const AClass& FuncReturningARef();
  //..
  }

  //a possible user's code
  BClass AnObjectOfB;
  const AClass *pAClass = &(AnObjectOfB.FunReturningARef());
  //After the call pAClass becomes a dangeling pointer..
 

Item 29 : 编写对异常免疫(exception-safe)的代码
--------------------------------------------------
 tag: Exception safety
 
 ·Exception safety functions即使发生异常也不回泄露字元或允许任何数据结构败坏。区分为三种可能的保证:基本型、强类型、不抛出异常型
 ·强烈保证型通常能以 copy-and-swap 实现,但强烈保证兵匪对所有函数都可实现或具备现实意义。
 ·函数提供的“Exception-safe保证”通常只等于其所调用之各个函数的“Exception-safe保证"中的最弱者。

 对异常免疫的函数在异常发生的时候应该具备两个特征:
  不泄漏任何资源(内存、锁等等)
  不造成任何数据结构的损坏
 并能够提供至少以下保证中的一项:

 Exception-safe functions 提供以下三个保证之一:
  基本的保证:当异常抛出时,程序中的对象、数据免遭破坏。
  较强的保证:当异常抛出时,程序的状态不会被改变。若成功调用函数,则系统进入成功后的状态;如果函数中因异常而出错,系统应该留在调用函数前的状态:
  最强的保证:不会有异常抛出。例如对内置类型的操作就不会抛出异常。这是最理想的,但也很难做到。更多的函数只能在前两者中做一选择。

 为了能够提供较强的保证,也即系统的状态不因异常抛出与否而变化,大师又重新提出了“先拷贝后交换”(copy-and-swap)这一方法论来。
 用不那么严谨的说法:为了避免在操作对象时触发异常影响系统状态,“先拷贝后交换”先是创建了一个临时对象,将所有的操作都施加在该临时对象上。如果没有出错,把这个处理过的临时对象和真正需要处理的对象交换一通,算是顺利完成任务;如果有错并抛出了异常,原系统状态也不会被影响,因为真正需要处理的对象根本没有被动过。
 当然,天下没有免费的午餐。
 “先拷贝后交换”不仅耗费了一个临时对象的存储代价,同时支出的还有后面交换对象时的时间和资源开销。因此,对异常免疫的较强保证是很好很强大,但是实际中并不是任何时候都需要做到那么高的保证。杀鸡岂需用牛刀?

 最后要提醒的是,对异常免疫的函数也符合“短板理论”:木桶能装的水与其最短的那块木板有关,函数对异常免疫的程度也由函数中程度最低的代码(包括其调用的函数)决定。某个函数如果调用了另外一个一出现异常就崩溃的函数,那么这个函数就不能提供基本的异常免疫保证。


Item 30 : 透彻了解 inlining
--------------------------------------------------
 tag: inline
 ·将大多数 inlining 限制在小型、被频繁调用的函数身上。可使日后的调用过程和二进制升级更容易,也使程序的速度提升机会更大。
 ·不要只因为 function templates 出现在头文件,就将他们声明为 inline.
 
 使用内联函数(inline function)可以省去一般函数调用的入栈操作开销,比宏(macro)要好用。从编译器的角度来看,没有函数调用的代码要更容易优化。
 但是天下没有免费的午餐,以空间换时间的内联函数同时也带来了更大的程序占用空间,更甚者还会因为这变大的代码空间导致额外的内存换页操作,降低指令缓存(instruction cache)的命中率……这些都是使用内联函数需要考虑到的负面影响。(Scott还是辩证地提醒了一点:当内联函数非常短小时,相比一般意义上的函数调用,它能够帮助编译器生成更小的最终代码和运行时更高的指令缓存命中率)

 内联函数的声明可以是显式的:使用inline关键字;也可以是隐式的:在类的定义中定义函数。

 内联函数一般而言都是定义在头文件中,这是因为大多数编译器的内联动作都是发生在编译过程中。(也有着链接甚至是运行中才进行内联的,但是俺们这里随大流,讲主要矛盾)
 虽然内联函数和函数模板有一点相似:它们都几乎定义在头文件中,但是这两者之间没有必然联系,而非有的程序员想的那样“函数模板一定是内联的”)

 内联函数的定义仅仅是对编译器提出内联的请求。编译器完全有可能忽视这个请求,于是某“内联函数”有可能在最后还是生成了一般函数的代码:

  请求内联的函数有可能太过于复杂
  请求内联的函数有可能是虚函数(虚函数的真正实体要在运行时才能得知,让编译器编译阶段去做内联实在有点强人所难)
  请求内联的函数没什么问题,但是在代码中有用函数指针的方式调用该函数(这样编译器也没办法,如果不生成一般函数哪来的函数指针?)
  这些情况下确实不应该把函数作为内联函数。一个“内联函数”是否最终生成了内联函数,还得编译器说了算。
 然而编译器并不是总能帮助我们做出正确的决定,还有一些情况是需要我们自己做出判断的:

 请求内联的函数是构造/析构函数(表面上看起来某个构造/析构函数很短小甚至是空的,但是为了构造/析构类中的其他成员,编译器有可能会“自觉”地写入必要的代码,这样的构造/析构函数就有可能不适合再做内联了)这一点原文中有更详细的说明。
 当编写支持库时(library)也不建议使用内联函数,因为一旦用户使用了这些含有内联函数的库并编译了自己的程序,这些内联函数就已经“写死”在他们的程序中了。当日后对原先的库做了更新修改,用户就必须重新编译整个程序才能用上新的补丁。而一般的函数就不会有这个问题:他们是动态链接的,用户根本感觉不到任何改动。
 考虑到很多调试器(debugger)无法调试内联函数(本来就没有这么一个“函数”,叫人家怎么设断点?),在调试版本中也不建议使用内联函数。
 有那么多需要注意的地方,大师最后总结了一下:用好内联函数的第一步就是:不用内联函数。并没有那么多的函数真正需要内联,因为80%的程序运行时间都是花在了20%的代码中。第二步是把内联函数当成是手工优化的手段,仅仅在非常需要效率和优化的代码中使用内联。



Item 31 : 将文件间的编译依存关系降至最低
--------------------------------------------------
 tag: Handle class,Interface classes ,  接口类 实现类,
 ·相依于声明式,不要相依于定义式。两个手段来实现:Handle classes 和 Interface classes.
 ·程序库头文件应该以 “ 完全且仅有声明式 ”(full and declaration-only forms)的形式存在,不论是否涉及 templates.
 
 大师说了,C++的设计还是有缺陷的:它无法把接口(interface)的设计和实现(implementation)的设计完全划分开来。
 比如说在一个类的(接口)声明当中,总是或多或少的会泄漏一些实现上的细节,虽然这样做与接口的设计并没有太多联系。

  class  AClass  {
  public :
     void  interface_1();
     std::string  interface_2();
  private :
     //  implementation details are leaking as below..
     std::string  internalData_1;
     BClass internalData_2;
  }   

 往往还需要引用其他头文件中相关对象的定义(如下面的代码),从而产生了对这些头文件的(在编译时的)依赖。因此每次这些文件中的某个有变化时,依赖它的所有文件都需要重新编译。
  #include  < string >
 #include  " BClass.h "  //
 
 【注意】这里貌似逻辑不是很顺:就算没有那些私有成员的声明,接口函数的返回值如果是string或是BClass等类型,不还是一样需要依赖引用其他头文件吗?
 这是两种不一样的情况,实现和接口。
  前面说的实现细节的泄漏是会导致编译依赖的,因为编译器需要了解这些类型对象的大小进而为其分配内存空间;
  但是接口,比如说函数的返回值或是参数表中的参数,就不需要编译器去考虑分配内存的问题,因此也就没有所谓的编译依赖了。


 将类分割为两个classes,一个只提供接口,另一个负责实现该接口。
   class  AClassImpl 
   {
   private :
      //  implementation details are moved here..
      std::string  internalData_1;
      BClass internalData_2;
   }
  
   class  AClass 
   {
   public :
      void  interface_1();
      std::string  interface_2();
   private :
      //  there is only a pointer to implementation
      std::tr1::shared_ptr < AClassImpl >  pImpl;
  }
  
   // a constructor: instantiations of AClass and AClassImpl should always be bound together.
   AClass::AClass( // ..) : pImpl(new AClassImpl( // ..))
   {   } 

 分离的关键在于以“声明的依存性”替换“定义的依存性”:让头文件尽可能自我满足,如果不行,就让它与其他文件内的声明式(而非定义式)相依:
 ·若使用 object references 或 object pointers 可以完成任务,就不要使用 objects.但如果要定义某类型的object,就需要用到定义式,。
 ·尽量以class声明式替换class定义式。
 ·为声明式和定义式提供不同的头文件。

 第二种方法中,抽象类/接口类提供了所有接口的纯虚函数形式:会有该类的子类去实现这些接口。
 在抽象类/接口类中还会有一个静态(static)的工厂函数(比如create()/produce()/factory()……),这个函数实际上起到了构造函数的作用,它“制造”出子类对象来完成真正的任务,同时返回这个对象的指针(通常是智能指针如shared_ptr)。凭借这个返回的指针就可以进行正常的操作,同时不会有编译依赖的担心。一个简陋的代码见下:

  class  AClass: public  AClassFactory  {
  public :
      AClass()   {}
      void  interface_1();
      std:: string  interface_2();
      virtual   ~ AClass();
  }
 
  class  AClassFactory  {
  public :
      virtual   void  interface_1()  =   0 ;
      virtual  std::string  interface_2()  =   0 ;
      virtual   ~AClassFactory()  { /* .. */ }
      static  std::tr1::shared_ptr < AClassFactory >  Produce( /* .. */ )
      {
         // this factory function could be more complicated in practice..
         return  std::tr1::shared_ptr < AClassFactory > ( new  AClass);
      }
  }
 
 
  // AClassFactory could be used in this way..
  std::tr1::shared_ptr < AClassFactory >  pAClassObject;
  pAClassObject  =  AClassFactory::Produce( /* .. */ );
  // pAClassObject->..

 


 

posted on 2010-03-15 22:53 Euan 阅读(417) 评论(0)  编辑 收藏 引用 所属分类: C/C++

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理