提问人:JeffV 提问时间:9/13/2008 最后编辑:Peter G.JeffV 更新时间:4/10/2021 访问量:122783
为什么要使用“PIMPL”成语?[复制]
Why should the "PIMPL" idiom be used? [duplicate]
问:
背景资料:
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");
}
答:
将对 impl->Purr 的调用放在 .cpp 文件中意味着将来可以执行完全不同的操作,而无需更改头文件。
也许明年他们发现了一个他们可以调用的辅助方法,因此他们可以更改代码以直接调用它,而根本不使用 impl->Purr。(是的,他们也可以通过更新实际的impl::P urr方法来实现相同的目的,但在这种情况下,你被困在一个额外的函数调用中,除了依次调用下一个函数之外什么也做不了。
这也意味着标题只有定义,没有任何实现,这使得分离更清晰,这是成语的重点。
我想大多数人都把这称为手柄身体成语。参见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 的所有内容。
这种技术用于一种称为合同设计的方法。
评论
通常,在所有者类(在本例中为 Cat)的标头中对 PIMPL 类的唯一引用是正向声明,就像您在此处所做的那样,因为这可以大大减少依赖关系。
例如,如果 PIMPL 类将 ComplicatedClass 作为成员(而不仅仅是指针或对它的引用),则需要在使用之前完全定义 ComplicatedClass。在实践中,这意味着包括文件“ComplicatedClass.h”(它还将间接包含ComplicatedClass所依赖的任何内容)。这可能会导致单个标头填充拉入大量内容,这不利于管理依赖项(和编译时间)。
当你使用 PIMPL 习惯语时,你只需要 #include 你的所有者类型(这里是 Cat)的公共接口中使用的内容。这对使用你的库的人来说会更好,也意味着你不需要担心人们依赖于你库的某些内部部分 - 无论是错误的,还是因为他们想做一些你不允许的事情,所以他们在包含你的文件之前 #define 私有的公开。
如果它是一个简单的类,通常没有任何理由使用 PIMPL,但对于类型相当大的时候,它可能是一个很大的帮助(尤其是在避免长时间构建方面)。
- 因为您希望能够使用 的私有成员。 如果没有声明,将不允许进行此类访问。
Purr()
CatImpl
Cat::Purr()
friend
- 因为这样你就不会混合责任:一个类实现,一个类转发。
评论
Cat
如果您的类使用 PIMPL 惯用语,则可以避免更改公共类的头文件。
这允许您向 PIMPL 类添加/删除方法,而无需修改外部类的头文件。您也可以在 PIMPL 中添加/删除 #includes。
当您更改外部类的头文件时,您必须重新编译 #includes 它的所有内容(如果其中任何一个是头文件,则必须重新编译 #includes 它们的所有内容,依此类推)。
我不知道这是否是一个值得一提的差异,但是......
是否可以将实现放在自己的命名空间中,并为用户看到的代码提供公共包装器/库命名空间:
catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
printf("purrrrrr");
}
这样,所有库代码都可以使用 cat 命名空间,并且当需要向用户公开类时,可以在 catlib 命名空间中创建包装器。
我发现这很能说明问题,尽管 PIMPL 成语很有名,但我并不认为它在现实生活中经常出现(例如,在开源项目中)。
我常常在想,“好处”是不是被夸大了;是的,你可以使你的一些实现细节更加隐蔽,是的,你可以在不改变标题的情况下改变你的实现,但这些在现实中并不明显。
也就是说,目前尚不清楚您的实现是否需要隐藏得那么好,也许很少有人真正只更改实现;比如说,一旦你需要添加新的方法,你就需要改变标题。
评论
在过去的几天里,我刚刚实施了我的第一个 PIMPL 课程。我用它来消除我遇到的问题,包括Borland Builder中的文件*winsock2.*h。它似乎搞砸了结构对齐,并且由于我在类私有数据中有套接字的东西,这些问题正在蔓延到包含标头的任何 .cpp 文件。
通过使用 PIMPL,winsock2.h 仅包含在一个 .cpp 文件中,我可以在其中盖上问题的盖子,而不用担心它会回来咬我。
为了回答最初的问题,我发现将调用转发到 PIMPL 类的优势在于,PIMPL 类与你 PIMPL 之前的原始类相同,而且你的实现不会以某种奇怪的方式分布在两个类上。将公共成员实现为简单地转发到 PIMPL 类要清楚得多。
就像诺德先生说的,一个班级,一个责任。
好吧,我不会使用它。我有一个更好的选择:
文件 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 成语。
评论
值得一提的是,它将实现与接口分开。这在小型项目中通常不是很重要。但是,在大型项目和库中,它可用于显着减少构建时间。
考虑到实现可能包含许多标头,可能涉及模板元编程,这需要时间自行编译。为什么只想使用必须包含所有这些的用户?因此,所有必要的文件都使用 pimpl 惯用语隐藏(因此是 的正向声明),并且使用接口不会强制用户包含它们。Cat
Cat
CatImpl
我正在开发一个用于非线性优化的库(阅读“很多讨厌的数学”),它是在模板中实现的,所以大部分代码都在标题中。编译大约需要五分钟(在一个体面的多核 CPU 上),而仅仅解析一个空的标头就需要大约一分钟。因此,任何使用该库的人每次编译代码时都必须等待几分钟,这使得开发非常乏味。但是,通过隐藏实现和标头,只需包含一个简单的接口文件,该文件可以立即编译。.cpp
它不一定与保护实现不被其他公司复制有任何关系——无论如何这不太可能发生,除非可以从成员变量的定义中猜出算法的内部工作原理(如果是这样,它可能不是很复杂,不值得首先保护)。
我们使用 PIMPL 习惯用来模拟面向方面的编程,其中在成员函数执行之前和之后调用前、后和错误方面。
struct Omg{
void purr(){ cout<< "purr\n"; }
};
struct Lol{
Omg* omg;
/*...*/
void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};
我们还使用指向基类的指针在许多类之间共享不同的方面。
这种方法的缺点是,库用户必须考虑将要执行的所有方面,但只能看到他/她的类。它需要浏览文档以查找任何副作用。
评论