C++ 为什么要使用“PIMPL”习语?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/60570/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 12:33:52  来源:igfitidea点击:

Why should the "PIMPL" idiom be used?

c++oopinformation-hidingpimpl-idiom

提问by JeffV

Backgrounder:

背景资料:

The PIMPL Idiom(Pointer to IMPLementation) is a technique for implementation hiding in which a public class wraps a structure or class that cannot be seen outside the library the public class is part of.

所述PIMPL成语(指针实现)是用于执行隐藏在其中一个公共类包装的结构或类,可以不在库的公共类是的一部分外部看到的技术。

This hides internal implementation details and data from the user of the library.

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

When implementing this idiom why would you place the public methods on the pimpl class and not the public class since the public classes method implementations would be compiled into the library and the user only has the header file?

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

To illustrate, this code puts the Purr()implementation on the impl class and wraps it as well.

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

Why not implement Purr directly on the public class?

为什么不直接在公共类上实现 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");
}

采纳答案by Xavier Nodet

  • Because you want Purr()to be able to use private members of CatImpl. Cat::Purr()would not be allowed such an access without a frienddeclaration.
  • Because you then don't mix responsibilities: one class implements, one class forwards.
  • 因为您希望Purr()能够使用CatImpl. Cat::Purr()如果没有friend声明,将不允许这样的访问。
  • 因为你不会混合职责:一类实现,一类转发。

回答by Rob Wells

I think most people refer to this as the Handle Body idiom. See James Coplien's book Advanced C++ Programming Styles and Idioms (Amazon link). It's also known as the Cheshire Catbecause of Lewis Caroll's character that fades away until only the grin remains.

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

The example code should be distributed across two sets of source files. Then only Cat.h is the file that is shipped with the product.

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

CatImpl.h is included by Cat.cpp and CatImpl.cpp contains the implementation for CatImpl::Purr(). This won't be visible to the public using your product.

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

Basically the idea is to hide as much as possible of the implementation from prying eyes. This is most useful where you have a commercial product that is shipped as a series of libraries that are accessed via an API that the customer's code is compiled against and linked to.

基本上这个想法是尽可能多地隐藏实施的窥探。如果您的商业产品是作为一系列库提供的,这些库可通过 API 访问,客户的代码将根据该 API 编译并链接到该 API,那么这将非常有用。

We did this with the rewrite of IONAs Orbix 3.3 product in 2000.

我们在 2000 年通过重写 IONAs Orbix 3.3 产品来做到这一点。

As mentioned by others, using his technique completely decouples the implementation from the interface of the object. Then you won't have to recompile everything that uses Cat if you just want to change the implementation of Purr().

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

This technique is used in a methodology called design by contract.

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

回答by the swine

For what is worth, it separates the implementation from the interface. This is usually not very important in small size projects. But, in large projects and libraries, it can be used to reduce the build times significantly.

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

Consider that the implementation of Catmay include many headers, may involve template meta-programming which takes time to compile on its own. Why should a user, who just wants to use the Cathave to include all that? Hence, all the necessary files are hidden using the pimpl idiom (hence the forward declaration of CatImpl), and using the interface does not force the user to include them.

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

I'm developing a library for nonlinear optimization (read "lots of nasty math"), which is implemented in templates, so most of the code is in headers. It takes about five minutes to compile (on a decent multi-core CPU), and just parsing the headers in an otherwise empty .cpptakes about a minute. So anyone using the library has to wait a couple of minutes every time they compile their code, which makes the development quite tedious. However, by hiding the implementation and the headers, one just includes a simple interface file, which compiles instantly.

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

It does not necessarily have anything to do with protecting the implementation from being copied by other companies - which wouldn't probably happen anyway, unless the inner workings of your algorithm can be guessed from the definitions of the member variables (if so, it is probably not very complicated and not worth protecting in the first place).

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

回答by Nick

If your class uses the pimpl idiom, you can avoid changing the header file on the public class.

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

This allows you to add/remove methods to the pimpl class, without modifying the external class's header file. You can also add/remove #includes to the pimpl too.

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

When you change the external class's header file, you have to recompile everything that #includes it (and if any of those are header files, you have to recompile everything that #includes them, and so on)

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

回答by Wilka

Typically, the only reference to Pimpl class in the header for the Owner class (Cat in this case) would be a forward declaration, as you have done here, because that can greatly reduce the dependencies.

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

For example, if your Pimpl class has ComplicatedClass as a member (and not just a pointer or reference to it) then you would need to have ComplicatedClass fully defined before it's use. In practice, this means including "ComplicatedClass.h" (which will also indirectly include anything ComplicatedClass depends on). This can lead to a single header fill pulling in lots and lots of stuff, which is bad for managing your dependencies (and your compile times).

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

When you use the pimpl idion, you only need to #include the stuff used in the public interface of your Owner type (which would be Cat here). Which makes things better for people using your library, and means you don't need to worry about people depending on some internal part of your library - either by mistake, or because they want to do something you don't allow so they #define private public before including your files.

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

If it's a simple class, there's usually no reason to use a Pimpl, but for times when the types are quite big, it can be a big help (especially in avoiding long build times)

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

回答by Esben Nielsen

Well, I wouldn't use it. I have a better alternative:

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

foo.h:

foo.h:

class Foo {
public:
    virtual ~Foo() { }
    virtual void someMethod() = 0;

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

foo.cpp:

foo.cpp:

namespace {
    class FooImpl: virtual public Foo {

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

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

Does this pattern have a name?

这个图案有名字吗?

As an also Python and Java programmer, I like this a lot more than the pImpl idiom.

作为一名 Python 和 Java 程序员,与 pImpl 习语相比,我更喜欢这个。

回答by nurettin

We use PIMPL idiom in order to emulate aspect oriented programming where pre, post and error aspects are called before and after the execution of a member function.

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

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

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

We also use pointer to base class to share different aspects between many classes.

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

The drawback of this approach is that the library user has to take into account all the aspects that are going to be executed, but only sees his class. It requires browsing the documentation for any side-effects.

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

回答by Phil Wright

Placing the call to the impl->Purr inside the cpp file means that in the future you could do something completely different without having to change the header file. Maybe next year they discover a helper method they could have called instead and so they can change the code to call that directly and not use impl->Purr at all. (Yes, they could achieve the same thing by updating the actual impl::Purr method as well but in that case you are stuck with an extra function call that achieves nothing but calling the next function in turn)

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

It also means the header only has definitions and does not have any implementation which makes for a cleaner separation, which is the whole point of the idiom.

这也意味着标头只有定义,没有任何实现更清晰的分离,这是习语的全部意义。

回答by markh44

I just implemented my first pimpl class over the last couple of days. I used it to eliminate problems I was having including winsock2.h in Borland Builder. It seemed to be screwing up struct alignment and since I had socket things in the class private data, those problems were spreading to any cpp file that included the header.

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

By using pimpl, winsock2.h was included in only one cpp file where I could put a lid on the problem and not worry that it would come back to bite me.

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

To answer the original question, the advantage I found in forwarding the calls to the pimpl class was that the pimpl class is the same as what your original class would have been before you pimpl'd it, plus your implementations aren't spread over 2 classes in some weird fashion. It's much clearer to implement the publics to simply forward to the pimpl class.

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

Like Mr Nodet said, one class, one responsibility.

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

回答by JeffV

I don't know if this is a difference worth mentioning but...

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

Would it be possible to have the implementation in its own namespace and have a public wrapper / library namespace for the code the user sees:

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

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

This way all library code can make use of the cat namespace and as the need to expose a class to the user arises a wrapper could be created in the catlib namespace.

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