为什么要使用“PIMPL”习语呢?

背景资料:

PIMPL成语 (实现指针)是一种实现隐藏的技术,在这种技术中,公共类包装一个结构或类,这些结构或类在公共类所属的库之外是看不到的。

这隐藏了库的用户的内部实现细节和数据。

当实现这个习惯用法时,为什么要把公共方法放在pimpl类而不是公共类中,因为公共类方法的实现将被编译到库中,而用户只有头文件?

为了说明,这段代码把Purr()实现放在impl类上,并且包装它。

为什么不直接在公共课上实施Purr?

 // header file: class Cat { private: class CatImpl; // Not defined here CatImpl *cat_; // Handle public: Cat(); // Constructor ~Cat(); // Destructor // Other operations... Purr(); }; // CPP file: #include "cat.h" class Cat::CatImpl { Purr(); ... // The actual implementation can be anything }; Cat::Cat() { cat_ = new CatImpl; } Cat::~Cat() { delete cat_; } Cat::Purr(){ cat_->Purr(); } CatImpl::Purr(){ printf("purrrrrr"); } 
  • 因为你想让Purr()能够使用CatImpl私有成员。 Cat::Purr()不允许没有friend声明的访问。
  • 因为你不要混合责任:一个class级实施,一个class级转发。

我想大多数人都把这称为“句柄”的成语。 请参阅James Coplien的书“高级C ++编程风格和习惯( 亚马逊链接 )”。 它也被称为柴郡猫,因为刘易斯·卡罗尔的性格消失,直到只有咧嘴一笑。

示例代码应该分布在两组源文件中。 那么只有Cat.h是产品随附的文件。

CatImpl.h包含在Cat.cpp中,CatImpl.cpp包含CatImpl :: Purr()的实现。 使用您的产品的公众将无法看到。

基本上这个想法就是尽可能地隐藏实现目标的实现。 这是最有用的地方,你有一个商业产品作为一系列的库,通过客户的代码编译和链接到API访问的库。

我们在2000年重写了IONAs Orbix 3.3产品。

正如其他人所提到的,使用他的技术将实现从对象的接口完全分离出来。 那么如果你只是想改变Purr()的实现,你将不必重新编译使用Cat的所有东西。

这种技术被用在一种被称为devise合同的方法中。

对于什么是值得的,它将实现与接口分开。 在小型项目中这通常不是很重要。 但是,在大型项目和库中,它可以用来显着缩短构build时间。

考虑到Cat的实现可能包含许多头文件,可能会涉及模板元编程,这需要花费时间来自行编译。 为什么只是想要使用Cat必须包含所有这些? 因此,使用pimpl习语(因此CatImpl的前向声明)隐藏了所有必需的文件,并且使用该界面不强制用户包括它们。

我正在开发一个用于非线性优化的库(阅读“很多讨厌的math”),它是在模板中实现的,所以大部分代码都在头文件中。 大约需要五分钟的时间才能编译(在一个体面的多核CPU上),而只是在空的.cppparsing头文件需要大约一分钟的时间。 所以使用这个库的人每次编译他们的代码都要等几分钟,这使得开发起来很繁琐 。 但是,通过隐藏实现和头文件,只需包含一个简单的接口文件即可立即编译。

除非你的algorithm的内部工作可以从成员variables的定义中猜出来,否则它不一定与保护实现免受其他公司的复制有关(如果是这样,它是可能不是很复杂,首先不值得保护)。

如果你的类使用pimpl习语,你可以避免更改公共类的头文件。

这允许你添加/删除方法到pimpl类,而不需要修改外部类的头文件。 你也可以添加/删除#includes到pimpl。

当你改变外部类的头文件时,你必须重新编译包含它的所有东西(如果其中的任何一个是头文件,你必须重新编译包含它们的所有东西,等等)

通常情况下,对于Owner类(在这种情况下,Cat)头的唯一引用Pimpl类将是一个前向声明,因为这可以大大减less依赖关系。

例如,如果您的Pimpl类有ComplicatedClass作为成员(而不仅仅是一个指针或引用它),那么您需要在使用之前将ComplicatedClass完全定义。 实际上,这意味着包括“ComplicatedClass.h”(这也将间接地包含任何ComplicatedClass所依赖的)。 这可能会导致单个头部填充大量的东西,这是不利于pipe理你的依赖关系(和你的编译时间)。

当你使用pimpl idion的时候,你只需要#include在你所有者types的公共接口中使用的东西(这里就是Cat)。 这使得使用你的图书馆的人变得更好,并且意味着你不需要担心取决于你的图书馆的某些内部部分的人们 – 或者是错误的,或者是因为他们想做一些你不允许的事情,所以他们#define私人公众在包括您的文件之前。

如果这是一个简单的课程,通常没有理由使用Pimpl,但是在types相当大的时候,这可能是一个很大的帮助(尤其是避免长时间的构build)

那么,我不会使用它。 我有一个更好的select:

foo.h中:

 class Foo { public: virtual ~Foo() { } virtual void someMethod() = 0; // This "replaces" the constructor static Foo *create(); } 

Foo.cpp中:

 namespace { class FooImpl: virtual public Foo { public: void someMethod() { //.... } }; } Foo *Foo::create() { return new FooImpl; } 

这个模式有一个名字吗?

作为Python和Java程序员,我比pImpl更喜欢这个成语。

将调用放到cpp文件中的impl-> Purr意味着将来可以做一些完全不同的事情,而不必更改头文件。 也许明年他们会发现一个他们可以调用的帮助方法,所以他们可以直接调用这个代码,而不是使用impl-> Purr。 (是的,他们也可以通过更新实际的impl :: Purr方法来达到同样的效果,但是在这种情况下,你会遇到一个额外的函数调用,它只能调用下一个函数)

这也意味着头只有定义,并没有任何实现,使更清晰的分离,这是成语的整个点。

在过去的几天里,我刚刚实施了我的第一个pimpl课程。 我用它来消除我在Borland Builder中包含winsock2.h的问题。 这似乎是搞砸了结构alignment,因为我在类的私人数据套接字的东西,这些问题蔓延到包括头的任何CPP文件。

通过使用pimpl,winsock2.h只被包含在一个cpp文件中,在那里我可以解决这个问题,而不用担心它会回来咬我。

为了回答最初的问题,我发现在将调用转发给pimpl类时的优点是,pimpl类与你原来的类在你执行它之前所做的一样,加上你的实现不会遍布在2以一些怪异的方式上课。 让公众简单地向公众开放,更加清晰。

像诺特先生说的,一个class级,一个责任。

我们使用PIMPL惯用法来模仿面向方面的编程,在执行成员函数之前和之后调用pre,post和error方面。

 struct Omg{ void purr(){ cout<< "purr\n"; } }; struct Lol{ Omg* omg; /*...*/ void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } } }; 

我们还使用指向基类的指针来分享许多类之间的不同方面。

这种方法的缺点是,图书馆用户必须考虑将要执行的所有方面,但只能看到他的class级。 它需要浏览文档的任何副作用。

我不知道这是否值得一提,但…

是否有可能在自己的命名空间中实现,并为用户所看到的代码提供公共的包装器/库名称空间:

 catlib::Cat::Purr(){ cat_->Purr(); } cat::Cat::Purr(){ printf("purrrrrr"); } 

通过这种方式,所有的库代码都可以使用cat命名空间,并且随着向用户公开类的需要,可以在catlib命名空间中创build一个包装器。

我发现,尽pipepimpl习语众所周知,但我并不认为它在现实生活中经常出现(例如在开源项目中)。

我经常想知道“好处”是否被夸大了; 是的,你可以使一些你的实现细节更加隐藏,是的,你可以改变你的实现,而不改变头,但这并不明显,这些是现实中的巨大优势。

也就是说,目前还不清楚是否需要将您的实现隐藏起来,也许很less有人真正改变实现; 只要你需要添加新的方法,比如说,你需要改变标题。