141

背景资料:

PIMPL Idiom (指向 IPLementation的指针)是一种实现隐藏技术,其中公共类包装了在公共类所属的库之外无法看到的结构或类。

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

在实现这个习惯用法时,为什么要将公共方法放在 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");
}
4

11 回答 11

72

我认为大多数人将其称为Handle Body成语。请参阅 James Coplien 的书Advanced C++ Programming Styles and Idioms。它也被称为柴郡猫,因为刘易斯卡罗尔的角色逐渐消失,直到只剩下笑容。

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

CatImpl.h包含在Cat.cpp中,CatImpl.cpp包含CatImpl::Purr()的实现。使用您的产品的公众不会看到此信息。

基本上,这个想法是尽可能多地隐藏实现以防窥探。

这在您拥有作为一系列库提供的商业产品时最有用,这些库通过 API 访问,客户的代码根据这些库进行编译和链接。

为此,我们在 2000 年重写了IONA 的 Orbix 3.3 产品。

正如其他人所提到的,使用他的技术将实现与对象的接口完全分离。然后,如果您只想更改Purr()的实现,则不必重新编译使用Cat的所有内容。

这种技术用于一种称为按合同设计的方法中。

于 2008-09-13T14:51:46.893 回答
42
  • 因为您希望Purr()能够使用CatImpl. Cat::Purr()如果没有friend声明,将不允许这样的访问。
  • 因为你不混合职责:一个类实现,一个类转发。
于 2008-09-13T15:16:18.597 回答
22

值得一提的是,它将实现与接口分开。这在小型项目中通常不是很重要。但是,在大型项目和库中,它可用于显着减少构建时间。

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

我正在开发一个用于非线性优化的库(阅读“大量讨厌的数学”),它在模板中实现,因此大部分代码都在标头中。编译大约需要 5 分钟(在一个体面的多核 CPU 上),仅在一个空的文件中解析头文件.cpp大约需要 1 分钟。因此,任何使用该库的人每次编译代码时都必须等待几分钟,这使得开发相当乏味。但是,通过隐藏实现和标头,只需包含一个简单的接口文件,即可立即编译。

它不一定与保护实现不被其他公司复制有关 - 这可能不会发生,除非可以从成员变量的定义中猜出算法的内部工作原理(如果是这样,它是可能不是很复杂,首先不值得保护)。

于 2014-09-23T11:18:03.327 回答
15

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

这允许您向 PIMPL 类添加/删除方法,而无需修改外部类的头文件。您也可以向 PIMPL 添加/删除#includes。

当您更改外部类的头文件时,您必须重新编译 #includes 它的所有内容(如果其中任何一个是头文件,您必须重新编译 #includes 它们的所有内容,依此类推)。

于 2008-09-13T15:28:30.783 回答
7

通常,所有者类(在本例中为Cat )的标头中对 PIMPL 类的唯一引用将是前向声明,正如您在此处所做的那样,因为这可以大大减少依赖关系。

例如,如果您的 PIMPL 类具有ComplicatedClass作为成员(而不仅仅是指针或对它的引用),那么您需要在使用之前完全定义ComplicatedClass 。实际上,这意味着包括文件“ComplicatedClass.h”(这也将间接包括任何ComplicatedClass所依赖的内容)。这可能会导致单个标头填充拉入大量内容,这不利于管理您的依赖项(以及您的编译时间)。

当您使用 PIMPL 成语时,您只需要 #include 在您的所有者类型的公共接口中使用的内容(此处为Cat)。这让使用你的图书馆的人变得更好,这意味着你不需要担心人们依赖于你图书馆的某些内部部分——无论是错误的,还是因为他们想做你不允许的事情,所以他们#在包含您的文件之前定义私有公共。

如果它是一个简单的类,通常没有任何理由使用 PIMPL,但对于类型非常大的时候,它可能会有很大帮助(尤其是在避免长时间构建方面)。

于 2008-09-13T14:53:59.837 回答
4

好吧,我不会用它。我有一个更好的选择:

文件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 成语更喜欢这个。

于 2011-06-21T13:30:54.247 回答
3

在 .cpp 文件中调用 impl->Purr 意味着将来您可以做一些完全不同的事情而无需更改头文件。

也许明年他们会发现一个他们可以调用的辅助方法,因此他们可以更改代码以直接调用它,而根本不使用 impl->Purr。(是的,他们也可以通过更新实际的 impl::Purr 方法来实现相同的目的,但是在这种情况下,您会遇到一个额外的函数调用,该调用只能依次调用下一个函数。)

这也意味着标题只有定义,没有任何实现更清晰的分离,这是成语的重点。

于 2008-09-13T14:45:22.180 回答
3

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

struct Omg{
   void purr(){ cout<< "purr\n"; }
};

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

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

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

于 2014-11-11T18:20:59.133 回答
1

在过去的几天里,我刚刚实现了我的第一个 PIMPL 课程。我用它来消除我遇到的问题,包括 Borland Builder 中的文件 *winsock2.*h。它似乎搞砸了结构对齐,因为我在类私有数据中有套接字的东西,这些问题正在蔓延到任何包含标题的 .cpp 文件中。

通过使用 PIMPL,winsock2.h仅包含在一个 .cpp 文件中,我可以在其中解决问题,而不必担心它会回来咬我。

为了回答最初的问题,我在将调用转发给 PIMPL 类时发现的优势是 PIMPL 类与您在 pimpl 之前的原始类相同,而且您的实现不会分布在两个以某种奇怪的方式上课。实现公共成员以简单地转发到 PIMPL 类要清楚得多。

就像Nodet 先生说的,一个班级,一个责任。

于 2009-03-24T16:34:13.050 回答
0

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

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

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

这样,所有库代码都可以使用 cat 命名空间,并且当需要向用户公开一个类时,可以在 catlib 命名空间中创建一个包装器。

于 2008-09-13T15:31:47.980 回答
0

我发现它说明了这一点,尽管 PIMPL 习语有多么广为人知,但我认为它在现实生活中并不经常出现(例如,在开源项目中)。

我经常想知道“好处”是否被夸大了;是的,你可以让你的一些实现细节更加隐藏,是的,你可以在不改变标题的情况下改变你的实现,但这些在现实中并不是很明显的优势。

也就是说,不清楚你的实现有没有必要隐藏得那么好,而且人们真的只改变实现的情况可能很少见;只要您需要添加新方法,例如,无论如何您都需要更改标题。

于 2008-09-15T09:54:01.083 回答