PIMPL 问题:如何在不重复代码的情况下实现多个接口
我有这个 pimpl 设计,其中实现类是多态的,但接口应该只包含一个指针,使它们多态在某种程度上违背了设计的目的。
因此,我创建了 Impl 和 Intf 基类来提供引用计数。然后用户可以创建他们的实现。一个例子:
class Impl {
mutable int _ref;
public:
Impl() : _ref(0) {}
virtual ~Impl() {}
int addRef() const { return ++_ref; }
int decRef() const { return --_ref; }
};
template <typename TImpl>
class Intf {
TImpl* impl;
public:
Intf(TImpl* t = 0) : impl(0) {}
Intf(const Intf& other) : impl(other.impl) { if (impl) impl->addRef(); }
Intf& operator=(const Intf& other) {
if (other.impl) other.impl->addRef();
if (impl && impl->decRef() <= 0) delete impl;
impl = other.impl;
}
~Intf() { if (impl && impl->decRef() <= 0) delete impl; }
protected:
TImpl* GetImpl() const { return impl; }
void SetImpl(... //etc
};
class ShapeImpl : public Impl {
public:
virtual void draw() = 0;
};
class Shape : public Intf<ShapeImpl> {
public:
Shape(ShapeImpl* i) : Intf<ShapeImpl>(i) {}
void draw() {
ShapeImpl* i = GetImpl();
if (i) i->draw();
}
};
class TriangleImpl : public ShapeImpl {
public:
void draw();
};
class PolygonImpl : public ShapeImpl {
public:
void draw();
void addSegment(Point a, Point b);
};
这里是有问题的地方。 Polygon 类有两种可能的声明:
class Polygon1 : public Intf<PolygonImpl> {
public:
void draw() {
PolygonImpl* i = GetImpl();
if (i) i->draw();
}
void addSegment(Point a, Point b) {
PolygonImpl* i = GetImpl();
if (i) i->addSegment(a,b);
}
};
class Polygon2 : public Shape {
void addSegment(Point a, Point b) {
ShapeImpl* i = GetImpl();
if (i) dynamic_cast<Polygon*>(i)->addSegment(a,b);
}
}
在 Polygon1 中,我重写了绘制代码,因为我没有继承它。在 Polygon2 中,我需要丑陋的动态转换,因为 GetImpl() 不知道 PolygonImpl。我想做的是这样的:
template <typename TImpl>
struct Shape_Interface {
void draw() {
TImpl* i = GetImpl();
if (i) i->draw();
}
};
template <typename TImpl>
struct Polygon_Interface : public Shape_Interface<Timpl> {
void addSegment(Point a, Point b) { ... }
};
class Shape : public TIntf<ShapeImpl>, public Shape_Interface<ShapeImpl> {...};
class Polygon : public TIntf<PolygonImpl>, public Polygon_Interface<PolygonImpl> {
public:
Polygon(PolygonImpl* i) : TIntf<PolygonImpl>(i) {}
};
但是这里当然有一个问题。我无法从 Interface 类访问 GetImpl() ,除非我从 Intf 派生它们。如果我这样做,我需要将 Intf 设为虚拟,无论它出现在哪里。
template <typename TImpl>
class PolygonInterface : public virtual Intf<TImpl> { ... };
class Polygon : public virtual Intf<PolygonImpl>, public PolygonInterface { ... }
或者我可以存储 TImpl*&在每个接口中并使用对基础 Intf::impl 的引用来构造它们。但这仅仅意味着我有一个指针指向我自己包含的每个接口。
template <typename TImpl>
class PolygonInterface {
TImpl*& impl;
public:
PolygonInterface(TImpl*& i) : impl(i) {}
...};
这两种解决方案都会使 Intf 类膨胀,添加额外的取消引用,并且基本上没有比直接多态性提供任何好处。
所以,问题是,除了到处复制代码(及其维护问题)之外,是否还有我错过的第三种方法可以解决这个问题?
完全应该,但不起作用:我希望有基类联合来覆盖类布局,并且对于多态类,要求它们具有完全相同的 vtable 布局。然后 Intf 和 ShapeInterface 都会声明一个 T* 元素并以相同的方式访问它:
class Shape : public union Intf<ShapeImpl>, public union ShapeInterface<ShapeImpl> {};
I have this pimpl design where the implementation classes are polymorphic but the interfaces are supposed to just contain a pointer, making them polymorphic somewhat defeats the purpose of the design.
So I create my Impl and Intf base classes to provide reference counting. And then the user can create their implementations. An example:
class Impl {
mutable int _ref;
public:
Impl() : _ref(0) {}
virtual ~Impl() {}
int addRef() const { return ++_ref; }
int decRef() const { return --_ref; }
};
template <typename TImpl>
class Intf {
TImpl* impl;
public:
Intf(TImpl* t = 0) : impl(0) {}
Intf(const Intf& other) : impl(other.impl) { if (impl) impl->addRef(); }
Intf& operator=(const Intf& other) {
if (other.impl) other.impl->addRef();
if (impl && impl->decRef() <= 0) delete impl;
impl = other.impl;
}
~Intf() { if (impl && impl->decRef() <= 0) delete impl; }
protected:
TImpl* GetImpl() const { return impl; }
void SetImpl(... //etc
};
class ShapeImpl : public Impl {
public:
virtual void draw() = 0;
};
class Shape : public Intf<ShapeImpl> {
public:
Shape(ShapeImpl* i) : Intf<ShapeImpl>(i) {}
void draw() {
ShapeImpl* i = GetImpl();
if (i) i->draw();
}
};
class TriangleImpl : public ShapeImpl {
public:
void draw();
};
class PolygonImpl : public ShapeImpl {
public:
void draw();
void addSegment(Point a, Point b);
};
Here is where have the issue. There are two possible declaration for class Polygon:
class Polygon1 : public Intf<PolygonImpl> {
public:
void draw() {
PolygonImpl* i = GetImpl();
if (i) i->draw();
}
void addSegment(Point a, Point b) {
PolygonImpl* i = GetImpl();
if (i) i->addSegment(a,b);
}
};
class Polygon2 : public Shape {
void addSegment(Point a, Point b) {
ShapeImpl* i = GetImpl();
if (i) dynamic_cast<Polygon*>(i)->addSegment(a,b);
}
}
In the Polygon1, I have rewrite the code for draw because I have not inherited it. In Polygon2 I need ugly dynamic casts because GetImpl() doesn't know about PolygonImpl. What I would like to do is something like this:
template <typename TImpl>
struct Shape_Interface {
void draw() {
TImpl* i = GetImpl();
if (i) i->draw();
}
};
template <typename TImpl>
struct Polygon_Interface : public Shape_Interface<Timpl> {
void addSegment(Point a, Point b) { ... }
};
class Shape : public TIntf<ShapeImpl>, public Shape_Interface<ShapeImpl> {...};
class Polygon : public TIntf<PolygonImpl>, public Polygon_Interface<PolygonImpl> {
public:
Polygon(PolygonImpl* i) : TIntf<PolygonImpl>(i) {}
};
But of course there's a problem here. I can't access GetImpl() from the Interface classes unless I derive them from Intf. And if I do that, I need to make Intf virtual everywhere it appears.
template <typename TImpl>
class PolygonInterface : public virtual Intf<TImpl> { ... };
class Polygon : public virtual Intf<PolygonImpl>, public PolygonInterface { ... }
OR I can store a TImpl*& in each Interface and construct them with a reference to the base Intf::impl. But that just means I have a pointer pointing back into myself for every interface included.
template <typename TImpl>
class PolygonInterface {
TImpl*& impl;
public:
PolygonInterface(TImpl*& i) : impl(i) {}
...};
Both of these solutions bloat the Intf class, add an extra dereference, and basically provide no benefit over straight polymorphism.
So, the question is, is there a third way, that I've missed that would solve this issue besides just duplicating the code everywhere (with its maintenance issues)?
TOTALLY SHOULD, BUT DOESN'T WORK: I wish there were base classes unions that just overlaid the class layouts and, for polymorphic classes, required that they have the exact same vtable layout. Then both Intf and ShapeInterface would each declare a single T* element and access it identically:
class Shape : public union Intf<ShapeImpl>, public union ShapeInterface<ShapeImpl> {};
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我应该注意到,您的
Impl
类只不过是shared_ptr
的重新实现,没有线程安全性和所有这些优势。Pimpl 只是一种避免不必要的编译时依赖的技术。
您不需要实际知道如何实现一个类来继承它。它会破坏封装的目的(尽管你的编译器会......)。
所以...我认为你并没有尝试在这里使用 Pimpl。我宁愿认为这是一种代理模式,因为显然:
如果你想隐藏实现细节
使用
Pimpl
,但真正的,这意味着在复制构造和赋值而不是仅仅传递指针(无论是否进行引用计数,尽管引用计数当然更好:))。如果您想要一个代理类,
只需使用普通的
shared_ptr
即可。对于继承
,当你从一个类继承时,它的私有成员如何实现并不重要。所以只需继承它即可。
如果您想添加一些新的私有成员(通常情况),那么:
正如您所看到的,与经典实现没有太大区别,只是有一个指针代替了一堆数据。
注意
如果你转发声明
DerivedImpl
(这是Pimpl的目标),那么编译器自动生成的析构函数是......错误的。问题是,为了生成析构函数的代码,编译器需要 DerivedImpl 的定义(即:完整类型)才能知道如何销毁它,因为对删除的调用是隐藏在shared_ptr的内部。但是,它可能只会在编译时生成警告(但您会遇到内存泄漏)。
此外,如果您想要一个深度副本(而不是浅度副本,其中副本和原始副本都指向同一个
DerivedImpl
实例),您还必须手动定义副本- 构造函数 AND 赋值运算符。您可能决定创建一个比
shared_ptr
更好的类,它将具有深度复制语义(可以像 cryptopp 中那样称为member_ptr
,或者只是Pimpl
> ;) )。但这引入了一个微妙的错误:虽然为复制构造函数和赋值运算符生成的代码可以被认为是正确的,但它们不是,因为您再次需要一个完整的类型(因此需要定义DerivedImpl< /code>),所以你必须手动编写它们。
这很痛苦……我为你感到难过。
编辑:让我们来讨论一下形状。
你看:Circle 和 Rectangle 都不关心 Shape 是否使用 Pimpl,顾名思义,Pimpl 是一个实现细节,是私有的,不与该类的后代共享。
正如我所解释的,Circle 和 Rectangle 也都使用 Pimpl,每个都有自己的“实现类”(顺便说一下,它只不过是一个没有方法的简单结构)。
I should note that your
Impl
class is nothing more than the reimplementation of ashared_ptr
without the thread safety and all those cast bonuses.Pimpl is nothing but a technic to avoid needless compile-time dependencies.
You do not need to actually know how a class is implemented to inherit from it. It would defeat the purpose of encapsulation (though your compiler does...).
So... I think that you are not trying to use Pimpl here. I would rather think this is a kind of Proxy patterns, since apparently:
If you want to hide implementation details
Use
Pimpl
, but the real one, it means copying in depth during copy construction and assignment rather than just passing the pointer around (whether ref-counted or not, though ref-counted is preferable of course :) ).If you want a proxy class
Just use a plain
shared_ptr
.For inheritance
It does not matter, when you inherit from a class, how its private members are implemented. So just inherit from it.
If you want to add some new private members (usual case), then:
There is not much difference with classic implementation, as you can see, just that there is a pointer in lieu of a bunch of data.
BEWARE
If you forward declare
DerivedImpl
(which is the goal of Pimpl), then the destructor automatically generated by the compiler is... wrong.The problem is that in order to generate the code for the destructor, the compiler needs the definition of
DerivedImpl
(ie: a complete type) in order to know how to destroy it, since a call to delete is hidden in the bowels of shared_ptr. However it may only generate a warning at compilation time (but you'll have a memory leak).Furthermore, if you want an in-depth copy (rather than a shallow one, which consists in the copy and the original both pointing to the same
DerivedImpl
instance), you will also have to define manually the copy-constructor AND the assignment operator.You may decide to create a better class that
shared_ptr
which will have deep-copy semantics (which could be calledmember_ptr
as in cryptopp, or justPimpl
;) ). This introduce a subtle bug though: while the code generated for the copy-constructor and the assignement operator could be thought of as correct, they are not, since once again you need a complete type (and thus the definition ofDerivedImpl
), so you will have to write them manually.This is painful... and I'm sorry for you.
EDIT: Let's have a Shape discussion.
You see: neither Circle nor Rectangle care if Shape uses Pimpl or not, as its name implies, Pimpl is an implementation detail, something private that is not shared with the descendants of the class.
And as I explained, both Circle and Rectangle use Pimpl too, each with their own 'implementation class' (which can be nothing more than a simple struct with no method by the way).
我认为你是对的,因为我最初不明白你的问题。
我认为你试图将一个正方形强行放入一个圆孔中......它不太适合 C++。
您可以强制容器保存指向给定基本布局的对象的指针,然后允许从那里实际指向任意组合的对象,假设您作为程序员仅实际放置以下对象:实际上具有相同的内存布局(成员数据 - 类不存在诸如成员函数布局之类的东西,除非它具有虚拟值,您希望避免这种情况)。
注意,在绝对最低限度上,您仍然必须在 IShape 中定义一个虚拟析构函数,否则对象删除将会严重失败
并且您可以拥有所有都采用指向公共实现核心的指针的类,以便可以使用他们共享的元素(或者可以通过指针作为模板静态完成 - 共享数据)。
但问题是,如果我尝试创建一个示例,当我尝试考虑以下问题时,我就会失败:所有形状共享的数据是什么?我想你可以有一个点向量,然后它可以根据需要的任何形状而大或小。但即便如此,Draw() 确实是多态的,它不是一个可以由多种类型共享的实现 - 它必须针对形状的各种分类进行定制。即圆形和多边形不可能共享相同的Draw()。如果没有 vtable(或其他一些动态函数指针构造),您就无法改变从某些常见实现或客户端调用的函数。
您的第一组代码充满了令人困惑的结构。也许您可以添加一个新的、简化的示例,以更现实的方式纯粹显示您正在尝试执行的操作(并忽略 C++ 没有您想要的机制这一事实 - 只需演示您的机制应该是什么样子)。
在我看来,我只是没有得到实际的实际应用,除非您尝试执行以下操作:
采用一个 COM 类,它继承自其他两个 COM 接口:
现在我有一个菱形继承模式: IShellBrowser最终继承自 IUnknown,ICommDlgBrowser 也是如此。但必须编写自己的 IUnknown:AddRef 和 IUnknown::Release 实现(这是一个高度标准的实现)似乎非常愚蠢,因为没有办法让编译器让另一个继承类为 IShellBrowser 和/提供缺少的虚拟函数或 ICommDlgBrowser。
即,我最终不得不:
因为我不知道如何从其他任何地方“继承”或“注入”这些函数实现到 MyShellBrowserDialog 中,它们实际上填充了所需的虚拟成员函数 IShellBrowser 或 ICommDlgBrowser。
如果实现更复杂,如果我愿意,我可以手动将 vtable 链接到继承的实现者:
如果我需要混合来实际引用最派生的类以与其交互,我可以添加一个模板参数 IUnknownMixin,以使其能够访问我自己。
但是我的类可以拥有哪些 IUnknownMixin 本身无法提供的共同元素或从中受益?
任何复合类可以具有哪些公共元素是各种 mixin 想要访问的、它们需要从自身派生的?只需让 mixin 接受一个类型参数并访问它即可。如果它的实例数据是最派生的,那么你会得到类似的结果:
最终你的问题对我来说仍然有些困惑。也许您可以创建一个示例来显示您首选的自然语法,该语法可以清楚地完成某些任务,因为我在您最初的帖子中没有看到这一点,而且我自己似乎无法通过尝试这些想法来找出它。
I think you were right in that I didn't understand your question initially.
I think you're trying to force a square shape into a round hole... it don't quite fit C++.
You can force that your container holds pointers to objects of a given base-layout, and then allow objects of arbitrary composition to be actually pointed to from there, assuming that you as a programmer only actually place objects that in fact have identical memory layouts (member-data - there's no such thing as member-function-layout for a class unless it has virtuals, which you wish to avoid).
NOTE at the absolute MINIMUM, you must still have a virtual destructor defined in IShape, or object deletion is going to fail miserably
And you could have classes which all take a pointer to a common implementation core, so that all compositions can be initialized with the element that they share (or it could be done statically as a template via pointer - the shared data).
But the thing is, if I try to create an example, I fall flat the second I try to consider: what is the data shared by all shapes? I suppose you could have a vector of Points, which then could be as large or small as any shape required. But even so, Draw() is truly polymorphic, it isn't an implementation that can possibly be shared by multiple types - it has to be customized for various classifications of shapes. i.e. a circle and a polygon cannot possibly share the same Draw(). And without a vtable (or some other dynamic function pointer construct), you cannot vary the function called from some common implementation or client.
Your first set of code is full of confusing constructs. Maybe you can add a new, simplified example that PURELY shows - in a more realistic way - what you're trying to do (and ignore the fact that C++ doesn't have the mechanics you want - just demonstrate what your mechanic should look like).
To my mind, I just don't get the actual practical application, unless you're tyring to do something like the following:
Take a COM class, which inherits from two other COM Interfaces:
And now I have a diamond inheritence pattern: IShellBrowser inherits ultimately from IUnknown, as does ICommDlgBrowser. But it seems incredibly silly to have to write my own IUnknown:AddRef and IUnknown::Release implementation, which is a highly standard implementation, because there's no way to cause the compiler to let another inherited class supply the missing virtual functions for IShellBrowser and/or ICommDlgBrowser.
i.e., I end up having to:
because there's no way I know of to "inherit" or "inject" those function implementations into MyShellBrowserDialog from anywhere else which actually fill-in the needed virtual member function for either IShellBrowser or ICommDlgBrowser.
I can, if the implementations were more complex, manually link up the vtable to an inherited implementor if I wished:
And if I needed the mix-in to actually refer to the most-derived class to interact with it, I could add a template parameter to IUnknownMixin, to give it access to myself.
But what common elements could my class have or benefit by that IUnknownMixin couldn't itself supply?
What common elements could any composite class have that various mixins would want to have access to, which they needed to derive from themselves? Just have the mixins take a type parameter and access that. If its instance data in the most derived, then you have something like:
Ultimately your question remains somewhat confusing to me. Perhaps you could create that example that shows your preferred-natural-syntax that accomplishes something clearly, as I just don't see that in your initial post, and I can't seem to sleuth it out from toying with these ideas myself.
我见过很多针对这个基本难题的解决方案:多态性+接口变化。
一个基本方法是提供一种查询扩展接口的方法 - 这样您就可以在 Windows 下进行 COM 编程:
另一种选择是创建一个更丰富的基接口类 - 它具有您需要的所有接口,然后简单地为基类中的那些定义一个默认的、无操作的实现,它返回 false 或抛出异常,以指示相关子类不支持它(否则子类将为该成员函数提供功能实现) 。
我希望这可以帮助您看到一些可能性。
Smalltalk 具有一个奇妙的属性,即能够询问元类型系统给定实例是否支持特定方法 - 并且它支持拥有一个类处理程序,该处理程序可以在给定实例被告知执行其不执行的操作时随时执行支持 - 以及该操作是什么,因此您可以将其作为代理转发,或者您可以抛出不同的错误,或者只是悄悄地忽略该操作作为无操作)。
Objective-C 支持所有与 Smalltalk 相同的模式!通过在运行时访问类型系统可以完成非常非常酷的事情。我认为 .NET 可以沿着这些思路提取一些疯狂而酷的东西(尽管从我所看到的来看,我怀疑它是否像 Smalltalk 或 Objective-C 一样优雅)。
无论如何,...祝你好运:)
I have seen lots of solutions to this basic conundrum: polymorphism + variation in interfaces.
One basic approach is to provide a way to query for extended interfaces - so you have something along the lines of COM programming under Windows:
Another option would be to make a single richer base interface class - which has all the interfaces you need, and then to simply define a default, no-op implementation for those in the base class, which returns false or throws to indicate that it isn't supported by the subclass in question (else the subclass would have provided a functional implementation for this member function).
I hope that this helps you to see some possibilities.
Smalltalk had the wonderful attribute of being able to ask the meta-type-system whether a given instance supported a particular method - and it supported having a class-handler that could execute anytime a given instance was told to perform an operation it didn't support - along with what operation that was, so you could forward it as a proxy, or you could throw a different error, or simply quietly ignore that operation as a no-op).
Objective-C supports all of those same modalities as Smalltalk! Very, very cool things can be accomplished by having access to the type-system at runtime. I assume that .NET can pull of some crazy cool stuff along those lines (though I doubt that its nearly as elegant as Smalltalk or Objective-C, from what I've seen).
Anyway, ... good luck :)