皮姆普成语在实践中
关于这个pimpl习语 ,有几个问题,但是我更加好奇它在实践中被多less次使用。
我知道性能和封装之间有一些折衷,加上一些debugging烦恼由于额外的redirect。
那么,这是应该在每一个class级,或全或无的基础上采取的东西? 这是最佳做法还是个人喜好?
我意识到这有点主观,所以让我列出我的首要任务:
- 代码清晰
- 代码可维护性
- 性能
我总是认为我需要在某个时候将我的代码作为一个库来公开,所以这也是一个考虑因素。
编辑:任何其他选项来完成同样的事情将是值得欢迎的build议。
我想说,不pipe你是按class还是全无,这是否取决于你为什么selectpimpl成语。 我build立图书馆的原因有以下几点:
- 希望隐藏实现,以避免泄露信息(是的,这不是一个FOSS项目:)
- 希望隐藏实现,以使客户端代码更less依赖。 如果您构build共享库(DLL),则可以更改您的pimpl类,而无需重新编译该应用程序。
- 希望减less使用库编译类所花费的时间。
- 想要修复名称空间冲突(或类似)。
这些原因都没有提示采取全有或全无的方法。 在第一个例子中,你只是把你想要隐藏的东西简单化,而在第二种情况下,对于你希望改变的类来说,这样做可能就足够了。 同样,对于第三和第四个原因,只有隐藏不重要的成员才能获得好处,而这些成员又需要额外的头文件(例如第三方库,甚至是STL)。
无论如何,我的观点是我通常不会发现这样的东西太有用:
class Point { public: Point(double x, double y); Point(const Point& src); ~Point(); Point& operator= (const Point& rhs); void setX(double x); void setY(double y); double getX() const; double getY() const; private: class PointImpl; PointImpl* pimpl; }
在这种情况下,权衡开始打你,因为指针需要解除引用,并且方法不能被内联。 但是,如果你只为非平凡的类而做,那么轻微的开销通常是可以容忍的,没有任何问题。
pimpl ideom的最大用途之一是创build稳定的C ++ ABI。 几乎每个 Qt类都使用“D”types的指针。 这样可以在不破坏ABI的情况下执行更容易的更改。
代码清晰
代码清晰度是非常主观的,但是在我看来,拥有单个数据成员的头比拥有许多数据成员的头更具可读性。 然而,实现文件噪音较大,因此清晰度降低了。 如果这个类是一个基类,那么这个问题可能不是问题,大部分是由派生类使用的,而不是维护的。
可维护性
对于pimpl'd类的可维护性,我个人发现在数据成员的每个访问中额外的取消引用是单调乏味的。 如果数据是纯粹的私有数据,访问者无法提供帮助,因为无论如何您都不应该公开accessor或mutator,而且您一直在不断地取消引用pimpl。
为了派生类的可维护性,我发现这个习语在所有情况下都是纯粹的胜利,因为头文件列出了更less的不相关的细节。 所有客户端编译单元的编译时间也得到了改进。
性能
在许多情况下,性能损失很小,而且很less。 从长远来看,虚拟function的性能损失在数量级上。 我们正在讨论每个数据成员每个访问额外的解引用,以及pimpl的dynamic内存分配,以及在销毁时释放内存。 如果pimpl类没有经常访问其数据成员,那么“pimpl'd类”对象经常被创build并且是短暂的,那么dynamic分配就会超出额外的引用。
决策
我认为性能至关重要的类,例如一个额外的引用或内存分配会造成显着的差异,不pipe怎样,都不应该使用pimpl。 在这种性能下降的基础类中,如果编译时间显着提高,那么头文件被广泛地包含在内可能应该使用pimpl。 如果编译时间没有缩短,那么这就代表了您的代码清晰度。
对于所有其他情况,这完全是一个味道问题。 在做出决定之前,尝试一下并测量运行时性能和编译时性能。
pImpl在实现std :: swap和operator =时有很强的exception保证,非常有用。 我倾向于说,如果你的课程支持其中的任何一个,并且有不止一个非平凡的领域,那么它通常不再是偏好。
否则,这是关于您希望客户端通过头文件绑定到实现的紧密程度。 如果二进制不兼容的变化不是问题,那么你可能没有太多的可维护性,虽然如果编译速度成为一个问题,通常有节省。
性能成本可能更多地与丢失内联有关,而不是间接的,但这是一个疯狂的猜测。
您可以随后添加pImpl,并声明从现在开始,客户端将不必因为添加私有字段而重新编译。
所以这些都没有提出一个全有或全无的方法。 你可以有select地为那些给你带来好处的class级做,而不是那些没有的class级,稍后改变你的想法。 像pImpl听起来像太多的devise实现迭代器的例子…
这个习语很大程度上帮助大型项目的编译时间。
外部链接
这也不错
我在我自己的图书馆的几个地方使用这个成语,在这两种情况下,干净地将接口从实现中分离出来。 例如,我有一个在.h文件中完全声明的XML读取器类,它具有一个在非公开的.h和.cpp文件中声明和定义的RealXMLReader类的PIMPL。 RealXMlReader反过来是我使用的XMLparsing器(目前是Expat)的便捷包装器。
这种安排允许我从未来的Expat转换到另一个XMLparsing器,而不必重新编译所有的客户端代码(当然我仍然需要重新链接)。
请注意,我不这样做的编译时性能的原因,只为了方便。 有几个PIMPL fabnatics谁坚持说,任何包含超过三个文件的项目将是不可编译的,除非你在整个使用PIMPL。 值得注意的是,这些人从来没有提供任何实际的证据,只是模糊地提到“拉特科斯”和“指数时间”。
当我们有r值语义时,pImpl将会工作得最好。
pImpl的“替代”,也将实现隐藏实现的细节,是使用抽象的基类,并把实现放在派生类。 用户调用某种“工厂”方法来创build实例,通常会使用一个指针(可能是共享的)到抽象类。
pImpl的基本原理可以是:
- 保存在一个V表。 是的,但你的编译器将内联所有的转发,你会真的保存任何东西。
- 如果你的模块包含了多个相互了解的类,但对外界却隐藏了这个类。
pImpl的容器类的语义可以是: – 不可复制的,不可赋值的。所以你“build立”你的pImpl并在销毁时“删除” – 共享。 所以你有shared_ptr而不是Impl *
使用shared_ptr,只要类在析构函数处完成,就可以使用前向声明。 应该定义你的析构函数,即使是默认的(它可能会是)。
-
热插拔。 你可以实现“可能是空的”并实现“交换”。 用户可以创build一个实例,并将非常量引用传递给它以填充“swap”。
-
2阶段build设。 你构造一个空的,然后调用“load()”来填充它。
共享是我有一个远程喜欢没有r值语义的唯一的一个。 有了它们,我们也可以实现不可复制的不可分配。 我喜欢能够调用一个给我的函数。
但是,我发现现在我更倾向于使用比pImpl更多的抽象基类,即使只有一个实现。
当我想避免一个头文件污染我的代码库时,我通常会使用它。 Windows.h就是最好的例子。 这是非常糟糕的行为,我宁愿自杀,也不愿意到处可见。 所以假设你想要一个基于类的API,把它隐藏在pimpl类后面就可以很好地解决这个问题。 (如果你只是愿意揭露个人function,那么当然,这些function可以在前面声明,而不必将它们放到pimpl类中)
我不会在任何地方使用pimpl,一方面是因为性能受到打击,另一方面是因为通常只有很less的额外工作。 它给你的主要是实现和接口之间的隔离。 通常情况下,这不是一个非常重要的事情。