现在的位置: 首页 > 综合 > 正文

条款43.明智使用Pimpl

2013年08月18日 ⁄ 综合 ⁄ 共 2374字 ⁄ 字号 评论关闭

概要

抑制语言的分离欲望C++将私有成员指定为不可访问的,但是并没有指定为不可见的,虽然这样自有其好处,但是可以考虑通过Pimpl惯用法使私有成员真正的不可见,从而实现编译器防火墙,并提高信息隐藏度(条款1141)。

讨论

如果创建“编译器防火墙”将调用代码和类的私有部分完全隔离是有意义的,就应该使用Pimpl惯用法:将它们隐藏在一个不透明的指针后面(即指向已经声明但未定义的类的指针,使用恰当的Smart指针则更好)。例如:

 

class Map {

 // …

private:

 struct  Impl;

 shared_ptr<Impl> pimpl_;

};

 

应该用Pimpl来存储所有的私有成员,包括成员数据和私有成员函数。这使得我们能够随意改变类的私有实现细节,而不用重新编译调用代码---独立和自由正是这个惯用法的标记性特点(条款41)。

 

请注意:一定要如上所示使用两个声明来声明Pimpl。将两行合并成一条语句,即采用struct Impl* pimpl;这种形式也是合法的,但是意义就不同了,此时Impl处于外围作用域中,而不是类中嵌套类型。

 

使用Pimpl的理由至少有三个,而且它们都源自C++语言的可访问性(是否能够调用或者使用某种东西)和可见性(是否能看到它从而依赖它的定义)之间的差异。特别的是,类的所有私有成员在成员函数和友元之外是不可访问的,但是对整个世界---所有看得到类定义的代码而言都是可见的。

 

这种差异的第一个后果,就是潜在的更长的构建时间,因为需要处理不必要的类型定义。通过值保存的私有数据成员的类型,通过值接受的私有成员函数的参数,或者用在可见函数实现体的类型都必须被定义,即使在此编译单元中根本不需要,这会导致更长的编译时间,例如:

 

class C {

 // …

private:

 AComplicatedType act_;

};

 

包括类C定义的头文件也必须#include包含AComplicatedType定义的头文件,这将会依次连带着包含AComplicatedType可能需要的每一个头文件,然后,如此这般地处理AComplicatedType需要的头文件。如果头文件非常的广泛,编译时间将会显著地被影响。

 

这种差异的第二个后果是对于试图调用函数的代码带来二义性和名称隐藏。就算不能在类和类的友元外调用私有成员函数,但是它们确实参与了名称查找和重载解析[TODO,查resolution的标准译法],从而使调用无法或存在二义性。C++在进行可访问性检查之前执行名称查找和重载解析。这就是为什么可见性变得优先的原因:

 

int Twice( int );             // 1

 

class Calc {

public:

 string Twice( string );      // 2

 

private:

 char* Twice( char* );        // 3

 

 int Test() {

  return Twice( 21 );      // A: , 2 3 不行(1 应该可行,

 }                            // 但是它不能被考虑, 因为它被隐藏了)

};

 

Calc c;

c.Twice( "Hello" );       // B: , 3 不能被访问(2 应该可以

                              // 但它能被考虑,因为3 是更好的匹配)

 

A行,解决方法是显式的限定调用,即::Twice( 21 ),强制名称查找选择全局函数。在B行,解决方法是显式的增加转换,即c.Twice( string("Hello")),以重载解析强制选择需要的函数。这些调用问题中,有些是可以用Pimpl惯用法以外的办法解决的,例如永远不写成员函数的私有重载,但并不是所有 Pimpl惯用法能解决的调用问题都有这样的应急之策[我的翻译是“但是所有可以用Pimpl惯用法解决的问题并不一定都有这样的应急之策”,哪句更好?]

 

这种差异的第三个后果是对错误处理和安全的影响。考虑Tom Cargill Widget示例:

 

class Widget {// …

public:

 Widget& operator=( const Widget& );

 

private:

 T1 t1_;

 T2 t2_;

};

 

简而言之,如果T1T2的操作失败是不可逆,我们就无法编写operator=来提供强保证或甚至最小需求的(基本)保证(条款71)。好在以下一个简单的改变就能总是为出错时安全[error-safe]的赋值操作至少提供基本保证,只要用到的T1T2的操作(特别的是构造和析构函数)没有副作用,通常还能提供强保证:即通过指针而不是值保存成员对象,最好是全都隐藏在Pimpl指针后。

 

class Widget {// …

public:

 Widget& operator=( const Widget& );

 

private:

 struct Impl;

 shared_ptr<Impl> pimpl_;

};

 

Widget& Widget::operator=( const Widget& ) {

 shared_ptr<Impl> temp( new Impl( /*...*/ ) );

 

 // 改变 temp->t1_ temp->t2_; 如果失败则抛出异常否则提交,使用:

pimpl_ = temp;

return *this;

}

例外

只有在弄清楚了增加间接层次确实有好处之后,才能添加复杂性,Pimpl也一样(条款68)。

参考

[Coplien92] §5.5

[Dewhurst03] §8

[Lakos96] §6.4.2

[Meyers97] §34

[Murray93] §3.3

[Stroustrup94] §2.10, §24.4.2

[Sutter00] §23, §26-30

[Sutter02] §18, §22

[Sutter04] §16-17

 

抱歉!评论已关闭.