首页 / 知识

关于c ++:为什么应使用“ PIMPL”这一成语?

2023-04-15 05:18:00

关于c ++:为什么应使用“ PIMPL”这一成语?

Why should the “PIMPL” idiom be used?

本问题已经有最佳答案,请猛点这里访问。

背景资料:

PIMPL成语(实现的指针)是一种用于隐藏实现的技术,其中,公共类包装了公共类所属的库外部看不到的结构或类。

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

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

为了说明这一点,此代码将Purr()实现放在impl类上,并将其也包装起来。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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");
}

我认为大多数人将此称为"句柄主体"成语。请参阅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的所有内容。

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


  • 因为您希望Purr()能够使用CatImpl的私有成员。如果没有friend声明,将不允许Cat::Purr()这样的访问。
  • 因为这样您就不再混淆职责:一类实施,一类转发。


为了有价值,它将实现与接口分离。在小型项目中,这通常不是很重要。但是,在大型项目和库中,可以使用它来显着减少构建时间。

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

我正在开发一个用于非线性优化的库(请阅读"很多讨厌的数学"),该库是在模板中实现的,因此大多数代码都在标头中。编译(在不错的多核CPU上)大约需要五分钟,而仅在空白的.cpp中解析标头大约需要一分钟。因此,使用该库的任何人每次编译代码时都必须等待几分钟,这使开发工作变得很繁琐。但是,通过隐藏实现和标头,可以只包含一个简单的接口文件,该文件可以立即进行编译。

它与保护实现不受其他公司复制并不一定有任何关系-除非您可以从成员变量的定义中猜出算法的内部工作原理,否则无论如何都不会发生(如果是,则为可能不是很复杂,一开始就不值得保护)。


如果您的班级使用pimpl习惯用法,则可以避免在公共班级上更改头文件。

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

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


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

例如,如果您的Pimpl类具有ComplicatedClass作为成员(而不仅仅是指针或对其的引用),则在使用ComplicatedClass之前,需要对其进行完全定义。实际上,这意味着包括" ComplicatedClass.h"(它还将间接包括ComplicatedClass所依赖的任何内容)。这可能导致单个标头填充会引入很多东西,这对于管理依赖项(以及编译时间)是不利的。

使用pimpl idion时,只需#include所有者类型(此处为Cat)的公共接口中使用的内容。这对使用您的图书馆的人来说使事情变得更好,并且意味着您不必担心图书馆的内部某些部分的人-是由于错误,还是因为他们想做您不允许做的事情,所以他们#define包括您的文件之前为私人公开。

如果这是一个简单的类,通常没有理由使用Pimpl,但是对于类型很大的时候,这可能是一个很大的帮助(尤其是避免长时间的构建)


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

foo.h:

1
2
3
4
5
6
7
8
class Foo {
public:
    virtual ~Foo() { }
    virtual void someMethod() = 0;

    // This"replaces" the constructor
    static Foo *create();
}

foo.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace {
    class FooImpl: virtual public Foo {

    public:
        void someMethod() {
            //....
        }    
    };
}

Foo *Foo::create() {
    return new FooImpl;
}

这个图案有名字吗?

作为Python和Java程序员,我比pImpl习惯更喜欢它。


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

1
2
3
4
5
6
7
8
9
10
struct Omg{
   void purr(){ cout<<"purr
"
; }
};

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

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

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


将对impl-> Purr的调用放在cpp文件中意味着,将来您可以做完全不同的事情而不必更改头文件。也许明年他们会发现可以调用的辅助方法,因此可以更改代码以直接调用该方法,而根本不使用impl-> Purr。 (是的,他们也可以通过更新实际的impl :: Purr方法来实现相同的目的,但是在这种情况下,您会陷入额外的函数调用,而该函数调用只能依次调用下一个函数什么也没有实现)

这也意味着标头仅具有定义,并且没有任何实现更清晰分隔的实现,这是习惯用法的全部重点。


在过去的几天里,我刚刚实施了我的第一届pimpl课。我用它来解决我在Borland Builder中包含winsock2.h时遇到的问题。这似乎搞砸了结构对齐,并且由于我在类私有数据中有套接字内容,因此这些问题已蔓延到任何包含标头的cpp文件中。

通过使用pimpl,winsock2.h仅包含在一个cpp文件中,在这里我可以控制问题,而不用担心它会再次咬住我。

为了回答原始问题,我发现将呼叫转发到pimpl类的好处是pimpl类与您在插入pimpl类之前的原始类相同,而且实现没有分散到2个对象上以一些奇怪的方式上课。让公众直接进入pimpl类更加清晰。

就像Nodet先生所说的那样,一堂课,一项责任。


我发现这说明了尽管pimpl惯用语非常有名,但我并不认为它在现实生活中经常出现(例如在开源项目中)。

我经常想知道这些"好处"是否夸大了;是的,您可以使一些实现细节更加隐蔽,是的,可以在不更改标题的情况下更改实现,但是实际上这些并不是很大的优势。

就是说,尚不清楚是否需要对实现进行很好的隐藏,而且人们很少真正只更改实现,这很罕见。例如,一旦需要添加新方法,就需要更改标题。


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

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

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

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


指针用于数据结构

最新内容

相关内容

猜你喜欢