【问题】
在C++中,如果类定义中的任何部分被改变(即使是私有成员),那么这个类所有的使用者代码都必须重新编译。问了降低这种依赖性,使用一种常见的技术就是利用一个不透明指针(opaque operator)来隐藏一部分实现细节:
class X
{
public:
/*公有成员*/
protected:
/*保护成员*/
private:
/*私有成员*/
class XImpl* pimpl_; //非透明指针,指向已前置声明的类
};
【提问】
1.那些部分应该放入Ximpl?有常见的四种规则,它们是:
- 将全部私有数据(但不是函数)放入Ximpl;
- 将全部的私有成员(即包括函数)放入Ximpl;
- 将全部的私有成员和保护成员放入Ximpl;
- 使Ximpl完全变成为原来的X,将X编写为一个完全由简单的前置函数组成的公共接口(一个句柄/本体的变体)。
它们有什么优缺点,你如何从中选择合适的?
2.Ximpl需要指向一个X对象的“反向指针(back pointer)”吗?
【解答】
两个定义:
- 可见类(visible class):客户代码所见并操纵的类(这里即X)。
- pimpl:可见类隐藏了一个透明指针(也称为pimpl_)下的类实现(在这里指XImpl)。
这个习惯的手法一大优势是:它打破了编译期依存性,系统编译速度更快,因为清理了额外的#include;二是它使代码打来的编译影响范围缩小了,因为类位于Pimpl内部的部分可以自动改变,而不需要重新编译代码。
1.答案:
有常见的四种原则放入到Ximpl:
- 将全部的私有数据(但不是函数)放入Ximpl——这样做我们可以对任何数据成员进行前置声明(而不是#include——这会使客户代码对其形成依赖),当然,我们通常可以做的更好;
- 将全部的私有成员(包括函数)放入Ximpl——现在这几乎是作者常用手法;客户端不应该关心私有的的东西,私有的东西应该藏起来。
对此两条警告,这也是为什么加上几乎原因:
- 即使虚函数是私有的,你也不能把虚函数隐藏在pimpl中。
- 如果pimpl中的函数使用其他函数,其可能需要一个指向可见对象的“反指指针”——这又增加了一层间接性。这个反之指针称为self_。
- 将全部的私有成员和保护成员放入Ximpl——如此更近一步的做法是错误的:保护成员绝不应该放入pimpl,因为这样做就是对其弃之不用,保护成员正是为了派生类能看到并使用而存在,如果放入pimpl,它们基本无用了。
- 使Ximpl完全成为原来的X,将X编写为一个完全由简单的前置函数组成的公共接口——这只是少数几个有限情况下使用,其好处可以避免使用反向指针,因为所有的服务全部都在pimpl类中提供了。其主要的缺点是:这样做会是可见类对于继承而言完全没用,无论是基类还是派生类。
2.答案:
不幸的是回答为是的。无论如何,我们都会把每一个对象分裂为两部分——只因为我们隐藏了其中一部分。
当可见类中的函数被调用的时候,经常要需要使用隐藏部分(即pimpl)中的一部分函数和数据,以便完成调用者的需求。这很好也合理。然而可能不太明显的情况是:在pimpl中的函数也经常必须调用可见类的中的函数——通常是因为需要调用的函数是公有成员或者虚函数。