为什么要使用" PIMPL"这个成语?

时间:2020-03-05 18:52:43  来源:igfitidea点击:

背景资料:

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先生所说的那样,一堂课,一项责任。