代码改变世界

Pimpl Idiom /handle body idiom

2015-02-26 10:52  youxin  阅读(1582)  评论(0编辑  收藏  举报
在读《Effective C++》和项目源代码时,看到pImpl Idiom。它可以用来降低文件间的编译依赖关系,通过把一个Class分成两个Class,一个只提供接口,另一个负责实现该接口,实现接口与实现的分离。这个分离的关键在于“以声明的依赖性”替换“定义的依赖性”,而编译依赖性最小化的本质是:让头文件尽可能的自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。


引用这里的一些描述:
The Pimpl idiom, also known as the compilation firewall or Cheshire Cat technique, is a "private implementation" technique useful only in CeePlusPlus and statically compiled languages like it... 

Benefits:

  1. Changing private member variables of a class does not require recompiling classes that depend on it, thus make times are faster, and theFragileBinaryInterfaceProblem is reduced.
  2. The header file does not need to #include classes that are used 'by value' in private member variables, thus compile times are faster.
  3. This is sorta like the way SmallTalk automatically handles classes... more pure encapsulation.

Drawbacks:

  1. More work for the implementor.
  2. Doesn't work for 'protected' members where access by subclasses is required.
  3. Somewhat harder to read code, since some information is no longer in the header file.
  4. Run-time performance is slightly compromised due to the pointer indirection, especially if function calls are virtual (branch prediction for indirect branches is generally poor).

How to do it:

  1. Put all the private member variables into a struct.
  2. Put the struct definition in the .cpp file.
  3. In the header file, put only the ForwardDeclaration of the struct.
  4. In the class definition, declare a (smart) pointer to the struct as the only private member variable.
  5. The constructors for the class need to create the struct.
  6. The destructor of the class needs to destroy the struct (possibly implicitly due to use of a smart pointer).
  7. The assignment operator and CopyConstructor need to copy the struct appropriately or else be disabled.

Code:

1   struct AImp;
2   class A {
3   public:
4     // Same public interface as A, but all delegated to concrete implementation.
5   private:
6     AImp * pimpl;
7   };



If you use a SmartPointer and you only have one implementation, there is no need to make any of the member functions virtual, except possibly the destructor. The run-time cost of non-virtual member function calls is much lower, and a compiler that does whole-program optimization can inline them even though they're in a separate translation unit. Here's an example:

 1  // foo.h
 2 
 3   class foo_impl;
 4 
 5   class foo {
 6     // Boilerplate
 7     friend class foo_impl;
 8     foo() {} // so only foo_impl can derive from foo
 9     const foo_impl * impl() const;
10     foo_impl * impl();
11   public:
12     virtual ~foo() {}
13     // Factories
14     static std::auto_ptr<foo> create(int value);
15     // Interface
16     int value() const;
17   };
18 
19   // foo.cpp
20 
21   class foo_impl : public foo {
22     friend class foo;
23     // Constructors mirroring the factory functions in foo
24     explicit foo_impl(int value) : value_(value) {}
25     // Member data
26     int value_;
27   };
28 
29   inline const foo_impl * foo::impl() const {
30     return static_cast<const foo_impl *>(this);
31   }
32   inline foo_impl * foo::impl() {
33     return static_cast<foo_impl *>(this);
34   }
35 
36   std::auto_ptr<foo> foo::create(int value) {
37     return std::auto_ptr<foo>(new foo_impl(value));
38   }
39 
40   int foo::value() const { return impl()->value_; }
41 
42 

Here, the destructor needs to be declared virtual foo so that std::auto_ptr<foo> calls foo_impl's destructor. If you use boost::shared_ptr<foo> instead, even that doesn't need to be virtual, because shared_ptr remembers how to call the correct destructor. (This doesn't improve performance or memory use, because shared_ptr is larger and slower than auto_ptr, but if you need to use shared_ptr anyway you may as well eliminate the virtual destructor.) -- BenHutchings


参考阅读:

Effective C++
http://c2.com/cgi/wiki?PimplIdiom
http://en.wikipedia.org/wiki/Opaque_pointer

-----------------------------

[C++]程序的设计的Pimpl机制  

一、遇到的问题

1.隐藏实现

我们在给客户端提供接口的时候只希望能暴露它的接口,而隐藏它的实现或者算法。这时候,至少至少有两种选择:

(1)写一个抽象类, 然后继承它

(2)使用PIMPL, 把实现放到内部公有的文件里,而对外部隐藏起来

 

2.重新编译

当我们有一个很大的工程的时候,我们一个底层的头文件不希望被修改,因为这会导致包含该头文件的所有源文件都要重新编译。

 

二、什么是PIMPLl机制

1.Private Implementation

直接的字面意思就是“实现私有化”,也如我们常常听到诸如“不要改动你的公有接口”这样的建议,Pimpl机制,顾名思义,将实现私有化,力图使得头文件对改变不透明。主要作用是解开类的使用接口和实现的耦合。

2.pointer to implementation

这种说法语义上更关注代码的实现方法,也就是一个指向实现的指针。

3.桥接模式

其实,这也是一个简单的桥接模式

 

三、具体分析

1.不使用PIMPL的情况

  1: //base.h 
  2: class Base 
  3: { 
  4: public: 
  5:   void foo(); 
  6: }; 
  7:  
  8: //sub.h 
  9: #include "base.h" 
 10: class sub : public Base 
 11: { 
 12: public: 
 13:   void go(); 
 14: };

可以看到,如果有base中加了一个新的成员函数或者只要做过改动,那么它的子类sub这个文件都是重新编译才行。在一个大工程中,这样的修改可能导致重新编译时间的激增。

2.一个稍好点的方法

一般来说,不在头文件中包含头文件是一个比较好的习惯,但是这也不能完全消除修改base.h带来的重新编译代价。一个稍好点的方法就是只在sub.cpp中包含base.h,但这还是要重新编译的,只是在表现上更完美了一些。

3.使用机制的情况

我们使用前置声明一个Impl类,并将这个类的一个指针实例放入主类中。之后我们只修改Impl类内部私有的内容。

  1: //base.h 
  2: class Imp; 
  3: class Base 
  4: { 
  5: public: 
  6:   void foo(); 
  7: private: 
  8:   Imp* pImp; 
  9: };

除非我们修改base的公有接口,否则这个头文件是不会被修改了。然后,我们用这个Impl类的实现来完成主类的细节实现,在主类的构造函数中,我们完成了实现类指针的实例化:

  1: //cpp中包含实现类的头文件 
  2: #include "imp.h" 
  3:  
  4: Base::Base() 
  5: :pImp(new Imp) 
  6: { 
  7: } 
  8:  
  9: //调用实现类 
 10: Base::foo() 
 11: { 
 12:   pImp->foo(); 
 13: } 
 14: Base::~Base() 
 15: { 
 16:   try 
 17:   { 
 18:     delete pImp; 
 19:   } 
 20:   catch (...) 
 21:   {     
 22:   } 
 23: } 
 24:  
 25: //这是真正的实现 
 26: Imp::foo() 
 27: { 
 28:   //do...xxx 
 29: }

4.实践

在实践中,常常采用内部类来完成Pimpl机制

//一个网上的例子

  1: // header 
  2: class fruit 
  3: { 
  4: public:  
  5: private: 
  6:   class impl; 
  7:   impl* pimpl_; 
  8: }  
  9:  
 10: // implementation 
 11: class fruit::impl 
 12: {  
 13:  
 14: };  
 15:  
 16: fruit::fruit() 
 17: { 
 18:   pimpl_ = new impl(); 
 19: }

四、引来的其它的问题

1.效率问题,每一个类的增加,肯定会增加开销。

2.这种机制不一定就是最好的机制,最简单的机制才是最好的机制

3.在构造和析构的时候,由于我们要new并且要保证delete,会引出RAII原则中的资源管理类的拷贝行为问题

所以,更好的办法是我们在使用这个机制的时候可以使用一个比较安全的智能指针,比如scoped_ptr和shared_ptr,但shared_ptr通常更合适,因为它支持拷贝和赋值。

  1: class sample   
  2: {  
  3: private:     
  4:   class impl;  //不完整的内部类声明     
  5:   shared_ptr<impl> p; //shared_ptr成员变量  
  6: public:      
  7:   sample();   //构造函数    
  8:   void print();  //提供给外界的接口   
  9: };    
 10:  
 11: //在sample的cpp中完整定义impl类和其他功能:    
 12: class sample::impl //内部类的实现  
 13: {  
 14: public:      
 15:   void print()       
 16:   {    
 17:     cout << "impl print" << endl; 
 18:   }   
 19: };    
 20:  
 21: //构造函数初始化shared_ptr  void sample::print()  
 22: sample::sample():p(new impl){}            
 23: {   
 24:   //调用pimpl实现print()   
 25:   p->print(); 
 26: } 

直接可以用:

  1: sample s;  
  2: s.print(); 

(在more effective C++ Item M14中有这个问题的讨论,之后我们再做学习讨论)

五、参考资料

在文章学习总结的过程中,参考引用并演绎了一些链接的内容,在这里表示感谢。

您可以随意转载演绎本文,但希望能保证完整性,给出下面的参考链接

本文链接:

http://pppboy.blog.163.com/blog/static/3020379620115185274478/

参考连接:

http://book.51cto.com/art/201009/225853.htm

http://blog.csdn.net/pongba/archive/2004/09/12/102232.aspx

http://blog.csdn.net/llu131313/archive/2011/06/01/6460423.aspx

http://blog.sina.com.cn/s/blog_48f93b530100n2bc.html

 


PIMPL (Private Implementation)  
城门失火殃及池鱼
pImpl惯用手法的运用方式大家都很清楚,其主要作用是解开类的使用接口和实现的耦合。如果不使用pImpl惯用手法,代码会像这样:
       //c.hpp
        #include<x.hpp>
class C
        {
        public:
            void f1();
        private: 
            X x; //与X的强耦合
        };
像上面这样的代码,C与它的实现就是强耦合的,从语义上,x成员数据是属于C的实现部分,不应该暴露给用户。语言的本质上来说,在用户的代码中,每一次使用”new C”和”C c1”这样的语句,都会将X的大小硬编码到编译后的二进制代码段中(如果X有虚函数,则还不止这些)——这是因为,对于”new C”这样的语句,其实相当于operator new(sizeof(C) )后面再跟上C的构造函数,而”C c1”则是在当前栈上腾出sizeof(C)大小的空间,然后调用C的构造函数。因此,每次X类作了改动,使用c.hpp的源文件都必须重新编译一次,因为X的大小可能改变了。
在一个大型的项目中,这种耦合可能会对build时间产生相当大的影响。
pImpl惯用手法可以将这种耦合消除,使用pImpl惯用手法的代码像这样:
        //c.hpp
        class X; //用前导声明取代include
        class C
        {
         ...
         private:
            X* pImpl; //声明一个X*的时候,class X不用完全定义
        };
在一个既定平台上,任何指针的大小都是相同的。之所以分为X*,Y*这些各种各样的指针,主要是提供一个高层的抽象语义,即该指针到底指向的是那个类的对象,并且,也给编译器一个指示,从而能够正确的对用户进行的操作(如调用X的成员函数)决议并检查。但是,如果从运行期的角度来说,每种指针都只不过是个32位的长整型(如果在64位机器上则是64位,根据当前硬件而定)。
正由于pImpl是个指针,所以这里X的二进制信息(sizeof(C)等)不会被耦合到C的使用接口上去,也就是说,当用户”new C”或”C c1”的时候,编译器生成的代码中不会掺杂X的任何信息,并且当用户使用C的时候,使用的是C的接口,也与X无关,从而X被这个指针彻底的与用户隔绝开来。只有C知道并能够操作pImpl成员指向的X对象。
 
防火墙
“修改X的定义会导致所有使用C的源文件重新编译”这种事就好比“城门失火,殃及池鱼”,其原因是“护城河”离“城门”太近了(耦合)。
pImpl惯用手法又被成为“编译期防火墙”,什么是“防火墙”,指针?不是。C++的编译模式为“分离式编译”,即不同的源文件是分开编译的。也就是说,不同的源文件之间有一道天然的防火墙,一个源文件“失火”并不会影响到另一个源文件
但是,这里我们考虑的是头文件,如果头文件“失火”又当如何呢?头文件是不能直接编译的,它包含于源文件中,并作为源文件的一部分被一起编译。
这也就是说,如果源文件S.cpp使用了C.hpp,那么class C的(接口部分的)变动将无可避免的导致S.CPP的重新编译。但是作为class C的实现部分的class X却完全不应该导致S.cpp的重新编译。
因此,我们需要把class X隔绝在C.hpp之外。这样,每个使用class C的源文件都与class X隔离开来(与class X不在同一个编译单元)。但是,既然class C使用了class X的对象来作为它的实现部分,就无可避免的要“依赖”于class X。只不过,这个“依赖”应该被描述为:“class C的实现部分依赖于class X”,而不应该是“class C的用户使用接口部分依赖于class X”。
如果我们直接将X的对象写在class C的数据成员里面,则显而易见,使用class C的用户“看到”了不该“看到”的东西——class X——它们之间产生了耦合。然而,如果使用一个指向class X的指针,就可以将X的二进制信息“推”到class C的实现文件中去,在那里,我们#include”x.hpp”,定义所有的成员函数,并依赖于X的实现,这都无所谓,因为C的实现本来就依赖于X,重要的是:此时class X的改动只会导致class C的实现文件重新编译,而用户使用class C的源文件则安然无恙!
    指针在这里充当了一座桥。将依赖信息“推”到了另一个编译单元,与用户隔绝开来。而防火墙是C++编译器的固有属性。
 
穿越C++编译期防火墙
是什么穿越了C++编译期防火墙?是指针!使用指针的源文件“知道”指针所指的是什么对象,但是不必直接“看到”那个对象——它可能在另一个编译单元,是指针穿越了编译期防火墙,连接到了那个对象。
从某种意义上说,只要是代表地址的符号都能够穿越C++编译期防火墙,而代表结构(constructs)的符号则不能。
    例 如函数名,它指的是函数代码的始地址,所以,函数能够声明在一个编译单元,但定义在另一个编译单元,编译器会负责将它们连接起来。用户只要得到函数的声明 就可以使用它。而类则不同,类名代表的是一个语言结构,使用类,必须知道类的定义,否则无法生成二进制代码。变量的符号实质上也是地址,但是使用变量一般 需要变量的定义,而使用extern修饰符则可以将变量的定义置于另一个编译单元中。


本文来自CSDN博客,转载请标明出处:http://www.cnblogs.com/yaukey/archive/0001/01/01/1530640.html