背景知识:
PIMPL成语(实现的指针)是一种用于隐藏实现的技术,其中,公共类包装了无法在库外看到的结构或类,而公共类是其中的一部分of
这会向库用户隐藏内部实现的详细信息和数据。
实现此惯用法时,为什么要将公共方法放在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");
}
#1 楼
因为您希望
Purr()
能够使用CatImpl
的私有成员。如果没有Cat::Purr()
声明,将不允许friend
这样的访问。因为那样您就不会混淆职责:一个类实现,一个类转发。
评论
维护起来很痛苦。但是再说一次,如果它是一个库类,则无论如何都不应对方法进行太多更改。我正在查看的代码似乎正在走安全的道路,并在所有地方使用pimpl。
– JeffV
08年9月13日在15:23
因为所有成员都是私有的,所以此行不是非法的:cat _-> Purr(); Purr()无法从外部访问,因为默认情况下它是私有的。我在这里想念什么?
– Binaryguy
15年8月14日在9:20
这两点都没有任何意义。如果您只有一个类别-Cat,那么它也将能够访问其成员,并且不会“混合可重复性”,因为它将是“实施”的类别。使用PIMPL的原因有所不同。
–doc
17年1月10日于13:32
由于提到@doc的原因,这个答案是完全错误的。仅仅为了使用其私人成员而引入一个新的类是荒谬的,而仅仅转发某些东西不是“责任”!
–cubuspl42
19年4月11日在8:00
#2 楼
我认为大多数人将此称为“句柄主体”成语。请参阅James Coplien的书《高级C ++编程样式和习语》(Amazon链接)。它也被称为Cheshire Cat,因为Lewis Caroll的角色逐渐消失,直到只有咧嘴笑为止。示例代码应分布在两组源文件中。然后只有Cat.h是产品随附的文件。Cat.cpp包含Cat.cpp,而CatImpl.cpp包含CatImpl :: Purr()的实现。使用您的产品,这对公众是不可见的。
基本上,这个主意是将尽可能多的实现隐藏起来。
我们通过在2000年重写IONA Orbix 3.3产品来做到这一点。 br />
正如其他人所提到的那样,使用他的技术将实现与对象接口完全分离。这样,如果您只想更改Purr()的实现,就不必重新编译使用Cat的所有内容。
该技术被用于按合同设计的方法中。
评论
@Rob,我想这几乎没有开销。额外的一堂课,但对他们来说却很少。只是现有类的一个薄包装。如果我错了,可以有人纠正我,但是内存的使用只是RAM中的一个额外的函数表,一个指向pimpl的指针以及代码空间中每个方法的重定向函数。虽然维护和调试很痛苦。
– JeffV
08/09/16在23:26
通过合同在设计中如何使用pimpl惯用语(或您所说的“ handle-body idiom”)?
– andreas buykx
08-09-22在20:35
嗨,安德里亚斯,您与API用户的接口仅在于公开的合同(句柄)中,而与实现主体以提供广告功能的方式无关。只要您不更改已发布的API的语义,就可以随意更改实现。
–罗伯·韦尔斯
2014年12月5日16:50
@RobWells我拒绝了这个答案,因为它也是错误的(但不如被接受的错误;此错误可能会得到解决)。问题:a)“基本上,这个想法是将尽可能多的实现隐藏起来。”它与简单的.h / .cpp类声明/定义分离和运输库(.h /(。a | .lib))有何不同?显然,它也向客户端隐藏了实现。 OP在问题中明确提到了这一点!
–cubuspl42
19年4月11日在8:18
FFR:基本上是指stackoverflow.com/questions/8972588/…中可接受的答案
–cubuspl42
19年4月11日在8:24
#3 楼
出于价值考虑,它将实现与接口分离。在小型项目中,这通常不是很重要。但是,在大型项目和库中,可以使用它来显着减少构建时间。考虑到
Cat
的实现可能包含许多标头,可能涉及模板元编程,因此需要花一些时间在它自己的。为什么只想使用Cat
的用户必须包括所有这些内容?因此,所有必要的文件都使用pimpl习惯用法(因此正向声明CatImpl
)隐藏,并且使用该接口不会强制用户包括它们。我正在开发一个用于非线性的库优化(请阅读“令人讨厌的数学”),该优化是在模板中实现的,因此大多数代码都在标头中。编译(在不错的多核CPU上)大约需要五分钟,而仅在空白的
.cpp
中解析标头大约需要一分钟。因此,使用该库的任何人每次编译代码时都必须等待几分钟,这使开发工作变得很繁琐。但是,通过隐藏实现和标头,可以只包含一个简单的接口文件,该文件可以立即编译。不一定与保护实现不被其他公司复制有任何关系-除非您可以从成员变量的定义中猜出算法的内部工作原理,否则无论如何都不会发生(如果这样的话,它可能不是很复杂,一开始就不值得保护)。
#4 楼
如果您的类使用pimpl习惯用法,则可以避免在公共类上更改头文件。这允许您在不修改外部类的头文件的情况下向pimpl类添加/删除方法。您还可以在pimpl中添加/删除#include。
更改外部类的头文件时,必须重新编译包含#include的所有内容(如果其中任何一个是头文件,则需要必须重新编译#包含它们的所有内容,依此类推)
#5 楼
通常,在Owner类(在本例中为Cat)的标头中,对Pimpl类的唯一引用将是前向声明,就像您在此处所做的那样,因为这样可以大大减少依赖性。对于例如,如果您的Pimpl类具有ComplicatedClass作为成员(而不仅仅是指针或对其的引用),那么您将需要在使用ComplicatedClass之前对其进行完全定义。实际上,这意味着包括“ ComplicatedClass.h”(它也将间接包括ComplicatedClass所依赖的任何内容)。这可能导致单个标头填充会引入很多东西,这不利于管理依赖项(以及编译时间)。
使用pimpl idion时,您只需要#包括您的所有者类型(此处为Cat)的公共界面中使用的内容。这对使用您的图书馆的人来说使事情变得更好,并且意味着您无需担心图书馆的某些内部部分,无论是由于错误还是因为他们想做您不允许做的事情,所以他们#define
如果是一个简单的类,通常没有理由使用Pimpl,但是对于类型很大的时候,这可能会有很大的帮助(尤其是避免构建时间长)
#6 楼
好吧,我不会使用它。我有一个更好的选择:class Foo {
public:
virtual ~Foo() { }
virtual void someMethod() = 0;
// This "replaces" the constructor
static Foo *create();
}
namespace {
class FooImpl: virtual public Foo {
public:
void someMethod() {
//....
}
};
}
Foo *Foo::create() {
return new FooImpl;
}
这个模式有名字吗?
作为Python和Java程序员,我比pImpl习惯更喜欢这个。
评论
如果您已经将自己局限于工厂创建对象的方法,那很好。但是它完全消除了值语义,而传统的pImpl可以使用这两种方法。
– Dennis Zickefoose
2011年6月21日15:49
好吧,pImpl基本上只是包装指针。您需要做的就是使上面的create()返回PointerWrapperWithCopySemantics
–埃斯本·尼尔森(Esben Nielsen)
2011-6-22 13:26
为什么继承超过组成?为什么要增加vtable的开销?为什么要虚拟继承?
–正面
13-10-11在14:08
这不适用于模板方法
–smac89
18年2月19日在4:22
#7 楼
我们使用PIMPL习惯用法来模拟面向方面的编程,其中在执行成员函数之前和之后调用前,后和错误方面。struct Omg{
void purr(){ cout<< "purr\n"; }
};
struct Lol{
Omg* omg;
/*...*/
void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};
我们还使用指向基类的指针在许多类之间共享不同方面。
这种方法的缺点是库用户必须考虑将要执行的所有方面,但只能看到他的课程。它需要浏览文档以了解任何副作用。
评论
抛开编译速度参数,这是PIMPL提供的最有益的东西,IMO。
–李国Z
19年2月20日在1:30
#8 楼
将对impl-> Purr的调用放在cpp文件中意味着,将来您可以做完全不同的事情而不必更改头文件。也许明年他们会发现可以调用的辅助方法,因此可以更改代码以直接调用该方法,而根本不使用impl-> Purr。 (是的,他们也可以通过更新实际的impl :: Purr方法来实现相同的目的,但是在这种情况下,您将陷入一个额外的函数调用中,该函数调用只能依次调用下一个函数,什么也无法实现。)这也意味着标头仅具有定义,没有任何实现更清晰分离的实现,这是该习语的全部要点。
#9 楼
在过去的几天里,我刚刚实施了第一届pimpl课。我用它来解决我在Borland Builder中包含winsock2.h时遇到的问题。它似乎搞砸了结构对齐,并且由于我在类私有数据中包含套接字的内容,这些问题已蔓延到任何包含标头的cpp文件中。通过使用pimpl,包括了winsock2.h在一个cpp文件中,我可以控制该问题,而不必担心它会再次咬住我。
要回答最初的问题,我发现了将呼叫转移到pimpl类是pimpl类与您在进行pimpl之前的原始类相同,而且您的实现并未以某种怪异的方式分布在2个类上。让公众直接进入pimpl类更加清晰。
像Nodet先生说的那样,一堂课,一项责任。
#10 楼
我不知道这是否值得一提,但是...是否有可能在自己的名称空间中实现该实现,并为用户看到的代码提供一个公共包装器/库名称空间:
catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
printf("purrrrrr");
}
这样,所有库代码都可以利用cat名称空间,并且由于需要向用户公开类,因此可以在catlib中创建包装器名称空间。
#11 楼
我发现这表明,尽管pimpl惯用语是众所周知的,但我并不认为它在现实生活中经常出现(例如,在开源项目中)。我经常怀疑“好处”是否被夸大了;是的,您可以使一些实现细节更加隐蔽,是的,可以在不更改标题的情况下更改实现,但是这些在现实中并不是很大的优势。
例如,尚不清楚您的实现是否需要隐藏得这么好,而且人们很少真正只更改实现,这是非常罕见的。需要添加新方法时,例如,无论如何都需要更改标题。
评论
是的,只有您可以选择一个好的公共界面并坚持使用它,它才会有所作为。正如Rob Wells所提到的,如果您需要将更新的库分发给链接到库的已编译版本的人员而又不强迫他们重新编译,则这很重要,您只需提供一个新的DLL和密码即可。请注意,使用接口(==在C ++中没有数据成员的抽象类)可以实现几乎相同的事情,并且维护较少(无需手动转发每个公共方法)。但是OTOH,您必须使用特殊的语法来创建实例(即,调用工厂方法)。
– j_random_hacker
09年9月21日在23:14
Qt广泛使用PIMPL习惯用法。
–el.pescado
2010年1月9日,18:15
评论
因为应该避免使用PIMP惯用语?..很好的答案,我发现此链接也包含全面的信息:marcmutz.wordpress.com/translated-articles/pimp-my-pimpl
如果您想帮忙做维护编码器,请记住,这是一种接口模式。不要在每个内部类都使用它。引用《银翼杀手》,我见过s ***你们人们不会相信。
请注意,PIMPL可以带来很多好处,尤其是在较大的项目中,但是会严重使原本简单的较小程序复杂化。在此问题的最下方,列出了在项目中使用PIMPL的最低要求。并非每个人都应该遵循相同的清单,自己做一个并坚持下去。我认为这可能是最好的方法。
我自己的经验是,pimpl受到那些生产大型未记录框架然后离开公司的人们的青睐,因此他们的前同事不得不处理分类蜂,这很难分析...。