在实践中真的使用了pImpl成语吗?


165

我正在阅读Herb Sutter撰写的“ Exceptional C ++”一书,在那本书中,我了解了pImpl习惯用法。基本上,该想法是为a的private对象创建一个结构class并动态分配它们以减少编译时间(并以更好的方式隐藏私有实现)。

例如:

class X
{
private:
  C c;
  D d;  
} ;

可以更改为:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

以及在CPP中的定义:

struct X::XImpl
{
  C c;
  D d;
};

这似乎很有趣,但是我以前从未见过这种方法,无论是在我工作的公司还是在看过源代码的开源项目中,我都从未见过。因此,我想知道这种技术是否真的在实践中使用?

我应该在任何地方使用它还是要谨慎使用?是否建议将此技术用于性能非常重要的嵌入式系统?


这与确定X是(抽象)接口,Xilp是实现的本质上是相同的吗?struct XImpl : public X。对我来说,这很自然。还有其他我想念的问题吗?
亚伦McDaid

@AaronMcDaid:相似,但具有以下优点:(a)成员函数不必是虚拟的,并且(b)不需要实例化工厂或实现类的定义。
迈克·西摩

2
@AaronMcDaid pimpl习惯用法避免了虚函数调用。它也有点C ++风格(对于C ++风格的某些概念);您调用构造函数,而不是工厂函数。我已经使用了这两种方法,具体取决于现有代码库中的内容-pimpl惯用语(最初称为Cheshire cat惯用语,并且比Herb对它的描述至少早了5年)似乎具有更长的历史并且更加在C ++中被广泛使用,但是在其他情况下两者都可以工作。
James Kanze 2012年

30
在C ++中,pimpl应该使用const unique_ptr<XImpl>而不是实现XImpl*
尼尔摹

1
“在我工作过的公司或开源项目中,从未见过这种方法。” Qt几乎从未使用过它。
ManuelSchneid3r

Answers:


132

因此,我想知道这种技术是否真的在实践中使用?我应该在任何地方使用它还是要谨慎使用?

当然可以使用。我几乎在每个班级都在我的项目中使用它。


使用PIMPL习惯用法的原因:

二进制兼容性

在开发库时,可以在XImpl不破坏与客户端的二进制兼容性的情况下添加/修改字段(这意味着崩溃!)。由于在XXimpl类添加新字段时类的二进制布局不会更改,因此可以安全地在次要版本更新中向库中添加新功能。

当然,您也可以在不破坏二进制兼容性的情况下向X/ 添加新的公共/私有非虚拟方法XImpl,但这与标准的标头/实现技术相当。

资料隐藏

如果您正在开发一个库,尤其是专有库,则可能不希望公开用于实现库公共接口的其他库/实现技术。要么是由于知识产权问题,要么是因为您认为用户可能会被诱使对实现进行危险的假设,或者只是通过使用可怕的转换技巧来破坏封装。PIMPL解决/缓解了这一难题。

编译时间

减少了编译时间,因为X当您向类中添加/删除字段和/或方法时XImpl(仅映射标准方法中的添加私有字段/方法),仅需要重建的源(实现)文件。实际上,这是一种常见的操作。

使用标准的标头/实现技术(没有PIMPL),当您向中添加新字段时X,曾经分配过的每个客户端X(无论是在堆栈上还是在堆上)都需要重新编译,因为它必须调整分配的大小。好吧,每个从未分配X的客户端也都需要重新编译,但这只是开销(客户端上的结果代码是相同的)。

而且,即使添加和更改XClient1.cpp了私有方法,即使出于封装原因无法调用此方法,也需要重新编译标准的标头/实现分隔符!像上面一样,这是纯开销,并且与现实C ++构建系统的工作方式有关。X::foo()XX.hXClient1.cpp

当然,当您仅修改方法的实现时(因为您不触摸标题)就不需要重新编译,但这与标准的标题/实现技术相当。


是否建议将此技术用于性能非常重要的嵌入式系统中?

这取决于目标的强大程度。但是,这个问题的唯一答案是:衡量和评估您的得失。另外,请考虑到,如果您不发布客户打算在嵌入式系统中使用的库,则仅适用于编译时间!


16
+1是因为出于同样的原因,它也在我工作的公司中广泛使用。
Benoit 2012年

9
同样,二进制兼容性
Ambroz Bizjak 2012年

9
在Qt库中,此方法还用于智能指针情况。因此,QString在内部将其内容保留为不可变类。当“复制”公共类时,将复制私有成员的指针而不是整个私有类。这些私有类然后也使用智能指针,因此,除了指针复制而不是全类复制而导致的性能大大提高以外,您基本上还可以对大多数类进行垃圾回收
Timothy Baldridge 2012年

8
更重要的是,借助pimpl惯用语,Qt可以在单个主要版本中保持向前和向后二进制兼容性(在大多数情况下)。IMO这是迄今为止使用它的最重要的原因。
whitequark 2012年

1
这对于实现平台特定的代码也很有用,因为您可以保留相同的API。
doc

49

似乎很多库都在使用它来保持其API的稳定,至少对于某些版本而言是如此。

但是,对于所有事物,绝对不要在任何地方都不要使用任何东西。使用前请务必三思。评估它给您带来的好处,以及是否值得您付出代价。

可能给您带来的好处是:

  • 帮助保持共享库的二进制兼容性
  • 隐藏某些内部细节
  • 减少重新编译周期

这些可能对您来说不是真正的优势。像我一样,我不在乎几分钟的重新编译时间。最终用户通常也不会,因为他们总是从头开始编译一次。

可能的缺点是(也取决于实现方式以及它们是否对您来说是真正的缺点):

  • 与天真的变体相比,由于分配更多,导致内存使用量增加
  • 维护工作量增加(您必须至少编写转发功能)
  • 性能损失(编译器可能无法内联东西,就像您的类的天真的实现一样)

因此,请仔细为所有内容赋予价值,并自己进行评估。对我来说,几乎总是证明使用pimpl习惯是不值得的。我个人使用它(或至少类似的东西)的情况只有一种:

我的Linux stat调用的C ++包装器。在此,根据#defines设置的内容,C头的结构可能有所不同。而且由于我的包装标头无法控制所有这些标头,因此我只#include <sys/stat.h>.cxx文件中使用,避免了这些问题。


2
它应该几乎总是用于系统接口,以使接口代码系统独立。例如,我的File类(公开了很多信息stat将在Unix下返回)在Windows和Unix下使用相同的接口。
James Kanze 2012年

5
@JamesKanze:即使是我个人,也要先坐一会儿,然后想一想是否有足够#ifdef的时间使包装纸尽可能薄。但是每个人都有不同的目标,重要的是要花一些时间思考它,而不是盲目地遵循某些东西。
PlasmaHH 2012年

31

与所有其他人就商品达成一致,但让我证明一个限制:不适用于模板

原因是模板实例化要求在实例化发生的地方有可用的完整声明。(这是您看不到CPP文件中定义的模板方法的主要原因)

您仍然可以引用模板化的子类,但是由于必须将它们全部包括在内,因此在编译时“实现解耦”的所有优点(避免在所有地方都包含所有平台特定的代码,从而缩短了编译时间)都丢失了。

对于经典的OOP(基于继承)是一个很好的范例,但对于通用编程(基于专业化)则不是。


4
您必须更加精确: PIMPL类用作模板类型参数时绝对没有问题。仅当实现类本身需要在外部类的模板参数上进行参数化时,再也不能从接口头中将其隐藏,即使它仍然是私有类。如果您可以删除模板参数,那么您当然仍然可以“正确”使用PIMPL。使用类型删除,您还可以在基础非模板类中进行PIMPL,然后让模板类从其派生。
恢复莫妮卡2014年

22

其他人已经提供了技术上的优点/缺点,但是我认为以下几点值得注意:

首先,不要教条主义。如果pImpl适用于您的情况,请使用它-不要仅仅因为“它更好地面向对象,因为它确实隐藏了实现”等原因而使用它。引用C ++常见问题解答:

封装用于代码,而不是人员(

仅给您一个使用开源软件的示例以及使用原因:OpenThreads,OpenSceneGraph使用的线程库。主要思想是从标头中删除<Thread.h>所有特定于平台的代码,因为内部状态变量(例如线程句柄)因平台而异。这样一来,您就可以在不了解其他平台特性的情况下针对您的库编译代码,因为所有内容都是隐藏的。


12

我主要考虑将PIMPL用于暴露给其他模块用作API的类。这有很多好处,因为它可以重新编译PIMPL实现中所做的更改,而不会影响项目的其余部分。此外,对于API类,它们促进了二进制兼容性(模块实现中的更改不会影响这些模块的客户端,因此不必重新编译它们,因为新实现具有相同的二进制接口,即PIMPL公开的接口)。

至于对每个类使用PIMPL,我会考虑谨慎,因为所有这些好处都是有代价的:为了访问实现方法,需要额外的间接级别。


“为了访问实现方法,需要额外的间接级别。” 它是?
xaxxon

@xaxxon是的。如果方法水平较低,则pimpl较慢。例如,切勿将其用于生活紧张的事物。
Erik Aronesty

@xaxxon我会说一般情况下需要额外的级别。如果执行内联,则执行否。但是,在不同的dll中编译的代码中,linlining不是一个选择。
Ghita

5

我认为这是最基本的去耦工具之一。

我在嵌入式项目(SetTopBox)上使用pimpl(以及Exceptional C ++的许多其他用法)。

这个idoim在我们的项目中的特殊目的是隐藏XImpl类使用的类型。具体来说,我们用它来隐藏不同硬件的实现细节,在其中引入了不同的头。对于一个平台,我们有不同的XImpl类实现,而对于另一个平台,则有不同的实现。不论平台如何,X类的布局均保持不变。


4

过去,我过去经常使用这种技术,但后来发现自己逐渐摆脱了这种技术。

当然,对班级用户隐藏实现细节是个好主意。但是,您也可以通过让类的用户使用抽象接口并将实现细节作为具体的类来实现。

pImpl的优点是:

  1. 假设此接口只有一个实现,那么不使用抽象类/具体实现就更清楚了

  2. 如果您具有一组类(一个模块),以便多个类访问相同的“ impl”,但是该模块的用户将仅使用“暴露的”类。

  3. 如果认为这是一件坏事,则没有v表。

我发现了pImpl的缺点(其中抽象接口效果更好)

  1. 尽管您可能只有一个“生产”实现,但通过使用抽象接口,您也可以创建在单元测试中可用的“模拟”实现。

  2. (最大的问题)。在unique_ptr移动之前,关于如何存储pImpl的选择有限。原始指针,您遇到类不可复制的问题。旧的auto_ptr不适用于预先声明的类(无论如何,不​​适用于所有编译器)。因此,人们开始使用shared_ptr,这很好地使您的类具有可复制性,但是当然,两个副本都具有您可能不会期望的相同的基础shared_ptr(修改一个,并且两个都被修改)。因此,解决方案通常是对内部指针使用原始指针,并使该类不可复制,并向该指针返回一个shared_ptr。因此,有两个电话是new。(实际上3个旧的shared_ptr给了您第二个)。

  3. 从技术上讲,并不是真正的const正确,因为constness不会传播到成员指针。

因此,总的来说,多年来,我已经从pImpl转移到了抽象接口的使用(以及创建实例的工厂方法)。


3

正如许多其他人所说,Pimpl习惯用法允许达到完整的信息隐藏和编译独立性,不幸的是,这会带来性能损失(额外的指针间接)和额外的内存需求(成员指针本身)的代价。额外的成本对于嵌入式软件开发至关重要,尤其是在必须尽可能节省内存的情况下。使用C ++抽象类作为接口将以相同的成本带来相同的好处。这实际上表明了C ++的一个巨大缺陷,在这种情况下,如果不重复使用类似C的接口(带有不透明指针作为参数的全局方法),就不可能具有真正的信息隐藏和编译独立性,而不会产生其他资源缺陷:这主要是因为类的声明,该类必须由其用户包括在内,


3

这是我遇到的实际情况,这种习语在很大程度上帮助了我。我最近决定在游戏引擎中支持DirectX 11以及我现有的DirectX 9支持。该引擎已经包装了大多数DX功能,因此没有一个DX接口可以直接使用。它们只是在标头中定义为私有成员。该引擎利用DLL作为扩展,与其他扩展一样增加了键盘,鼠标,操纵杆和脚本支持。尽管大多数这些DLL都不直接使用DX,但它们仅需通过公开DX的头文件就需要知识和与DX的链接。在添加DX 11时,此复杂性将急剧增加,但这是不必要的。将DX成员移动到仅在源代码中定义的Pimpl中消除了此强制。除了减少库依赖之外,


2

它在许多项目中实践中使用。它的有用性在很大程度上取决于项目的类型。Qt是使用它的最杰出的项目之一,其基本思想是向用户(其他使用Qt的开发人员)隐藏实现或特定于平台的代码。

这是一个高尚的主意,但是它有一个真正的缺点:调试只要隐藏在私有实现中的代码具有较高的质量,就可以了,但是如果其中存在错误,则用户/开发人员会遇到问题,因为即使他有实现源代码,它也只是指向隐藏实现的愚蠢指针。

因此,几乎在所有设计决策中都有利弊。


9
它很愚蠢,但却被键入了...为什么您不能遵循调试器中的代码?
UncleZeiv 2012年

2
一般来说,要调试成Qt代码,您需要自己构建Qt。一旦完成,就可以毫无问题地进入PIMPL方法,并检查PIMPL数据的内容。
恢复莫妮卡2014年

0

我看到的一个好处是,它允许程序员以相当快的方式实现某些操作:

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:我希望我不要误会动作语义。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.