PIMPL IDIOM & FAST PIMPL

PIMPL是C++开发中经常使用的一种惯用法,其原理主要是将对定义的依赖转换为对声明的依赖,通过前向声明,达到接口与实现的分离的效果,并将编译时文件间的依赖降到最低,从而大大缩短程序编译的时间。

PIMPL的实现方式,第一种是让实现类成为接口类的一个私有指针成员变量

比如一个数据访问类DataAccess,

// DataAccess.h
#include "DataAccessFWD.h"  // 包含DataAccessImpl等类的前向声明

class DataAccess
{
  enum {ORACLE, DB2, MSSQL, MYSQL};
public:
  DataAccess( int db = ORACLE );
  ~DataAccess();
  bool open( const char* username, const char* password, const char* connstr );
private:
  DataAccessImpl* DAPimpl;  // 具体实现隐藏其中
};

// DataAccessImpl.h, 其public接口与DataAccess类保持一致
#include "occi.h"
using namespace oracle::occi;

class DataAccessImpl
{
  enum {ORACLE, DB2, MSSQL, MYSQL};
public:
  DataAccessImpl( int db = ORACLE );
  ~DataAccessImpl();
  bool open( const char* username, const char* password, const char* connstr );
private:
  int dbType;

  Environment* env;
  Connection* conn;
  Statement* stmt;
  ResultSet* rs;
};

 DataAccess类的实现如下:
#include "stdafx.h"
#include "DataAccess.h"
#include "DataAccessImpl.h"

DataAccess::DataAccess( int db /*= ORACLE */ ) : DAPimpl(new DataAccessImpl(db))
{
}

DataAccess::~DataAccess()
{
  // 释放实现类
  if (DAPimpl)
  {
     delete DAPimpl;
     DAPimpl = NULL:
  }
}

bool DataAccess::open(const char* username, const char* password, const char* connstr)
{
  // 调用实现类的接口
  return DAPimpl->open(username, password, connstr);
}

      从以上实现可以看出,实现与接口分离,实现类的修改并不影响接口,因此,当修改了实现类,并不会导致接口类的重新编译。
      另一种实现方式则是通过继承的方式实现:让接口类成为抽象基类,这个基类包含一个返回该类指针的静态方法,该方法通过实现类的构造函数实现,实现类继承自这个抽象基类,并实现基类描述的接口。
      PIMPL的一个缺点就是通用内存分配所导致的性能下降以及virtual函数带来的内存寻址转换的开销,因此程序员们又找到了一种FAST PIMPL的实现,它们基于固定内存分配,这比通用内存分配效率上要高许多,具体情况请Google - ‘Guru of the Week 28’。

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


GotW #28“Fast Pimpl”技术(The "Fast Pimpl" Idiom)
难度:6 / 10

采用一些称为“低依赖度”或“效能”方面的捷径,在很多时候颇有诱惑力,但它不总是好主意。这儿有个很精彩的方法能在客观上同时并安全的实现二者。

问题

标准的malloc()和new()调用的开销都是很大的。在下面的代码中,程序员最初在class Y中设计了一个类型X的成员:

    // file y.h

    #include "x.h"

    class Y {

        /*...*/

        X x_;

    };

    // file y.cpp

    Y::Y() {}

这个class Y的申明需要class X的申明已经可见(从x.h中)。要避免这个条件,程序员首先试图这么写:

    // file y.h

    class X;

    class Y {

        /*...*/

        X* px_;

    };

    // file y.cpp

    #include "x.h"

    Y::Y() : px_( new X ) {}

    Y::~Y() { delete px_; px_ = 0; }

这很好地隐藏了X,但它造成了:当Y被广泛使用时,动态内存分配上的开销降低了性能。最终,我们这无畏的程序员偶然间发现了“完美”解决方案:既不需要在y.h中包含x.h,也不需要动态内存分配(甚至连一个前向申明都不需要!):

    // file y.h

    class Y {

        /*...*/

        static const size_t sizeofx = /*some value*/;

        char x_[sizeofx];

    };

    // file y.cpp

    #include "x.h"

    Y::Y() {

        assert( sizeofx >= sizeof(X) );

        new (&x_[0]) X;

    }

    Y::~Y() {

        (reinterpret_cast<X*>(&x_[0]))->~X();

    }

讨论

解答

简短的答案

不要这样做。底线:C++不直接支持不定类型(opaque types),这是依赖于这一局限上的脆弱尝试(有些人甚至称此为“hack”[注1])。

程序员期望的肯定是其它一些东西,它被称为“Fast Pimpl”方法,我将在“为什么尝试3是糟糕的”一节中介绍。

为什么尝试3是糟糕的

首先,让我们稍许考虑一下为什么上面的尝试3是糟糕的,从以下方面:

1. 对齐。不象从::operator new()得到的动态内存,这个x_的字符buffer并不确保满足类型X的对齐要求。试图


让x_工作得更可靠,程序员需要使用一个“max_align”联合,它的实现如下:

    union max_align {

      short       dummy0;

      long        dummy1;

      double      dummy2;

      long double dummy3;

      void*       dummy4;

      /*...and pointers to functions, pointers to

           member functions, pointers to member data,

           pointers to classes, eye of newt, ...*/

    };

它需要这样使用:

    union {

      max_align m;

      char x_[sizeofx];

    };

这不确保完全可移植,但在实际使用中,已经足够完美了,只在极少的系统(也可能没有)上,它不能如愿工作。

我知道有些老手认可甚至推荐这种技巧。凭良心,我还是称之为hack,并强烈反对它。

2.脆弱。Y的作者必须极度小心处理Y的其它普通函数。例如,Y绝不能使用默认赋值操作,必须禁止它或自己提供一个版本。

写一个安全的Y::operator=()不是很难,我把它留给读者作为习题。记得在这个操作和Y::~Y()中考虑异常安全的需求。当你完成后,我想,你会同意这个方法制造的麻烦比它带来的好处远为严重。

3.维护代价。当sizeof(X)增大到超过sizeofx,程序员必须增大sizeofx。这是一个没有引起注意的维护负担。选择一个较大的sizeofx值可以减轻这个负担,但换来了效能方面的损失(见#4)。

4.低效能的。只要sizeofx大于sizeof(X),空间被浪费了。这个损失可以降到最小,但又造成了维护负担(见#3)。

5.顽固不化。我把它放在最后,但不是最轻:简而言之,明显,程序员试图做些“与众不同”的事情。说实话,在我的经验中,“与众不同”和“hack”几乎是同意词。无论何时,只要见到这种“颠覆”行为--如本例的在字符数组中分配对象,或GotW#23中谈到的用显式的析构加placement new来实现赋值--,你都一定要说“No”。

我就是这个意思。好好反思一下。

更好的解决方法:“Fast Pimpl”

隐藏X的动机是避免Y的用户必须要知道(于是也就依赖)X。C++社区中,为了消除这种实现依赖,通常使用“pimpl”方法(注2),也就是我们这个无畏的程序员最初所尝试的方法。

唯一的问题是,“pimpl”方法由于为X的对象在自由空间分配内存而导致性能下降。通常,对特殊类的内存分配性能问题,采用为它提供一个operator new的重载版本的方法,因为固定尺寸的内存分配器可以比通用内存分配器性能高得多。

不幸的是,这也意味着Y的作者必须也是X的作者。通常,这不成立。真正的解决方法是使用一个高效的Pimpl,也就是,有自己的类属operator new的pimpl类。

    // file y.h

    class YImpl;

    class Y {

      /*...*/

      YImpl* pimpl_;

    };

    // file y.cpp

    #include "x.h"

    struct YImpl {  // yes, 'struct' is allowed :-)

      /*...private stuff here...*/

      void* operator new( size_t )   { /*...*/ }

      void  operator delete( void* ) { /*...*/ }

    };

    Y::Y() : pimpl_( new YImpl ) {}

    Y::~Y() { delete pimpl_; pimpl_ = 0; }

“啊!”你叫道,“我们找到了圣杯--Fast Pimpl!”。好,是的,但先等一下,先想一下它是如何工作的,并且它的代价是什么。

你所收藏的C++宝典中肯定展示了如何实现一个高效的固定尺寸内存分配/释放函数,所以我就不多罗嗦了。我要讨论的是“可用性”:一个技巧,将它们放入一个通用固定尺寸内存分配器模板中的技巧,如下:

    template<size_t S>

    class FixedAllocator {

    public:

      void* Allocate( /*requested size is always S*/ );

      void  Deallocate( void* );

    private:

      /*...implemented using statics?...*/

    };

因为它的私有细节很可能使用static来实现,于是,问题是Deallocate()是否被一个static对象的析构函数调用(Because the private details are likely to use statics, however, there could be problems if Deallocate() is ever called from a static object's dtor)。也许更安全的方法是使用single模式,为每个被请求的尺寸使用一个独立链表,并用一个唯一对象来管理所有这些链表(或者,作为效能上的折中,为每个尺寸“桶”使用一个链表;如,一个链表管理大小在0~8的内存块,另一个链表表管理大小在9~16的内存块,等等。):

    class FixedAllocator {

    public:

      static FixedAllocator* Instance();

      void* Allocate( size_t );

      void  Deallocate( void* );

    private:

        /*...singleton implementation, typically

             with easier-to-manage statics than

             the templated alternative above...*/

    };

让我们用一个辅助基类来封装这些调用:

    struct FastPimpl {

      void* operator new( size_t s ) {

        return FixedAllocator::Instance()->Allocate(s);

      }

      void operator delete( void* p ) {

        FixedAllocator::Instance()->Deallocate(p);

      }

    };

现在,你可以很容易地写出你任意的Fast Pimpl:

    //  Want this one to be a Fast Pimpl?

    //  Easy, then just inherit...

    struct YImpl : FastPimpl {

      /*...private stuff here...*/

    };

但,小心!

这虽然很好,但也别乱用Fast Pimpl。你得到了最佳的内存分配速度,但被望了代价:维护这些独立的链表将导致空间效能的下降,因为这比通常情况现成更多的内存碎片(Managing separate free lists for objects of specific sizes usually means incurring a space efficiency penalty because any free space is fragmented (more than usual) across several lists)。

和其它性能优化方法一样,只有在使用了profiler剖析性能并证明需要性能优化后,才使用这个方法。

(注1. 我不是其中之一。 :-))

(注2. 参见GotW #24.)

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lqk1985/archive/2010/01/26/5256331.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值