为什么要使用“PIMPL”成语?

时间:2008-09-13 14:30:30

标签: c++ oop information-hiding pimpl-idiom

背景:

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

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

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

11 个答案:

答案 0 :(得分:61)

我认为大多数人都将此称为Handle Body成语。请参阅James Coplien的书“高级C ++编程风格和习语”(Amazon link)。它也被称为Cheshire Cat,因为刘易斯卡罗尔的角色逐渐消失,直到只留下咧嘴笑。

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

Cat.cmpl包含CatImpl.h,CatImpl.cpp包含CatImpl :: Purr()的实现。使用您的产品的公众无法看到这一点。

基本上,我们的想法是尽可能地隐藏实施方案。 如果您的商业产品是作为一系列库提供的,那么这是最有用的,这些库可以通过API来访问,客户的代码是针对这些库进行编译和链接的。

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

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

此技术用于名为design by contract的方法中。

答案 1 :(得分:40)

  • 因为您希望Purr()能够使用CatImpl的私人成员。如果没有Cat::Purr()声明,则不允许friend进行此类访问。
  • 因为你不混合责任:一个类实现,一个类转发。

答案 2 :(得分:19)

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

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

我正在开发一个非线性优化库(读“很多讨厌的数学”),它是在模板中实现的,所以大部分代码都在标题中。编译大约需要五分钟(在一个不错的多核CPU上),只需在空的.cpp中解析标题大约需要一分钟。因此,每次使用该库的人都必须在每次编译代码时等待几分钟,这使得开发非常tedious。但是,通过隐藏实现和标题,只需包含一个简单的接口文件,即可立即编译。

它不一定与保护实现不被其他公司复制有任何关系 - 除非你的算法的内部工作可以从成员变量的定义中猜到(如果是这样的话) ,它可能不是很复杂,一开始就不值得保护。)

答案 3 :(得分:14)

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

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

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

答案 4 :(得分:6)

通常,在Owner类的头文件中引用Pimpl类的唯一引用(在本例中为Cat)将是一个前向声明,正如您在此处所做的那样,因为这可以大大减少依赖性。

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

当你使用pimpl idion时,你只需要#include你所有者类型的公共接口中使用的东西(这里是Cat)。这使得使用你的图书馆的人的事情变得更好,这意味着你不需要担心依赖于图书馆内部部分的人 - 要么是错误的,要么是因为他们想要做一些你不允许的事情,所以他们#define包含您的文件之前的私人公众。

如果它是一个简单的类,通常没有理由使用Pimpl,但是对于类型非常大的时候,它可以是一个很大的帮助(特别是在避免长构建时间)

答案 5 :(得分: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习惯用法更喜欢这个。

答案 6 :(得分: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(); } }
};

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

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

答案 7 :(得分:2)

将调用放在cpp文件中的impl-&gt; Purr意味着将来你可以做一些完全不同的事情,而无需更改头文件。也许明年他们会发现他们可以调用的辅助方法,因此他们可以更改代码直接调用它,而不是使用impl-&gt; Purr。 (是的,他们可以通过更新实际的impl :: Purr方法来实现相同的功能,但在这种情况下,你会遇到一个额外的函数调用,只能依次调用下一个函数)

这也意味着标题只有定义,并且没有任何实现,这使得更清晰的分离,这是成语的全部要点。

答案 8 :(得分:1)

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

通过使用pimpl,winsock2.h只包含在一个cpp文件中,我可以对这个问题设置一个限制,不用担心它会回来咬我。

为了回答原始问题,我在将调用转发到pimpl类时发现的优势是pimpl类与你的原始类相同,然后你的实现不是以某种奇怪的方式分散了两个班级。实现公众简单地转发到pimpl类更加清晰。

像Nodet先生所说的那样,一个阶级,一个责任。

答案 9 :(得分:0)

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

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

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

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

答案 10 :(得分:0)

我发现它告诉我,尽管pimpl成语有多么出名,但我并不认为它在现实生活中经常出现(例如在开源项目中)。

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

也就是说,不清楚你的实现是否需要 隐藏得很好,而且很可能人们确实只改变了实现;例如,只要你需要添加新方法,就需要更改标题。