背景:
PIMPL Idiom(指向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");
}
答案 0 :(得分:61)
我认为大多数人都将此称为Handle Body成语。请参阅James Coplien的书“高级C ++编程风格和习语”(Amazon link)。它也被称为Cheshire Cat,因为刘易斯卡罗尔的角色逐渐消失,直到只留下咧嘴笑。
示例代码应分布在两组源文件中。然后,只有Cat.h是产品附带的文件。
Cat.cmpl包含CatImpl.h,CatImpl.cpp包含CatImpl :: Purr()的实现。使用您的产品的公众无法看到这一点。
基本上,我们的想法是尽可能地隐藏实施方案。 如果您的商业产品是作为一系列库提供的,那么这是最有用的,这些库可以通过API来访问,客户的代码是针对这些库进行编译和链接的。
我们在2000年重写了IONAs Orbix 3.3产品。
正如其他人所提到的,使用他的技术完全将实现与对象的接口分离。然后,如果您只想更改Purr()的实现,则不必重新编译使用Cat的所有内容。
此技术用于名为design by contract的方法中。
答案 1 :(得分:40)
Purr()
能够使用CatImpl
的私人成员。如果没有Cat::Purr()
声明,则不允许friend
进行此类访问。答案 2 :(得分:19)
值得的是,它将实现与接口分开。这在小型项目中通常不是很重要。但是,在大型项目和库中,它可用于显着缩短构建时间。
考虑Cat
的实现可能包含许多标题,可能涉及模板元编程,这需要花费时间自行编译。为什么只想使用Cat
的用户必须包含所有这些内容?因此,使用pimpl习惯用法隐藏所有必要的文件(因此CatImpl
的前向声明),并且使用界面不会强制用户包含它们。
我正在开发一个非线性优化库(读“很多讨厌的数学”),它是在模板中实现的,所以大部分代码都在标题中。编译大约需要五分钟(在一个不错的多核CPU上),只需在空的.cpp
中解析标题大约需要一分钟。因此,每次使用该库的人都必须在每次编译代码时等待几分钟,这使得开发非常tedious。但是,通过隐藏实现和标题,只需包含一个简单的接口文件,即可立即编译。
它不一定与保护实现不被其他公司复制有任何关系 - 除非你的算法的内部工作可以从成员变量的定义中猜到(如果是这样的话) ,它可能不是很复杂,一开始就不值得保护。)
答案 3 :(得分:14)
如果你的班级使用pimpl习语,你可以避免更改公共类的头文件。
这允许您向pimpl类添加/删除方法,而无需修改外部类的头文件。你也可以在pimpl中添加/删除#includes。
当您更改外部类的头文件时,您必须重新编译#includes它的所有内容(如果其中任何一个是头文件,您必须重新编译#includes它们的所有内容,等等)
答案 4 :(得分:6)
通常,在Owner类的头文件中引用Pimpl类的唯一引用(在本例中为Cat)将是一个前向声明,正如您在此处所做的那样,因为这可以大大减少依赖性。
例如,如果您的Pimpl类将ComplicatedClass作为成员(而不仅仅是指针或对它的引用),那么您需要在使用之前完全定义ComplicatedClass。在实践中,这意味着包括“ComplicatedClass.h”(它也将间接包括ComplicatedClass所依赖的任何内容)。这可能会导致单个标头填充大量内容,这对于管理依赖项(以及编译时间)是不利的。
当你使用pimpl idion时,你只需要#include你所有者类型的公共接口中使用的东西(这里是Cat)。这使得使用你的图书馆的人的事情变得更好,这意味着你不需要担心依赖于图书馆内部部分的人 - 要么是错误的,要么是因为他们想要做一些你不允许的事情,所以他们#define包含您的文件之前的私人公众。
如果它是一个简单的类,通常没有理由使用Pimpl,但是对于类型非常大的时候,它可以是一个很大的帮助(特别是在避免长构建时间)
答案 5 :(得分:4)
好吧,我不会用它。我有一个更好的选择:
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习惯用法更喜欢这个。
答案 6 :(得分:3)
我们使用PIMPL习惯用法来模拟面向方面的编程,其中在执行成员函数之前和之后调用pre,post和error方面。
struct Omg{
void purr(){ cout<< "purr\n"; }
};
struct Lol{
Omg* omg;
/*...*/
void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};
我们还使用指向基类的指针来共享许多类之间的不同方面。
这种方法的缺点是库用户必须考虑将要执行的所有方面,但只能看到他的类。它需要浏览文档以了解任何副作用。
答案 7 :(得分:2)
将调用放在cpp文件中的impl-&gt; Purr意味着将来你可以做一些完全不同的事情,而无需更改头文件。也许明年他们会发现他们可以调用的辅助方法,因此他们可以更改代码直接调用它,而不是使用impl-&gt; Purr。 (是的,他们可以通过更新实际的impl :: Purr方法来实现相同的功能,但在这种情况下,你会遇到一个额外的函数调用,只能依次调用下一个函数)
这也意味着标题只有定义,并且没有任何实现,这使得更清晰的分离,这是成语的全部要点。
答案 8 :(得分:1)
我刚刚在过去几天实施了我的第一个pimpl课程。我用它来消除我在Borland Builder中遇到的包括winsock2.h的问题。它似乎搞砸了结构对齐,因为我在类私有数据中有套接字的东西,这些问题正在传播到包含标题的任何cpp文件。
通过使用pimpl,winsock2.h只包含在一个cpp文件中,我可以对这个问题设置一个限制,不用担心它会回来咬我。
为了回答原始问题,我在将调用转发到pimpl类时发现的优势是pimpl类与你的原始类相同,然后你的实现不是以某种奇怪的方式分散了两个班级。实现公众简单地转发到pimpl类更加清晰。
像Nodet先生所说的那样,一个阶级,一个责任。答案 9 :(得分:0)
我不知道这是否值得一提但是......
是否可以将实现放在自己的命名空间中,并为用户看到的代码提供公共包装/库命名空间:
catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
printf("purrrrrr");
}
这样所有库代码都可以使用cat命名空间,并且由于需要向用户公开类,因此可以在catlib命名空间中创建包装器。
答案 10 :(得分:0)
我发现它告诉我,尽管pimpl成语有多么出名,但我并不认为它在现实生活中经常出现(例如在开源项目中)。
我常常想知道“好处”是否被夸大了;是的,你可以使你的一些实现细节更加隐藏,是的,你可以在不改变标题的情况下改变你的实现,但实际上这些都是很大的优势并不明显。
也就是说,不清楚你的实现是否需要 隐藏得很好,而且很可能人们确实只改变了实现;例如,只要你需要添加新方法,就需要更改标题。