为什么要使用“PIMPL”成语?[复制]

Why should the "PIMPL" idiom be used? [duplicate]

提问人:JeffV 提问时间:9/13/2008 最后编辑:Peter G.JeffV 更新时间:4/10/2021 访问量:122783

问:

背景资料:

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

这会对库的用户隐藏内部实现详细信息和数据。

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

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

为什么不直接在公共类上实现 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");
}
C++ oop 信息隐藏 pimpl-idiom

评论

35赞 mlvljr 10/8/2010
因为应该避免使用皮条客成语?..
4赞 zhanxw 2/18/2013
很好的答案,我发现这个链接也包含全面的信息: marcmutz.wordpress.com/translated-articles/pimp-my-pimpl
17赞 DevSolar 3/11/2013
如果你想帮维护编码员一个忙,请记住这是一个接口模式。不要将它用于每个内部类。引用《银翼杀手》的话,我见过你们这些人不会相信的s***。
2赞 osirisgothra 2/8/2014
请注意,PIMPL 可以带来很多好处,尤其是在大型项目中,但可能会使原本简单的小型程序严重复杂化。在这个问题的某个地方,有一个在项目中使用 PIMPL 的最低“先决条件”列表。不是每个人都应该遵循相同的清单,为自己制作一个,并坚持下去。在我看来,这可能是最好的方法。
8赞 Trantor 1/24/2017
我自己的经验是,那些生产大型无文档框架然后离开公司的人更喜欢 pimpl,所以他们以前的同事不得不处理更难分析的分类哔哔哩......。

答:

3赞 Phil Wright 9/13/2008 #1

将对 impl->Purr 的调用放在 .cpp 文件中意味着将来可以执行完全不同的操作,而无需更改头文件。

也许明年他们发现了一个他们可以调用的辅助方法,因此他们可以更改代码以直接调用它,而根本不使用 impl->Purr。(是的,他们也可以通过更新实际的impl::P urr方法来实现相同的目的,但在这种情况下,你被困在一个额外的函数调用中,除了依次调用下一个函数之外什么也做不了。

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

75赞 Rob Wells 9/13/2008 #2

我想大多数人都把这称为手柄身体成语。参见James Coplien的著作Advanced C++ Programming Styles and Idioms。它也被称为柴郡猫,因为刘易斯·卡罗尔的性格逐渐消失,直到只剩下笑容。

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

CatImpl.h 包含在 Cat.cpp 中,CatImpl.cpp包含 CatImpl::P urr() 的实现。使用您的产品的公众不会看到这一点。

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

当您有一个商业产品,该产品作为一系列库提供时最有用,这些库通过 API 访问,客户的代码是编译和链接到的。

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

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

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

评论

4赞 andreas buykx 9/23/2008
疙瘩成语(或你所说的手柄-身体成语)是如何通过合同在设计中使用?
0赞 Rob Wells 12/6/2014
嗨,Andreas,您与 API 用户的接口点仅在公开的合约(句柄)中,而不是在实现正文以提供广告功能的方式中。您可以根据需要自由更改实现,前提是您不更改已公布的 API 的语义。
1赞 JeffV 9/17/2008
@Rob,我猜这开销很小。一个额外的班级,但他们很少。只是现有类的薄包装器。如果我错了,有人会纠正我,但内存使用量只是 RAM 中的额外函数表、指向 pimpl 的指针和代码空间中每个方法的重定向函数。不过,维护和调试很痛苦。
0赞 cubuspl42 4/11/2019
@RobWells我否决了这个答案,因为它也是错误的(但不像公认的答案那么错误;这个答案可能会被修复)。问题: a) “基本上,我们的想法是尽可能多地隐藏实现,不被窥探。”它与简单的 .h/.cpp 类声明/定义分离和交付库 .h/(.a|.库)?显然,它还对客户端隐藏了实现。OP在问题中明确提到了这一点!
1赞 cubuspl42 4/11/2019
FFR:基本上是指 stackoverflow.com/questions/8972588/ 中公认的答案......
7赞 Wilka 9/13/2008 #3

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

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

当你使用 PIMPL 习惯语时,你只需要 #include 你的所有者类型(这里是 Cat)的公共接口中使用的内容。这对使用你的库的人来说会更好,也意味着你不需要担心人们依赖于你库的某些内部部分 - 无论是错误的,还是因为他们想做一些你不允许的事情,所以他们在包含你的文件之前 #define 私有的公开。

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

42赞 Xavier Nodet 9/13/2008 #4
  • 因为您希望能够使用 的私有成员。 如果没有声明,将不允许进行此类访问。Purr()CatImplCat::Purr()friend
  • 因为这样你就不会混合责任:一个类实现,一个类转发。

评论

7赞 JeffV 9/13/2008
不过,维持起来很痛苦。但话又说回来,如果它是一个库类,那么无论如何都不应该对方法进行太大的更改。我正在查看的代码似乎走在安全的道路上,到处都在使用疙瘩。
1赞 binaryguy 8/14/2015
这一行不是非法的,因为所有成员都是私有的:cat_->Purr();Purr() 无法从外部访问,因为 deafult 它是私有的。我在这里错过了什么?
2赞 mip 1/10/2017
这两点都没有任何意义。如果你有一个类 - 它也将能够访问它的成员,并且它不会“混合可复制性”,因为它将是“实现”的类。使用 PIMPL 的原因各不相同。Cat
2赞 cubuspl42 4/11/2019
由于@doc提到的原因,这个答案是完全错误的。仅仅为了使用其私有成员而引入一个新类是荒谬的,而且仅仅转发一些东西不是“责任”!
15赞 Nick 9/13/2008 #5

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

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

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

0赞 JeffV 9/13/2008 #6

我不知道这是否是一个值得一提的差异,但是......

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

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

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

0赞 DrPizza 9/15/2008 #7

我发现这很能说明问题,尽管 PIMPL 成语很有名,但我并不认为它在现实生活中经常出现(例如,在开源项目中)。

我常常在想,“好处”是不是被夸大了;是的,你可以使你的一些实现细节更加隐蔽,是的,你可以在不改变标题的情况下改变你的实现,但这些在现实中并不明显。

也就是说,目前尚不清楚您的实现是否需要隐藏得那么好,也许很少有人真正只更改实现;比如说,一旦你需要添加新的方法,你就需要改变标题。

评论

2赞 j_random_hacker 9/22/2009
是的,只有当你能选择一个好的公共界面并坚持下去时,它才会有所作为。正如 Rob Wells 所提到的,如果您需要将更新的库分发给链接到您的库的编译版本而不强迫他们重新编译的人,这一点很重要——您只需提供一个新的 DLL 即可。请注意,使用接口(== C++ 中没有数据成员的抽象类)可以以更少的维护实现大致相同的目的(无需手动转发每个公共方法)。但是 OTOH 您必须使用特殊语法来创建实例(即调用工厂方法)。
7赞 el.pescado - нет войне 1/10/2010
Qt广泛使用PIMPL习语。
1赞 markh44 3/25/2009 #8

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

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

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

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

4赞 Esben Nielsen 6/21/2011 #9

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

文件 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 成语。

评论

4赞 Dennis Zickefoose 6/21/2011
如果您已经将自己限制在创建对象的工厂方法中,那么这很好。但它完全消除了值语义,而传统的 pImpl 适用于任何一种方法。
2赞 Esben Nielsen 6/22/2011
好吧,pImpl 基本上只是包装指针。您需要做的就是使上面的 create() 返回一个 PointerWrapperWithCopySemantics<Foo> :-)。我通常反其道而行之,并返回一个 std::auto_ptr<Foo>。
14赞 derpface 10/11/2013
为什么继承而不是组成?为什么要增加 vtable 的开销?为什么选择虚拟继承?
2赞 smac89 2/19/2018
这不适用于模板方法
24赞 the swine 9/23/2014 #10

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

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

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

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

3赞 nurettin 11/12/2014 #11

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

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

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

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

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

评论

0赞 John Z. Li 2/20/2019
撇开编译速度的论点不谈,这是 PIMPL 提供的最有益的东西,IMO。