为什么要使用" 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"); }
解决方案
回答
将对impl-> Purr的调用放在cpp文件中意味着,将来我们可以做完全不同的事情而不必更改头文件。也许明年他们会发现本来可以调用的辅助方法,因此可以更改代码以直接调用该方法,而根本不使用impl-> Purr。 (是的,他们也可以通过更新实际的impl :: Purr方法来实现相同的目的,但是在这种情况下,我们将陷入一个额外的函数调用中,该函数只能依次调用下一个函数,什么也无法实现
这也意味着标头仅具有定义,并且没有任何实现更清晰分隔的实现,这是习惯用法的重点。
回答
我认为大多数人将此称为"句柄主体"成语。请参阅James Coplien的书《高级C ++编程样式和惯用语》(Amazon链接)。它也被称为柴郡猫(Cheshire Cat),因为刘易斯·卡洛尔(Lewis Caroll)的性格逐渐消失,直到只有咧嘴笑为止。
示例代码应分布在两组源文件中。然后,只有Cat.h是产品随附的文件。
CatImpl.h包含在Cat.cpp中,而CatImpl.cpp包含CatImpl :: Purr()的实现。使用产品的公众看不到它。
基本上,这个想法是尽可能地隐藏实施过程。
如果我们有一个商业产品,该产品是作为一系列库提供的,则可以通过编译客户代码并链接到该客户代码的API来访问该库。
为此,我们在2000年重写了IONAs Orbix 3.3产品。
正如其他人提到的那样,使用他的技术可以将实现与对象的接口完全分离。这样,如果我们只想更改Purr()的实现,则不必重新编译使用Cat的所有内容。
这项技术用于称为"按合同设计"的方法中。
回答
通常,正如我们在此处所做的那样,在Owner类(在本例中为Cat)的标头中,对Pimpl类的唯一引用将是前向声明,因为这样可以大大减少依赖性。
例如,如果Pimpl类具有ComplicatedClass作为成员(而不仅仅是指针或者对其的引用),那么我们将需要在使用ComplicatedClass之前对其进行完全定义。实际上,这意味着包括" ComplicatedClass.h"(也将间接包括ComplicatedClass所依赖的任何内容)。这可能导致单个标头填充会引入很多东西,这对于管理依赖项(以及编译时间)是不利的。
使用pimpl idion时,只需要#include所有者类型(此处为Cat)的公共接口中使用的内容。这对使用图书馆的人来说使事情变得更好,并且意味着我们无需担心人们会误认为图书馆的某些内部部分,或者因为他们想做我们不允许做的事情,所以他们#define公开,然后再包含文件。
如果这是一个简单的类,通常没有理由使用Pimpl,但是对于类型很大的时候,这可能是一个很大的帮助(尤其是避免长时间的构建)
回答
- 因为我们希望
Purr()
能够使用CatImpl
的私有成员。如果没有friend
声明,则不允许Cat :: Purr()这样的访问。 - 因为这样我们就不再混合职责:一类实施,一类转发。
回答
如果班级使用pimpl习惯用法,则可以避免在公共班级上更改头文件。
这使我们可以在pimpl类中添加/删除方法,而无需修改外部类的头文件。我们也可以在pimpl中添加/删除#includes。
更改外部类的头文件时,必须重新编译包含它的所有内容(如果其中任何一个是头文件,则必须重新编译包含它们的所有内容,依此类推)
回答
我不知道这是否值得一提,但...
是否有可能在其自己的名称空间中实现,并为用户看到的代码提供一个公共包装器/库名称空间:
catlib::Cat::Purr(){ cat_->Purr(); } cat::Cat::Purr(){ printf("purrrrrr"); }
这样,所有库代码都可以利用cat名称空间,并且由于需要向用户公开类,因此可以在catlib名称空间中创建包装器。
回答
我发现这说明了尽管pimpl习惯用法是众所周知的,但我并不认为它在现实生活中经常出现(例如在开源项目中)。
我常常想知道这些"好处"是否夸大了;是的,我们可以隐藏一些实现细节,是的,可以在不更改标头的情况下更改实现,但是这些在现实中并不是很大的优势。
就是说,尚不清楚实现是否需要隐藏得那么好,而且人们很少真正只更改实现,这是非常罕见的。例如,一旦需要添加新方法,就需要更改标题。
回答
在过去的几天里,我刚刚实施了我的第一个pimpl课程。我用它来解决我在Borland Builder中包含winsock2.h时遇到的问题。它似乎搞砸了结构对齐,并且由于我在类私有数据中有套接字内容,因此这些问题已蔓延到任何包含标头的cpp文件中。
通过使用pimpl,winsock2.h仅包含在一个cpp文件中,在这里我可以控制问题,而不用担心它会再次咬我。
为了回答原始问题,我发现将呼叫转发给pimpl类的好处是pimpl类与我们在插入pimpl类之前的原始类相同,而且实现没有分散到2个对象上以一些怪异的方式上课。让公众直接进入pimpl类更加清晰。
就像Nodet先生所说的那样,一堂课,一项责任。