读书笔记:Effective C++ 炒冷饭 - Item 31 减少文件间的编译依赖

读书笔记:Effective C++ 炒冷饭 - Item 31 减少文件间的编译依赖

[原创文章欢迎转载,但请保留作者信息]
Justin 于 2009-12-20

大师说了,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等类型,不还是一样需要依赖引用其他头文件吗?其实这是两种不一样的情况,实现和接口。前面说的实现细节的泄漏是会导致编译依赖的,因为编译器需要了解这些类型对象的大小进而为其分配内存空间;但是接口,比如说函数的返回值或是参数表中的参数,就不需要编译器去考虑分配内存的问题,因此也就没有所谓的编译依赖了。
问题知道了,那么解决办法呢,大师提出“骨肉分离法”(嗯……其实是我的杜撰@#¥%):将声明(declaration)和定义(definition)分开。

呃……下面的比喻,最好吃完饭再继续。
如果说接口是一个类的骨架,那么实现就是他的血肉;如果说声明让你摸到了骨头,那么定义应该就是血和肉生长的地方。
根据骨肉分离法,对于一个AClass类,第一步先把血肉(定义/实现)剥离开,只留下骨架。然后找个盒子(新建一个类,比如说AClassImpl),把血肉放进去。
接下来还有一步,在骨头盒子里(原AClass类)加一条绳子连着血肉盒子(一个指向AClassImpl的指针),这样才不至于让骨肉真正的分离,只要找到了骨头盒子,就一定能找到血肉盒子,然后对于这个“可怜”的AClass来说,它的全部“零件”都是完整的,啥也没丢,但是做到了骨肉分离。

也做到了没有编译依赖。
因为对于AClass的用户来说,他们面对的将是一个没有定义的类,这个类的后继改动,只要不涉及接口的改动,都不会导致用户程序的重新编译。
看到这里想想工作时看到的代码,原来前辈也有看过啊……
对比前面的例程,给一个“骨肉分离”了的版本吧:

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( // ..))
{
   
// ..
}

前面的文字是自己的理解,而大师的真言是这样的:

  • 如果可以用指针/引用的话,就不用对象。
  • 如果可以做到仅依赖声明,就不要依赖定义。
  • 为定义和声明分别准备两个头文件。这样一来,用户就可以很简单做到上面两点。

如果觉得骨肉分离太残忍,大师还有另外一个工具:工厂(factory)。
第二种方法中,抽象类/接口类提供了所有接口的纯虚函数形式:会有该类的子类去实现这些接口。与此同时,在抽象类/接口类中还会有一个静态(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->..

无论是骨肉分离法还是工厂模式,都可以去除编译依赖。代价是有的,要为之付出一点点额外代码执行的时间和空间。这个代价又可以通过内联函数(inline function)来减小一些。(不过有听过这种说法:大部分的编译器都会将短小的函数自动转成内联函数的)
尽管如此,只有在以上做法很明显地降低了系统的性能的情况下,才可以放弃分离实现和接口的努力。
这是大师的忠告。

你可能感兴趣的:(读书笔记:Effective C++ 炒冷饭 - Item 31 减少文件间的编译依赖)