pImpl成语是否真的在实践中使用?

Ren*_*ert 157 c++ oop pimpl-idiom

我正在阅读Herb Sutter的"Exceptional C++"一书,在那本书中我学到了关于pImpl的习语.基本上,我们的想法是为a的private对象创建一个结构class并动态分配它们以减少编译时间(并且还以更好的方式隐藏私有实现).

例如:

class X
{
private:
  C c;
  D d;  
} ;
Run Code Online (Sandbox Code Playgroud)

可以改为:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};
Run Code Online (Sandbox Code Playgroud)

并且,在CPP中,定义:

struct X::XImpl
{
  C c;
  D d;
};
Run Code Online (Sandbox Code Playgroud)

这看起来很有趣,但我以前从未见过这种方法,既没有在我工作的公司,也没有在我看过源代码的开源项目中.所以,我想知道这种技术真的在实践中使用了吗?

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

BЈо*_*вић 124

所以,我想知道这种技术真的在实践中使用了吗?我应该在任何地方使用它,还是谨慎使用?

当然,在你提到的几个原因中,几乎在每个班级中都使用了它,在我的项目中使用了它:

  • 数据隐藏
  • 重新编译时间确实减少了,因为只需要重建源文件,而不是标题,以及包含它的每个文件
  • 二进制兼容性 由于类声明没有改变,因此只更新库是安全的(假设您正在创建库)

这种技术是否被推荐用于嵌入式系统(性能非常重要)?

这取决于你的目标有多强大.然而,这个问题的唯一答案是:衡量和评估你获得和失去的东西.

  • +1因为它在我工作的公司中被广泛使用,并且出于同样的原因. (15认同)
  • 二进制兼容性 (9认同)
  • 在Qt库中,此方法也用于智能指针情境.所以QString在内部将其内容保持为不可变类.当公共类被"复制"时,私有成员的指针被复制而不是整个私有类.这些私有类然后也使用智能指针,因此您基本上可以使用大多数类进行垃圾收集,此外还可以通过指针复制而不是全类复制来大大提高性能 (9认同)
  • 更重要的是,使用pimpl成语Qt可以在单个主要版本中保持前向和后向二进制兼容性(在大多数情况下).IMO这是迄今为止使用它的最重要原因. (8认同)

Pla*_*aHH 46

似乎有很多库使用它来保持其API的稳定性,至少对于某些版本而言.

但至于所有事情,你绝不应该谨慎使用任何地方.在使用它之前一定要考虑.评估它给你带来的好处,以及它们是否物有所值.

可能给你带来的好处是:

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

那些对你来说可能是也可能不是真正的优势.对我来说,我不关心几分钟的重新编译时间.最终用户通常也不会,因为他们总是从头开始编译它.

可能的缺点是(在这里,取决于实现以及它们对您来说是否真正的缺点):

  • 由于分配比使用naïve变体更多,因此内存使用量增加
  • 增加维护工作量(您必须至少编写转发功能)
  • 性能损失(编译器可能无法内联内容,因为它与您的类的天真实现一样)

所以要小心地给所有东西一个价值,并为自己评估.对我来说,几乎总是证明使用pimpl成语并不值得付出努力.只有一个案例我个人使用它(或至少类似的东西):

我的C++包装器用于linux stat调用.这里C头的结构可能不同,具体取决于#defines设置的内容.由于我的包装器头不能控制所有这些,我只#include <sys/stat.h>在我的.cxx文件中避免这些问题.

  • @JamesKanze:即使在那里,我个人也会先坐一会儿,想想是否有一些`#ifdef'可以让包装纸尽可能薄.但是每个人都有不同的目标,重要的是要花时间去思考它而不是盲目地追随某些东西. (5认同)
  • 它应该几乎总是用于系统接口,以使接口代码系统独立.我的`File`类(它暴露了很多信息`stat`将在Unix下返回)在Windows和Unix下使用相同的接口. (2认同)

Emi*_*lia 29

同意所有其他有关货物的信息,但让我提出一个限制:与模板不兼容.

原因是模板实例化需要在实例化发生的地方提供完整的声明.(这是您没有看到CPP文件中定义的模板方法的主要原因)

您仍然可以参考模板化子类,但由于您必须将它们全部包含在内,因此编译时"实现解耦"的所有优点(避免在所有地方包含所有平台特定代码,缩短编译)都会丢失.

对于经典OOP(基于继承)而言,它是一个很好的范例,但对于通用编程(基于特化)则不是.

  • 你必须更精确:当使用**PIMPL类作为模板类型参数时,绝对没有问题.只有当实现类本身需要在外部类的模板参数上进行参数化时,它才能再从接口头中隐藏,即使它仍然是私有类.如果你可以删除模板参数,你当然可以做"适当的"PIMPL.使用类型删除,您还可以在基本非模板类中执行PIMPL,然后从中派生模板类. (4认同)

小智 21

其他人已经提供了技术上/下行,但我认为以下值得注意:

首先,不要教条.如果pImpl适用于您的情况,请使用它 - 不要仅仅因为"它更好的OO,因为它真的隐藏了实现"等等.引用C++ FAQ:

封装是代码,而不是人()

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


Ghi*_*ita 12

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

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

  • @xaxxon 是的,是的。如果方法是低级别的,pimpl 会更慢。例如,永远不要将它用于紧密循环的东西。 (2认同)

use*_*178 5

我认为这是解耦的最基本工具之一.

我在嵌入式项目(SetTopBox)上使用了pimpl(和Exceptional C++的许多其他成语).

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

  • 这是使用 PIMPL 的主要原因。它解决了“N*M”问题。我不知道为什么其他答案没有将此列为优势。想必作者们并不知道这一点。但这正是 PIMPL 的主要目的。其他方面仅与编写需要二进制兼容性的库相关。我个人并不认为“它加快了编译时间”是一个合理的观点,因为它也“减慢了开发时间,并使任何试图在未来维护代码的人感到头疼”。 (2认同)

Cas*_*Cow 5

过去我经常使用这种技术,但后来发现自己逐渐远离它。

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

pImpl 的优点是:

  1. 假设这个接口只有一个实现,不使用抽象类/具体实现会更清楚

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

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

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

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

  2. (最大的问题)。在 unique_ptr 和 move 出现之前,您对如何存储 pImpl 的选择受到限制。一个原始指针,你有关于你的类不可复制的问题。旧的 auto_ptr 不适用于前向声明的类(无论如何不适用于所有编译器)。所以人们开始使用 shared_ptr ,这很好地使您的类可复制,但当然,两个副本都具有您可能不期望的相同底层 shared_ptr (修改一个,两个都被修改)。因此,解决方案通常是对内部指针使用原始指针,并使类不可复制并返回一个 shared_ptr 来代替。所以两次调用new。(实际上 3 给了旧的 shared_ptr 给了你第二个)。

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

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