pImpl成语在实践中是否真的被使用?
我正在阅读Herb Sutter的书“Exceptional C ++”,并且在那本书中我学习了这个pImpl习语。 基本上,这个想法是为一个class
的private
对象创build一个结构,并dynamic地分配它们来减less编译时间 (并且也以更好的方式隐藏私有实现)。
例如:
class X { private: C c; D d; } ;
可以改成:
class X { private: struct XImpl; XImpl* pImpl; };
在CPP中定义如下:
struct X::XImpl { C c; D d; };
这看起来很有趣,但是我从来没有见过这种方法,在我工作过的公司,也没有见过源代码的开源项目。 那么,我想知道这个技术在实际中是否真的被使用了?
我应该在任何地方或谨慎使用吗? 这种技术是否被推荐用于embedded式系统(性能非常重要)?
那么,我想知道这个技术在实际中是否真的被使用了? 我应该在任何地方或谨慎使用吗?
当然,在我的项目中,几乎每个class级都会使用它,原因有几个:
- 数据隐藏
- 重新编译的时间真的减less了,因为只有源文件需要重build,而不是头文件和每个包含它的文件
- 二进制兼容。 由于类声明没有改变,因此更新库是安全的(假设你正在创build一个库)
这种技术是否被推荐用于embedded式系统(性能非常重要)?
这取决于你的目标是多么强大。 然而,这个问题的唯一答案是:衡量和评估你得到和失去的东西。
似乎有很多库在那里使用它来保持稳定的API,至less对于某些版本。
但至于万事万物,千万不要无所顾忌。 在使用之前一定要考虑。 评估它给你什么好处,如果他们是值得你付出的代价。
它可能给你的好处是:
- 有助于保持共享库的二进制兼容性
- 隐藏某些内部细节
- 减less重编译周期
这些对你来说可能是也可能不是真正的好处。 对我来说,我不在乎几分钟的重新编译时间。 最终用户通常也不会,因为他们总是从头开始编译一次。
可能的缺点是(也在这里,取决于实施,以及它们是否是真正的缺点):
- 内存使用增加,由于分配更多,比天真的变种
- 增加维护工作量(至less要编写转发function)
- 性能上的损失(编译器可能无法像内部实现类一样内联内容)
所以小心给一切都有价值,并为自己评估。 对我来说,几乎总是使用pimpl习语是不值得的。 只有一个我个人使用的情况(或者至less类似的情况):
我的C ++包装的Linux stat
调用。 这里来自C头的结构可能会有所不同,具体取决于设置了哪些#defines
。 而且由于我的包装头不能控制所有这些,所以我只在我的.cxx
文件中#include <sys/stat.h>
并避免这些问题。
同意所有其他的货物,但让我certificate一个限制: 不适用于模板 。
原因是模板实例化需要在实例化发生的地方提供完整的声明。 (这是你没有看到定义到CPP文件中的模板方法的主要原因)
你仍然可以参考模板化的子类,但是由于你必须包含所有的子类,所以编译时的“实现解耦”(避免包括所有平台特定的代码,缩短编译)的每一个好处都会丢失。
对于经典的OOP(基于inheritance)是一个很好的范例,但不适用于generics编程(基于专业化)。
其他人已经提供了技术上的/下降,但我认为以下值得注意的是:
首先,不要教条。 如果pImpl适用于您的情况,请使用它 – 不要仅仅因为“它更好的OO,因为它真的隐藏了实现”而使用它等等。引用C ++ FAQ:
封装是为了代码,而不是人( 源 )
只是给你一个开源软件的例子,它使用的原因是:OpenThreads, OpenSceneGraph使用的线程库。 主要思想是从头文件(例如<Thread.h>
)中移除所有平台特定的代码,因为内部状态variables(例如线程句柄)因平台而异。 通过这种方式,您可以在不了解其他平台的特性的情况下,针对您的库编译代码,因为所有内容都是隐藏的。
我主要考虑将PIMPL用于暴露其他模块作为API使用的类。 这有很多好处,因为它使PIMPL实现中所做更改的重新编译不会影响项目的其余部分。 另外,对于API类,它们提高了二进制兼容性(模块实现中的更改不会影响这些模块的客户端,因为新实现具有相同的二进制接口(由PIMPL公开的接口),所以不必重新编译。
至于每个类使用PIMPL,我会考虑注意,因为所有这些好处都是有代价的:为了访问实现方法需要额外的间接级别。
我认为这是解耦的最基本的工具之一。
我在embedded式项目(SetTopBox)上使用pimpl(以及Exceptional C ++中的许多其他成语)。
这个idoim在我们的项目中的特殊目的是隐藏XImpl类使用的types。 具体来说,我们用它来隐藏不同硬件的实现细节,不同的头文件将被拉进来。我们在一个平台上有不同的XImpl类实现,而另一个平台则不同。 不pipe是什么平台,Xclass的布局都一样。
过去我曾经使用过这种技术,但后来却发现自己已经离开了这个技术。
当然,将实现细节远离您class级的用户是个好主意。 但是,您也可以通过让类的用户使用抽象接口并将实现细节作为具体类来实现。
pImpl的优点是:
-
假设这个接口只有一个实现,通过不使用抽象类/具体实现就更清楚了
-
如果你有一套类(一个模块),使得几个类访问相同的“impl”,但模块的用户将只使用“暴露”的类。
-
没有V表,如果这被认为是一件坏事。
我发现pImpl的缺点(抽象界面效果更好)
-
虽然您可能只有一个“生产”实现,但通过使用抽象接口,您也可以创build一个在模块testing中工作的“模拟”实现。
-
(最大的问题)。 在unique_ptr和移动的日子之前,你已经限制了如何存储pImpl的select。 一个原始的指针,你有关于你的类是不可复制的问题。 一个旧的auto_ptr不能使用向前声明的类(不是所有的编译器)。 所以人们开始使用shared_ptr,这是很好的让你的类可复制,但当然这两个副本有相同的底层shared_ptr,你可能不希望(修改一个,都被修改)。 因此,解决scheme通常是使用内部原始指针,并使该类不可复制,并返回一个shared_ptr。 所以两个电话来新的。 (其实3给老的shared_ptr给了你第二个)。
-
从技术上讲,const不是正确的,因为const不会传播到成员指针。
因此,一般来说,我已经从pImpl转移到了抽象接口使用(而工厂方法来创build实例)。
正如许多其他人所说,Pimpl习语允许达到完全的信息隐藏和编译独立性,不幸的是性能损失(额外的指针间接)和额外的内存需求(成员指针本身)的代价。 在embedded式软件开发中,额外的成本可能是至关重要的,特别是在内存必须尽可能节约的情况下。 使用C ++抽象类作为接口将以相同的成本获得相同的好处。 这实际上显示了C ++的一个很大的缺陷,那就是没有重复出现类似C的接口(用不透明的指针作为参数的全局方法),没有额外的资源缺陷就不可能有真正的信息隐藏和编译独立性:这主要是因为必须由用户包含的类的声明不仅输出用户所需的类(公共方法)的接口,而且还输出用户不需要的内部(私有成员)的接口。
它在很多项目中用于实践。 它的用处很大程度上取决于项目的种类。 使用它的一个比较突出的项目是Qt ,其基本思想是隐藏用户(使用Qt的其他开发人员)的实现或特定于平台的代码。
这是一个高尚的想法,但这是一个真正的缺点:debugging只要隐藏在私有实现中的代码具有高质量,这一切都是好事,但是如果在那里存在错误,那么用户/开发人员有问题,因为它只是一个隐藏的实现的愚蠢的指针,即使他有实现的源代码。
所以在几乎所有的devise决策中都有利弊。
我能看到的一个好处是它允许程序员以相当快的方式执行某些操作:
X( X && move_semantics_are_cool ) : pImpl(NULL) { this->swap(move_semantics_are_cool); } X& swap( X& rhs ) { std::swap( pImpl, rhs.pImpl ); return *this; } X& operator=( X && move_semantics_are_cool ) { return this->swap(move_semantics_are_cool); } X& operator=( const X& rhs ) { X temporary_copy(rhs); return this->swap(temporary_copy); }
PS:我希望我不会误解移动语义。
这是我遇到的一个实际场景,这个习语很有帮助。 我最近决定在游戏引擎中支持DirectX 11以及我现有的DirectX 9支持。 引擎已经包装了大部分的DXfunction,所以没有一个DX接口被直接使用; 他们只是在标题中定义为私人成员。 该引擎利用DLL作为扩展,添加键盘,鼠标,游戏杆和脚本支持,与其他许多扩展一样。 虽然大多数这些DLL不直接使用DX,但是他们需要知识和与DX的链接,因为他们只是拉入了暴露DX的头文件。 在增加DX 11的时候,这个复杂性会显着增加,但是不必要的。 将DX成员移动到仅在源代码中定义的Pimpl中,从而消除了这种强制。 除了减less库依赖关系之外,我公开的接口也变得更清洁了,因为私有成员函数移动到了Pimpl中,只暴露了前端接口。