当虚拟继承是一个好的设计?

时间:2011-01-05 15:07:01

标签: c++ multiple-inheritance virtual-inheritance

EDIT3:请务必在回答之前清楚地了解我的要求(有EDIT2和很多评论)。有(或有)许多答案清楚地表明对问题的误解(我知道这也是我的错,对不起)

嗨,我在C ++中查看了有关虚拟继承(class B: public virtual A {...})的问题,但没有找到我的问题的答案。

我知道虚拟继承存在一些问题,但我想知道的是虚拟继承被视为良好的设计。

我看到人们提到IUnknownISerializable等接口,而且iostream设计基于虚拟继承。这些是很好地利用虚拟继承的好例子,那是因为没有更好的选择,或者因为虚拟继承 在这种情况下是正确的设计?感谢。

编辑:为了澄清,我问的是现实生活中的例子,请不要抽象。我知道虚拟继承是什么以及哪种继承模式需要它,我想知道的是何时它是做事的好方法,而不仅仅是复杂继承的结果。

EDIT2:换句话说,我想知道钻石层次结构(这是虚拟继承的原因)何时是一个好的设计

7 个答案:

答案 0 :(得分:24)

如果您有接口层次结构和相应的实现层次结构,则必须使接口基类成为虚拟基础。

E.g。

struct IBasicInterface
{
    virtual ~IBasicInterface() {}
    virtual void f() = 0;
};

struct IExtendedInterface : virtual IBasicInterface
{
    virtual ~IExtendedInterface() {}
    virtual void g() = 0;
};

// One possible implementation strategy
struct CBasicImpl : virtual IBasicInterface
{
    virtual ~CBasicImpl() {}
    virtual void f();
};

struct CExtendedImpl : virtual IExtendedInterface, CBasicImpl
{
    virtual ~CExtendedImpl() {}
    virtual void g();
};

通常,只有扩展基本接口的接口和不同情况下需要的多个实现策略才有意义。这样,您就拥有了清晰的接口层次结构,并且您的实现层次结构可以使用继承来避免常见实现的重复。如果你正在使用Visual Studio,你会收到很多警告C4250。

为防止意外切片,通常最好是CBasicImplCExtendedImpl类不可实例化,而是具有更高级别的继承,除了构造函数之外不提供额外功能。

答案 1 :(得分:4)

当A类扩展另一个B类时,虚拟继承是一个很好的设计选择,但B除了可能的析构函数之外没有虚拟成员函数。您可以将B这样的类视为mixins,其中类型层次结构只需要一个mixin类型的基类,以便从中受益。

一个很好的例子是与STL的libstdc ++实现中的一些iostream模板一起使用的虚拟继承。例如,libstdc ++使用:

声明模板basic_istream
template<typename _CharT, typename _Traits>
class basic_istream : virtual public basic_ios<_CharT, _Traits>

它使用虚拟继承来扩展basic_ios<_CharT, _Traits>因为istream应该只有一个输入streambuf,并且istream的许多操作应该总是具有相同的功能(特别是rdbuf成员函数来获取一个和只输入streambuf)。

现在假设您编写了一个类(baz_reader),它使用成员函数扩展std::istream以读入类型为baz的对象,以及另一个类(bat_reader)使用成员函数扩展std::istream以读入bat类型的对象。您可以拥有一个同时扩展baz_readerbat_reader的类。如果未使用虚拟继承,则baz_readerbat_reader基础将各自具有自己的输入streambuf - 可能不是意图。您可能希望baz_readerbat_reader基都读取相同的streambuf。如果没有std::istream中的虚拟继承来扩展std::basic_ios<char>,您可以通过将baz_readerbat_reader基础的成员readbuf设置为相同的streambuf对象来实现这一点,但是你会当一个人足够时,有两个指向streambuf的指针副本。

答案 2 :(得分:1)

Grrr ..虚拟继承必须用于抽象子类型。如果你要遵守面向对象的设计原则,绝对没有选择。如果不这样做,就会阻止其他程序员派生其他子类型。

首先是一个抽象示例:你有一些基本抽象A.你想要创建一个子类型B.请注意 subtype 必然意味着另一个抽象。如果它不是抽象的,那么它就是一种不是类型的实现。

现在另一个程序员出现了,想要制作A.酷的子类型C.

最后,又有一位程序员出现并想要一些既是B又是C的东西。当然,它也是A。在这些情况下,虚拟继承是必需的

这是一个真实世界的例子:来自编译器,建模数据类型:

struct function { ..
struct int_to_float_type : virtual function { ..

struct cloneable : virtual function { .. 

struct cloneable_int_to_float_type : 
  virtual function, 
  virtual int_to_float_type 
  virtual cloneable 
{ ..

struct function_f : cloneable_int_to_float_type { 

此处,function表示函数,int_to_float_type表示子类型 由从int到float的函数组成。 Cloneable是一个特殊财产 该函数可以克隆。 function_f是一个具体的(非抽象的) 功能

请注意,如果我最初没有将function作为int_to_float_type的虚拟基础,我就无法混合cloneable(反之亦然)。

通常,如果遵循“严格”OOP样式,则始终定义抽象点阵,然后为它们派生实现。您严格分开子类型,它只适用于抽象,实现

在Java中,这是强制执行的(接口不是类)。在C ++中它没有被强制执行,你不必遵循这种模式,但你应该意识到它,你正在使用的团队越大,或者你正在进行的项目,原因就越强你需要离开它。

Mixin打字需要在C ++中进行大量的内务处理。在Ocaml中,类和类类型是独立的并且由结构匹配(拥有或不拥有方法),因此继承总是方便的。这实际上比名义输入更容易使用。 Mixins提供了一种模拟结构类型的方法,该语言只有标称输入。

答案 3 :(得分:1)

虚拟继承不是好事或坏事 - 它是一个实现细节,就像任何其他细节一样,它存在于实现相同抽象发生的代码。当代码必须是超运行时,这通常是正确的事情,例如,在COM中,某些COM对象必须在进程之间共享,更不用说编译器等,需要使用IUnknown,正常的C ++库只需使用它shared_ptr。因此,在我看来,普通的C ++代码应该依赖于模板和类似代码,不应该需要虚拟继承,但在某些特殊情况下它是完全必要的。

答案 4 :(得分:1)

由于您要求提供具体示例,我建议您进行侵入式引用计数。在这种情况下,虚拟继承并不是好的设计,但虚拟继承是使工作正常工作的正确工具。

在90年代早期,我使用了一个类库,该类具有ReferenceCounted类,其他类可以从中获取引用计数和几种管理引用计数的方法。继承必须是虚拟的,否则如果你有多个基础,每个基础都是从ReferenceCounted非虚拟派生的,那么你最终会有多个引用计数。虚拟继承确保您的对象具有单个引用计数。

使用shared_ptr和其他人的非侵入式引用计数现在似乎更受欢迎,但是当类将this传递给其他方法时,侵入式引用计数仍然有用。在这种情况下,外部引用计数会丢失。我也喜欢这种侵入式引用计数说明了如何管理该类对象的生命周期。

[我认为我正在捍卫侵入式引用计数,因为我现在很少看到它,但我喜欢它。]

答案 5 :(得分:-1)

These FAQs回答了与虚拟继承相关的所有问题。甚至是你问题的答案(如果我正确地认出你的问题;)):FAQ item 25.5

答案 6 :(得分:-3)

强制使用多重继承时,

需要虚拟继承。有些问题无法通过避免多重继承而干净利落地解决。在那些情况下(很少见),您需要查看虚拟继承。 95%的情况下,您可以(并且应该)避免多重继承以保存自己(以及那些在您之后查看代码的人)很多麻烦。

作为旁注,COM不会强制您使用多重继承。创建一个从具有线性继承树的IUnknown(直接或间接)派生​​的COM对象是可能的(而且很常见)。