C++ pImpl 成语真的在实践中使用吗?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/8972588/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Is the pImpl idiom really used in practice?
提问by Renan Greinert
I am reading the book "Exceptional C++" by Herb Sutter, and in that book I have learned about the pImpl idiom. Basically, the idea is to create a structure for the private
objects of a class
and dynamically allocate them to decrease the compilation time(and also hide the private implementations in a better manner).
我正在阅读 Herb Sutter 的“Exceptional C++”一书,在那本书中我了解了 pImpl 习语。基本上,这个想法是为 a 的private
对象创建一个结构class
并动态分配它们以减少编译时间(并以更好的方式隐藏私有实现)。
For example:
例如:
class X
{
private:
C c;
D d;
} ;
could be changed to:
可以改为:
class X
{
private:
struct XImpl;
XImpl* pImpl;
};
and, in the CPP, the definition:
并且,在 CPP 中,定义:
struct X::XImpl
{
C c;
D d;
};
This seems pretty interesting, but I have never seen this kind of approach before, neither in the companies I have worked, nor in open source projects that I've seen the source code. So, I am wondering it this technique is really used in practice?
这看起来很有趣,但我以前从未见过这种方法,无论是在我工作过的公司中,还是在我见过源代码的开源项目中。所以,我想知道这个技术真的在实践中使用吗?
Should I use it everywhere, or with caution? And is this technique recommended to be used in embedded systems (where the performance is very important)?
我应该在任何地方使用它,还是谨慎使用?是否建议将此技术用于嵌入式系统(性能非常重要)?
采纳答案by B?ови?
So, I am wondering it this technique is really used in practice? Should I use it everywhere, or with caution?
所以,我想知道这个技术真的在实践中使用吗?我应该在任何地方使用它,还是谨慎使用?
Of course it is used. I use it in my project, in almost every class.
当然是使用了。我在我的项目中使用它,几乎在每个课程中。
Reasons for using the PIMPL idiom:
使用 PIMPL 习语的原因:
Binary compatibility
二进制兼容性
When you're developing a library, you can add/modify fields to XImpl
without breaking the binary compatibility with your client (which would mean crashes!). Since the binary layout of X
class doesn't change when you add new fields to Ximpl
class, it is safe to add new functionality to the library in minor versions updates.
在开发库时,您可以添加/修改字段,XImpl
而不会破坏与客户端的二进制兼容性(这意味着崩溃!)。由于向X
类添加新字段时类的二进制布局不会改变Ximpl
,因此在次要版本更新中向库添加新功能是安全的。
Of course, you can also add new public/private non-virtual methods to X
/XImpl
without breaking the binary compatibility, but that's on par with the standard header/implementation technique.
当然,您也可以在不破坏二进制兼容性的情况下向X
/添加新的公共/私有非虚拟方法XImpl
,但这与标准头文件/实现技术相当。
Data hiding
数据隐藏
If you're developing a library, especially a proprietary one, it might be desirable not to disclose what other libraries / implementation techniques were used to implement the public interface of your library. Either because of Intellectual Property issues, or because you believe that users might be tempted to take dangerous assumptions about the implementation or just break the encapsulation by using terrible casting tricks. PIMPL solves/mitigates that.
如果您正在开发一个库,尤其是专有库,则最好不要透露使用了哪些其他库/实现技术来实现您的库的公共接口。要么是因为知识产权问题,要么是因为您认为用户可能会倾向于对实现进行危险的假设,或者只是通过使用可怕的转换技巧来破坏封装。PIMPL 解决/减轻了这个问题。
Compilation time
编译时间
Compilation time is decreased, since only the source (implementation) file of X
needs to be rebuilt when you add/remove fields and/or methods to the XImpl
class (which maps to adding private fields/methods in the standard technique). In practice, it's a common operation.
编译时间减少了,因为X
当您向类添加/删除字段和/或方法XImpl
(这映射到在标准技术中添加私有字段/方法)时,只需要重建源(实现)文件。在实践中,这是一个常见的操作。
With the standard header/implementation technique (without PIMPL), when you add a new field to X
, every client that ever allocates X
(either on stack, or on heap) needs to be recompiled, because it must adjust the size of the allocation. Well, every client that doesn't ever allocate X alsoneed to be recompiled, but it's just overhead (the resulting code on the client side will be the same).
使用标准标头/实现技术(无 PIMPL),当您向 中添加新字段时X
,X
需要重新编译分配(在堆栈上或堆上)的每个客户端,因为它必须调整分配的大小。好吧,每个从未分配过 X 的客户端也需要重新编译,但这只是开销(客户端的结果代码将相同)。
What is more, with the standard header/implementation separation XClient1.cpp
needs to be recompiled even when a private method X::foo()
was added to X
and X.h
changed, even though XClient1.cpp
can't possibly call this method for encapsulation reasons! Like above, it's pure overhead and is related with how real-life C++ build systems work.
更重要的是,XClient1.cpp
即使X::foo()
添加X
和X.h
更改私有方法,也需要重新编译标准头/实现分离,即使XClient1.cpp
由于封装原因不可能调用此方法!像上面一样,它是纯粹的开销,并且与现实生活中的 C++ 构建系统的工作方式有关。
Of course, recompilation is not needed when you just modify the implementation of the methods (because you don't touch the header), but that's on par with the standard header/implementation technique.
当然,当您只修改方法的实现时不需要重新编译(因为您不接触标题),但这与标准标题/实现技术相当。
Is this technique recommended to be used in embedded systems (where the performance is very important)?
是否建议将此技术用于嵌入式系统(性能非常重要)?
That depends on how powerful your target is. However the only answer to this question is: measure and evaluate what you gain and lose. Also, take into consideration that if you're not publishing a library meant to be used in embedded systems by your clients, only the compilation time advantage applies!
这取决于你的目标有多强大。然而,这个问题的唯一答案是:衡量和评估您的所得和损失。此外,请考虑到,如果您不发布旨在供客户在嵌入式系统中使用的库,则只有编译时间优势适用!
回答by PlasmaHH
It seems that a lot of libraries out there use it to stay stable in their API, at least for some versions.
似乎有很多库使用它来保持 API 的稳定,至少在某些版本中是这样。
But as for all things, you should never use anything everywhere without caution. Always think before using it. Evaluate what advantages it gives you, and if they are worth the price you pay.
但至于所有的东西,你不应该在任何地方不小心使用任何东西。在使用它之前总是三思而后行。评估它为您带来的优势,以及它们是否值得您付出的代价。
The advantages it maygive you are:
它可能给你带来的好处是:
- helps in keeping binary compatibility of shared libraries
- hiding certain internal details
- decreasing recompilation cycles
- 有助于保持共享库的二进制兼容性
- 隐藏某些内部细节
- 减少重新编译周期
Those may or may not be real advantages to you. Like for me, I don't care about a few minutes recompilation time. End users usually also don't, as they always compile it once and from the beginning.
这些对你来说可能是也可能不是真正的优势。对我来说,我不在乎几分钟的重新编译时间。最终用户通常也不会,因为他们总是从头编译一次。
Possible disadvantages are (also here, depending on the implementation and whether they are real disadvantages for you):
可能的缺点是(也在这里,取决于实施以及它们是否对您来说是真正的缺点):
- Increase in memory usage due to more allocations than with the na?ve variant
- increased maintenance effort (you have to write at least the forwarding functions)
- performance loss (the compiler may not be able to inline stuff as it is with a na?ve implementation of your class)
- 由于比 na?ve 变体更多的分配,内存使用量增加
- 增加维护工作(您必须至少编写转发功能)
- 性能损失(编译器可能无法内联内容,因为它是您的类的幼稚实现)
So carefully give everything a value, and evaluate it for yourself. For me, it almost always turns out that using the pimpl idiom is not worth the effort. There is only one case where I personally use it (or at least something similar):
所以仔细地给一切一个价值,并为自己评估。对我来说,结果几乎总是证明使用 pimpl 成语是不值得的。只有一种情况我个人使用它(或至少类似的东西):
My C++ wrapper for the linux stat
call. Here the struct from the C header may be different, depending on what #defines
are set. And since my wrapper header can't control all of them, I only #include <sys/stat.h>
in my .cxx
file and avoid these problems.
我的 linuxstat
调用的C++ 包装器。这里来自 C 头文件的结构可能会有所不同,具体取决于#defines
设置的内容。而且由于我的包装头无法控制所有这些,我只#include <sys/stat.h>
在我的.cxx
文件中避免这些问题。
回答by Emilio Garavaglia
Agree with all the others about the goods, but let me put in evidence a limit: doesn't work well with templates.
同意所有其他关于商品的观点,但让我证明一个限制:不适用于模板。
The reason is that template instantiation requires the full declaration available where the instantiation took place. (And that's the main reason you don't see template methods defined into CPP files)
原因是模板实例化需要在实例化发生处可用的完整声明。(这就是您看不到 CPP 文件中定义的模板方法的主要原因)
You can still refer to templetised subclasses, but since you have to include them all, every advantage of "implementation decoupling" on compiling (avoiding to include all platoform specific code everywhere, shortening compilation) is lost.
您仍然可以引用模板化的子类,但是由于您必须将它们全部包含在内,“实现解耦”在编译(避免在任何地方包含所有平台特定代码,缩短编译)的所有优势都将丢失。
Is a good paradigm for classic OOP (inheritance based) but not for generic programming (specialization based).
是经典 OOP(基于继承)的良好范例,但不适用于泛型编程(基于专业化)。
回答by Emilio Garavaglia
Other people have already provided the technical up/downsides, but I think the following is worth noting:
其他人已经提供了技术上的优势/劣势,但我认为以下几点值得注意:
First and foremost, don't be dogmatic. If pImpl works for your situation, use it - don't use it just because "it's better OO since it reallyhides implementation" etc. Quoting the C++ FAQ:
首先,不要教条主义。如果 pImpl 适用于您的情况,请使用它 - 不要仅仅因为“它更好的面向对象,因为它确实隐藏了实现”等。引用 C++ 常见问题解答:
encapsulation is for code, not people (source)
封装是针对代码的,而不是针对人的(来源)
Just to give you an example of open source software where it is used and why: OpenThreads, the threading library used by the OpenSceneGraph. The main idea is to remove from the header (e.g. <Thread.h>
) all platform-specific code, because internal state variables (e.g. thread handles) differ from platform to platform. This way one can compile code against your library without any knowledge of the other platforms' idiosyncrasies, because everything is hidden.
只是给您一个使用它的开源软件示例以及原因:OpenThreads,OpenSceneGraph使用的线程库。主要思想是从头文件(例如<Thread.h>
)中删除所有特定于平台的代码,因为内部状态变量(例如线程句柄)因平台而异。通过这种方式,您可以在不了解其他平台特性的情况下针对您的库编译代码,因为一切都是隐藏的。
回答by Ghita
I would mainly consider PIMPL for classes exposed to be used as an API by other modules. This has many benefits, as it makes recompilation of the changes made in the PIMPL implementation does not affect the rest of the project. Also, for API classes they promote a binary compatibility (changes in a module implementation do not affect clients of those modules, they don't have to be recompiled as the new implementation has the same binary interface - the interface exposed by the PIMPL).
我将主要考虑 PIMPL 用于暴露以供其他模块用作 API 的类。这有很多好处,因为它使在 PIMPL 实现中所做的更改的重新编译不会影响项目的其余部分。此外,对于 API 类,它们促进了二进制兼容性(模块实现中的更改不会影响这些模块的客户端,它们不必重新编译,因为新实现具有相同的二进制接口 - PIMPL 公开的接口)。
As for using PIMPL for every class, I would consider caution because all those benefits come at a cost: an extra level of indirection is required in order to access the implementation methods.
至于对每个类使用 PIMPL,我会考虑谨慎,因为所有这些好处都是有代价的:为了访问实现方法,需要额外的间接级别。
回答by user377178
I think this is one of the most fundamental tools for decoupling.
我认为这是解耦最基本的工具之一。
I was using pimpl (and many other idioms from Exceptional C++) on embedded project (SetTopBox).
我在嵌入式项目 (SetTopBox) 上使用了 pimpl(以及来自 Exceptional C++ 的许多其他习语)。
The particular purpose of this idoim in our project was to hide the types XImpl class uses. Specifically we used it to hide details of implementations for different hardware, where different headers would be pulled in. We had different implementations of XImpl classes for one platform and different for the other. Layout of class X stayed the same regardless of the platfrom.
在我们的项目中这个 idoim 的特殊目的是隐藏 XImpl 类使用的类型。具体来说,我们用它来隐藏不同硬件的实现细节,其中将引入不同的头文件。我们有一个平台的 XImpl 类的不同实现,而另一个平台则不同。无论平台如何,X 类的布局都保持不变。
回答by CashCow
I used to use this technique a lot in the past but then found myself moving away from it.
过去我经常使用这种技术,但后来发现自己逐渐远离它。
Of course it is a good idea to hide the implementation detail away from the users of your class. However you can also do that by getting users of the class to use an abstract interface and for the implementation detail to be the concrete class.
当然,对类的用户隐藏实现细节是个好主意。但是,您也可以通过让类的用户使用抽象接口并将实现细节作为具体类来做到这一点。
The advantages of pImpl are:
pImpl 的优点是:
Assuming there is just one implementation of this interface, it is clearer by not using abstract class / concrete implementation
If you have a suite of classes (a module) such that several classes access the same "impl" but users of the module will only use the "exposed" classes.
No v-table if this is assumed to be a bad thing.
假设这个接口只有一个实现,不使用抽象类/具体实现会更清楚
如果您有一组类(一个模块),以便多个类访问相同的“impl”,但模块的用户将只使用“公开的”类。
如果认为这是一件坏事,则没有 v-table。
The disadvantages I found of pImpl (where abstract interface works better)
我发现 pImpl 的缺点(抽象接口效果更好)
Whilst you may have only one "production" implementation, by using an abstract interface you can also create a "mock" inmplementation that works in unit testing.
(The biggest issue). Before the days of unique_ptr and moving you had restricted choices as to how to store the pImpl. A raw pointer and you had issues about your class being non-copyable. An old auto_ptr wouldn't work with forwardly declared class (not on all compilers anyway). So people started using shared_ptr which was nice in making your class copyable but of course both copies had the same underlying shared_ptr which you might not expect (modify one and both are modified). So the solution was often to use raw pointer for the inner one and make the class non-copyable and return a shared_ptr to that instead. So two calls to new. (Actually 3 given old shared_ptr gave you a second one).
Technically not really const-correct as the constness isn't propagated through to a member pointer.
虽然您可能只有一个“生产”实现,但通过使用抽象接口,您还可以创建一个在单元测试中工作的“模拟”实现。
(最大的问题)。在 unique_ptr 和 move 出现之前,您对如何存储 pImpl 的选择受到限制。一个原始指针,你有关于你的类不可复制的问题。旧的 auto_ptr 不适用于前向声明的类(无论如何不适用于所有编译器)。所以人们开始使用 shared_ptr ,这很好地使您的类可复制,但当然两个副本都具有您可能不期望的相同底层 shared_ptr (修改一个,两个都被修改)。因此,解决方案通常是使用原始指针作为内部指针,并使类不可复制并返回一个 shared_ptr 来代替。所以两次调用new。(实际上 3 给了旧的 shared_ptr 给了你第二个)。
从技术上讲并不是真正的常量正确,因为常量不会传播到成员指针。
In general I have therefore moved away in the years from pImpl and into abstract interface usage instead (and factory methods to create instances).
总的来说,这些年来我已经从 pImpl 转移到抽象接口的使用(以及创建实例的工厂方法)。
回答by ncsc
As many other said, the Pimpl idiom allows to reach complete information hiding and compilation independency, unfortunately with the cost of performance loss (additional pointer indirection) and additional memory need (the member pointer itself). The additional cost can be critical in embedded software development, in particular in those scenarios where memory must be economized as much as possible. Using C++ abstract classes as interfaces would lead to the same benefits at the same cost. This shows actually a big deficiency of C++ where, without recurring to C-like interfaces (global methods with an opaque pointer as parameter), it is not possible to have true information hiding and compilation independency without additional resource drawbacks: this is mainly because the declaration of a class, which must be included by its users, exports not only the interface of the class (public methods) needed by the users, but also its internals (private members), not needed by the users.
正如许多其他人所说,Pimpl 习惯用法允许实现完整的信息隐藏和编译独立性,不幸的是,性能损失(额外的指针间接)和额外的内存需求(成员指针本身)。在嵌入式软件开发中,额外的成本可能至关重要,尤其是在必须尽可能节省内存的情况下。使用 C++ 抽象类作为接口将以相同的成本获得相同的好处。这实际上显示了 C++ 的一大缺陷,其中,如果不重复使用类似 C 的接口(具有不透明指针作为参数的全局方法),就不可能在没有额外资源缺陷的情况下实现真正的信息隐藏和编译独立性:这主要是因为类的声明,必须由其用户包含,
回答by Kit10
Here is an actual scenario I encountered, where this idiom helped a great deal. I recently decided to support DirectX 11, as well as my existing DirectX 9 support, in a game engine. The engine already wrapped most DX features, so none of the DX interfaces were used directly; they were just defined in the headers as private members. The engine utilizes DLLs as extensions, adding keyboard, mouse, joystick, and scripting support, as week as many other extensions. While most of those DLLs did not use DX directly, they required knowledge and linkage to DX simply because they pulled in headers that exposed DX. In adding DX 11, this complexity was to increase dramatically, however unnecessarily. Moving the DX members into a Pimpl defined only in the source eliminated this imposition. On top of this reduction of library dependencies, my exposed interfaces became cleaner as moved private member functions into the Pimpl, exposing only front facing interfaces.
这是我遇到的一个实际场景,这个习语帮助很大。我最近决定在游戏引擎中支持 DirectX 11 以及我现有的 DirectX 9 支持。引擎已经封装了大部分DX特性,所以没有直接使用DX接口;它们只是在标题中定义为私有成员。该引擎使用 DLL 作为扩展,添加键盘、鼠标、操纵杆和脚本支持,就像许多其他扩展一样。虽然这些 DLL 中的大多数不直接使用 DX,但它们需要知识和与 DX 的链接,因为它们引入了暴露 DX 的头文件。在添加 DX 11 后,这种复杂性会显着增加,但这是不必要的。将 DX 成员移动到仅在源中定义的 Pimpl 中消除了这种拼版。除了减少库依赖之外,
回答by Holger Kretzschmar
It is used in practice in a lot of projects. It's usefullness depends heavily on the kind of project. One of the more prominent projects using this is Qt, where the basic idea is to hide implementation or platformspecific code from the user (other developers using Qt).
它在许多项目中被实际使用。它的实用性在很大程度上取决于项目的类型。使用它的比较突出的项目之一是Qt,其基本思想是对用户(使用 Qt 的其他开发人员)隐藏实现或特定于平台的代码。
This is a noble idea but there is a real drawback to this: debugging As long as the code hidden in private implemetations is of premium quality this is all well, but if there are bugs in there, then the user/developer has a problem, because it just a dumb pointer to a hidden implementation, even if he has the implementations source code.
这是一个崇高的想法,但它有一个真正的缺点:调试只要隐藏在私有实现中的代码具有优质的质量,这一切都很好,但是如果其中存在错误,那么用户/开发人员就会遇到问题,因为它只是一个指向隐藏实现的愚蠢指针,即使他有实现源代码。
So as in nearly all design decisions there a pros and cons.
因此,几乎所有的设计决策都有利有弊。