[CAT*G Translation Project GotW#22-30: Draft]
GotW #24 Compilation Firewalls
著者:Herb Sutter
翻译:CAT*G
[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者CAT*G在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者CAT*G对违反上述两条原则的人不负任何责任。特此声明。
Revision 1.0
Guru of the Week 条款24:编译级防火墙
难度:6 / 10
(使用pimpl惯用法可以大大降低代码之间的相互依赖性,还可以减少程序的建立时间。但问题是,应该把那些东西放入pimpl对象里呢?如何才能安全的使用它呢?)
[Problem]
[问题]
在C++中,如果类定义中的任何部分被改变了(即使是私有成员),那么这个类所有的使用者代妈都必须重新编译。为了降低这种依赖性,使用的一种常见的技术就是利用一个不透明指针(opaque pointer)来隐藏一部分实现细节:
class X {
public:
/* ... 公有成员 ... */
protected:
/* ... 保护成员?... */
private:
/* ... 私有成员?... */
class XImpl* pimpl_; // 指向一个被前置声明了的(forward-declared)类
// 之不透明指针
};
[Questions]
[提问]
1.那些部分应该放入Ximpl?有四种常见的原则,它们是:
- 将全部私有数据(但不是函数)放入Ximpl;
- 将全部私有成员(译注:即包括函数)放入Ximpl;
- 将全部私有成员和保护成员放入Ximpl;
- 使Ximpl完全成为原来的X,将X编写为一个完全由简单的前置函数(forwarding functions)(一个句柄/本体的变体)组成的公共接口。
它们各有什么优缺点?你如何从中选择合适的?
2.Ximpl需要一个指向X对象的"反向指针(back pointer)"吗?
[Solution]
[解答]
首先看两个定义:
可见类(visible class):客户代吗所见并操纵的类(在这里即是X)。
pimpl:可见类中隐藏在一个透明指针(也称为pimpl_)下的类实现(在这里即是XImpl)。
在C++中,如果类定义中的任何部分被改变了(即使是私有成员),那么这个类所有的使用者代妈都必须重新编译。为了降低这种依赖性,使用的一种常见的技术就是利用一个不透明指针(opaque pointer)来隐藏一部分实现细节:
这是"句柄/本体"惯用法(handle/body idiom)的一种变体。如Coplien[注1]所记载,这种方法主要用于在共享代码的情况下进行引用计数(reference counting)。
正如Lakos[注2]所指出的那样,"句柄/本体"惯用法(表现为我所称为的"pimpl惯用法"之形式;这样的叫法缘自给其特意取的、易发音的"pimpl_"指针[注3])对于打破编译期的依赖性也是非常有用的。本条款的解答集中讨论这种用法,其中有些讨论总的来讲并不适从于"句柄/本体"惯用法。
使用这个惯用法的主要代价是性能:
1.每一个构造操作都须分配内存。自己定制的分配器(custom allocator)或许可以缓解内存的额外消耗,但这还不是涉及到更多的工作。
2.每一个隐藏成员都需要一个额外的间接层来予以对其访问。(如果被访问的隐藏成员本身又使用到了一个"反向指针(back pointer)"来调用可见类中的函数,那么就会有双重的间接性。)
1.那些部分应该放入Ximpl?有四种常见的原则,它们是:
- 将全部私有数据(但不是函数)放入Ximpl;
这是个不错的开端,因为现在我们得以对任何只用作数据成员的类进行前置声明(forward-declare)(而不是使用#include语句来包含类的真正声明--这会使客户代码对其形成依赖)。当然,我们通常可以做得更好。
- 将全部私有成员(译注:即包括函数)放入Ximpl;
这(几乎)是我平常的用法。不管怎么说,在C++中,"客户代码不应该也并不关心这些部分"就意味着"私有(private)",而私有的东西则最好藏起来(在一些拥有更"自由宽大"之法律的北欧国家里的情况除外)。
对此有两条警告,其中的第一个也就是我在上一段中加上"almost"的原因:
1.即使虚拟函数是私有的,你也不能把虚拟成员函数隐藏在pimpl类中。如果想要虚拟函数覆写基类中的同名虚拟函数,那么该虚拟函数就必须出现在真正的派生类中。如果虚拟函数不是继承而来的,那么为了让之后层级的派生类能够覆写它,其还是必须出现在可见类中。
2.如果pimpl中的函数要使用其它函数,其可能需要一个指向可见对象的"反向指针(back pointer)"--这又增加了一层间接性。这个反向指针通常被约定俗成的称为self_。
- 将全部私有成员和保护成员放入Ximpl;
如此更进一步的做法其实是错误的。保护成员(protected members)绝不应该被放进pimpl,因为这样做等于就是对其弃之不用。无论如何,保护成员(protected members)正是为了让派生类看到并使用而存在的,因此如果派生类无法看到或使用它们的话,它们也就基本上全无用处了。
- 使Ximpl完全成为原来的X,将X编写为一个完全由简单的前置函数(forwarding functions)(一个句柄/本体的变体)组成的公共接口。
这只在少数几个有限的情况下有用,其好处是可以避免使用反向指针,因为所有的服务全部都在pimpl类之中提供了。其主要的缺点是,这样做一般会使得可见类对于继承而言全无用处,无论是作为基类还是派生类。
2.Ximpl需要一个指向X对象的"反向指针(back pointer)"吗?
很不幸,回答通常为"是的"。无论如何,我们会把每一个对象分裂成两部分--只因为我们要隐藏其中一部分。
当可见类中的函数被调用的时候,经常需要使用隐藏部分(译注:即pimpl部分)中的一些函数和数据,以便完成是调用着的请求。这挺好,也很合理。然而可能不太明显的情况是:在pimpl中的函数也经常必须调用可见类中的函数--通常是因为需要调用的函数是公有成员或虚拟函数。
[注1]:James O. Coplien. Advanced C++ Programming Styles and Idioms (Addison-Wesley, 1992).
[注2]:J. Lakos. Large-Scale C++ Software Design (Addison-Wesley, 1996).
[注3]:我以前经常将其写作impl_。与其等价的pimpl_写法其实是有我的朋友和同事Jeff Sumner提出的;Jeff Sumner同我一样对用于指针变量的匈牙利式"p"前缀(译注:即用于命名变量的匈牙利表示法;在该表示法中,指针变量名以"p"开头,意即"pointer")颇为倾心,另外其还对恐怖的双关语有着独到的敏感(译注:pimpl发音与单词"pimple(意即痤疮、丘疹、脓疱、疙瘩、粉刺)"相同)。
(完)