为什么需要在菱形层次结构的中间指定虚拟继承?

时间:2011-03-02 10:39:35

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

我有类的钻石层次结构:

    A
  /   \
 B     C
  \   /
    D

为避免D中有两个A副本,我们需要在B和C使用虚拟继承。

class A                     {  }; 
class B: virtual public A {};
class C: virtual public A   { }; 
class D: public B, public C { }; 

问题:为什么需要在B和C上执行虚拟继承,即使模糊度在D?如果是D,那就更直观了。

为什么标准委员会将此功能设计为这样? 如果B和C类来自第三方库,我们该怎么办?

编辑:我的回答是指出B和C类在创建派生对象时不应该调用A的构造函数,因为它将被D调用。

5 个答案:

答案 0 :(得分:8)

我不确定他们选择以这种方式设计虚拟继承的确切原因,但我相信其原因与对象布局有关。

假设C ++的设计方式是解决钻石问题,你实际上会在D中继承B和C,而不是在B和C中实际继承A.现在,B和C的对象布局是什么?好吧,如果没有人试图从它们中虚拟继承,那么它们每个都有自己的A副本,并且可以使用标准的优化布局,其中B和C各自在它们的基础上具有A.但是,如果某人 实际上从B或C继承,则对象布局必须不同,因为两者必须共享其副本A.

这个问题是当编译器第一次看到B和C时,它无法知道是否有人将继承它们。因此,编译器必须依赖于虚拟继承中使用的较慢版本的继承,而不是默认情况下启用的更优化的继承版本。这违反了C ++原则“不支付你不用的东西”(零开销原则),你只支付你明确使用的语言功能。

答案 1 :(得分:8)

  

为什么需要在B和C上执行虚拟继承,即使模糊度在D?如果是D,那就更直观了。

在您的示例中,B和C专门使用virtual要求编译器确保只涉及A的一个副本。如果他们没有这样做,他们就会有效地说“我需要自己的A基类,我不希望与任何其他派生对象共享它”。这可能至关重要。

不想共享虚拟基类的示例

如果A是某种容器,B就是从它衍生而来并存储了一些特定类型的物体 - 比如“Bat”,而C则存储了“Cat”。如果D期望B和C独立提供关于蝙蝠和猫的数量的信息,如果C操作对蝙蝠做了什么,或者B操作对Cats做了什么,那么他们会非常惊讶。

想要共享虚拟基类的示例

假设D需要提供对A中某些函数或数据成员的访问,比如“A :: x”......如果A由B和C独立地(非虚拟地)继承,则编译器可以' t将D :: x解析为B :: x或C :: x,而程序员不必明确消除它的歧义。这意味着D不能用作A,尽管衍生链中隐含的只有两个“is-a”关系(即如果B“是”A,而D“是”B,则用户可以期望/需要使用D,好像D“是”A“。

  

为什么标准委员会将此功能设计为这样?

存在

virtual继承,因为它有时很有用。它由B和C指定,而不是D,因为它在B和C的设计方面是一个侵入性的概念,并且对B和C的封装,内存布局,构造和破坏以及函数分派有影响。

  

如果B和C类来自第三方库,我们该怎么办?

如果D需要从两者继承并提供对A的访问,但B和C不是设计为使用虚拟继承而且无法更改,则D必须负责将与A API匹配的任何请求转发给B和/或C和/或可选的另一个A直接继承(如果它需要可见的“是A”关系)。如果调用代码知道它正在处理D(即使是通过模板化),这可能是实用的,但是通过指向基类的指针对对象的操作将不知道D正在尝试执行的管理,并且整个事情可能是做对了很棘手。但这有点像说“如果我需要一个矢量而我只有一个列表”,“锯子而不是螺丝刀”......好吧,充分利用它或得到你真正需要的东西。

  

编辑:我的回答是指出B和C类在创建派生对象时不应该调用A的构造函数,因为它将被D调用。

这是一个重要的方面,是的。

答案 2 :(得分:2)

除了templatetypedef回答之外,还可以指出你也可以将A包装成

class AVirt:virtual public A{}; 

并从中继承其他类。在这种情况下,您不需要将其他继承标记为虚拟

答案 3 :(得分:1)

  

问题:为什么需要在B和C上执行虚拟继承,即使歧义在D?

因为B和C的方法必须知道它们可能必须处理布局与B和C自己的布局有很大不同的对象。对于单继承,这不是问题,因为派生类只是在父项的原始布局之后追加它们的属性。

使用多重继承,你不能这样做,因为首先没有单个父级的布局。此外(如果你想避免A的重复)父母的布局需要在A的属性上重叠。 C ++中的多重继承隐藏了很多复杂性。

答案 4 :(得分:0)

由于A是乘法继承的类,因此直接从它派生的类必须虚拟化。

如果您的情况是B和C都来自A并且您希望两者都在D中并且您不能使用钻石,则D可以仅从B和C中的一个派生,并且具有另一个的实例通过它可以转发功能。

解决方法是这样的:

class B : public A; // not your class, cannot change
class C : public A; // not your class, cannot change

class D : public B; // your class, implement the functions of B
class D2 : public C; // your class implement the functions of C

class D
{
   D2 d2;
};