为什么“PIMPL”应该被采用? 可以用成语吗?
背景资料:
PIMPL 惯用法(指向 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");
}
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.
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?
To illustrate, this code puts the Purr()
implementation on the impl class and wraps it as well.
Why not implement Purr directly on the public class?
// 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");
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
我发现它很能说明问题,尽管 PIMPL 习惯用法非常出名,但我并没有看到它在现实生活中经常出现(例如,在开源项目中)。
我常常想,“好处”是否被夸大了? 是的,您可以使一些实现细节更加隐藏,是的,您可以在不更改标头的情况下更改实现,但实际上这些优势并不明显。
也就是说,尚不清楚您的实现是否需要隐藏得很好,而且也许人们很少真正只更改实现; 例如,一旦您需要添加新方法,您就需要更改标头。
I find it telling that, in spite of how well-known the PIMPL idiom is, I don't see it crop up very often in real life (e.g., in open source projects).
I often wonder if the "benefits" are overblown; yes, you can make some of your implementation details even more hidden, and yes, you can change your implementation without changing the header, but it's not obvious that these are big advantages in reality.
That is to say, it's not clear that there's any need for your implementation to be that well hidden, and perhaps it's quite rare that people really do change only the implementation; as soon as you need to add new methods, say, you need to change the header anyway.
将对 impl->Purr 的调用放在 .cpp 文件中意味着将来您可以做一些完全不同的事情,而无需更改头文件。
也许明年他们会发现一个可以调用的辅助方法,因此他们可以更改代码以直接调用该方法,而根本不使用 impl->Purr。 (是的,他们也可以通过更新实际的 impl::Purr 方法来实现相同的目标,但在这种情况下,您会陷入额外的函数调用,除了依次调用下一个函数之外什么也实现不了。)
它也意味着标头只有定义,没有任何实现,这使得分离更清晰,这就是这个习惯用法的全部要点。
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.)
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.
嗯,我不会用它。 我有一个更好的选择:
文件 foo.h
文件 foo.cpp
这个模式有名称吗?
作为一个同时也是 Python 和 Java 程序员的人,我比 PIMPL 习惯更喜欢这个。
Well, I wouldn't use it. I have a better alternative:
File foo.h
File foo.cpp
Does this pattern have a name?
As someone who is also a Python and Java programmer, I like this a lot more than the PIMPL idiom.
通常,owner 类(本例中为 Cat)标头中对 PIMPL 类的唯一引用将是前向声明,正如您在此处所做的那样,因为可以大大减少依赖。
例如,如果您的 PIMPL 类将 ComplicatedClass 作为成员(而不仅仅是指向它的指针或引用),那么您需要在使用之前完全定义 ComplicatedClass。 实际上,这意味着包含文件“ComplicatedClass.h”(它也将间接包含 ComplicatedClass 所依赖的任何内容)。 这可能会导致单个标头填充拉入大量内容,这不利于管理您的依赖项(以及编译时间)。
当您使用 PIMPL 习惯用法时,您只需 #include owner 类型的公共接口中使用的内容(此处为 Cat)。 这让使用你的库的人变得更好,并且意味着你不需要担心人们依赖你的库的某些内部部分 - 无论是错误的,还是因为他们想做你不允许的事情,所以他们#在包含您的文件之前定义私有公共。
如果它是一个简单的类,通常没有任何理由使用 PIMPL,但当类型相当大时,它可能会有很大帮助(特别是在避免较长的构建时间方面)。
Typically, the only reference to a 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.
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 its use. In practice, this means including file "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).
When you use the PIMPL idiom, 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.
If it's a simple class, there's usually isn't any 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 习惯用法,则可以避免更改公共类上的头文件。
这允许您向 PIMPL 类添加/删除方法,而无需修改外部类的头文件。 您还可以向 PIMPL 添加/删除 #include。
当您更改外部类的头文件时,您必须重新编译#includes它的所有内容(如果其中任何一个是头文件,您必须重新编译#includes它们的所有内容,依此类推)。
If your class uses the PIMPL idiom, you can avoid changing the header file on the public class.
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.
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).
Purr()
能够使用CatImpl
的私有成员。 如果没有friend
声明,则不允许Cat::Purr()
进行此类访问。Purr()
to be able to use private members ofCatImpl
.Cat::Purr()
would not be allowed such an access without afriend
declaration.值得一提的是,它将实现与接口分开。 这在小型项目中通常不是很重要。 但是,在大型项目和库中,它可以用来显着减少构建时间。
考虑到
Cat
的实现可能包含许多标头,可能涉及模板元编程,这需要时间来自行编译。 为什么只想使用Cat
的用户必须包含所有这些内容? 因此,所有必需的文件都使用 pimpl 习惯用法隐藏(因此CatImpl
的前向声明),并且使用该接口不会强制用户包含它们。我正在开发一个用于非线性优化的库(阅读“大量令人讨厌的数学”),它是在模板中实现的,因此大部分代码都在标头中。 编译大约需要五分钟(在一个不错的多核 CPU 上),仅解析空的
.cpp
中的标头大约需要一分钟。 因此,使用该库的任何人每次编译代码时都必须等待几分钟,这使得开发变得非常乏味。 然而,通过隐藏实现和标头,只需包含一个简单的接口文件,即可立即编译。它不一定与保护实现不被其他公司复制有任何关系 - 这可能不会发生,除非可以从成员变量的定义中猜测算法的内部工作原理(如果是这样,那就是可能不是很复杂,而且一开始就不值得保护)。
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
Cat
may 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 theCat
have to include all that? Hence, all the necessary files are hidden using the pimpl idiom (hence the forward declaration ofCatImpl
), and using the interface does not force the user to include them.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
.cpp
takes 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.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).
我认为大多数人将其称为“Handle Body”惯用语。 请参阅 James Coplien 的书高级 C++ 编程风格和习惯用法 。 它也被称为柴郡猫,因为刘易斯·卡罗尔的角色逐渐消失,直到只剩下笑容。
示例代码应分布在两组源文件中。 那么只有 Cat.h 是产品附带的文件。
CatImpl.h 包含在 Cat.cpp 中,CatImpl.cpp 包含 CatImpl::Purr() 的实现 >。 使用您的产品的公众不会看到这一点。
基本上,这个想法是尽可能隐藏实施过程,以免被窥探。
当您拥有作为一系列库提供的商业产品时,这非常有用,这些库可通过 API 进行访问,客户的代码将根据该 API 进行编译和链接。
我们通过重写IONAOrbix 2000 年的 3.3 产品。
正如其他人提到的,使用他的技术将实现与对象的接口完全解耦。 那么,如果您只想更改 Purr() 的实现,则不必重新编译使用 Cat 的所有内容。
该技术用于名为按合同设计的方法。
I think most people refer to this as the Handle Body idiom. See James Coplien's book Advanced C++ Programming Styles and Idioms. It's also known as the Cheshire Cat because of Lewis Caroll's character that fades away until only the grin remains.
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.
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.
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.
We did this with the rewrite of IONA's Orbix 3.3 product in 2000.
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().
This technique is used in a methodology called design by contract.
过去几天我刚刚实现了我的第一个 PIMPL 课程。 我用它来解决我遇到的问题,包括 Borland Builder 中的文件 *winsock2.*h。 它似乎搞砸了结构对齐,并且由于我在类私有数据中有套接字内容,这些问题正在蔓延到包含标头的任何 .cpp 文件。
通过使用 PIMPL,winsock2.h 仅包含在一个 .cpp 文件中,我可以在其中解决问题,而不必担心它会回来困扰我。
为了回答最初的问题,我发现将调用转发到 PIMPL 类的优点是 PIMPL 类与您在 pimpl 之前的原始类相同,而且您的实现不会分布在两个以某种奇怪的方式上课。 实现公共成员以简单地转发到 PIMPL 类会更加清晰。
就像Nodet先生所说的,一类,一责。
I just implemented my first PIMPL class over the last couple of days. I used it to eliminate problems I was having, including file *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.
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.
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 two classes in some weird fashion. It's much clearer to implement the public members to simply forward to the PIMPL class.
Like Mr Nodet said, one class, one responsibility.
我们使用 PIMPL 习惯用法来模拟面向方面的编程,其中 pre、post 和错误方面在成员函数执行之前和之后调用。
我们还使用指向基类的指针来共享许多类之间的不同方面。
这种方法的缺点是库用户必须考虑将要执行的所有方面,但只能看到他/她的类。 它需要浏览文档以了解任何副作用。
We use the 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.
We also use a 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/her class. It requires browsing the documentation for any side effects.
我不知道这是否是一个值得一提的差异,但是...
是否可以在自己的命名空间中实现,并为用户看到的代码提供一个公共包装器/库命名空间:
这样所有库代码都可以使用 cat 命名空间,并且当需要向用户公开类时,可以在 catlib 命名空间中创建包装器。
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:
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.