虚拟继承如何解决“钻石”(多重继承)的歧义?


95
class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

我了解钻石的问题,并且上面的代码没有这个问题。

虚拟继承如何完全解决问题?

我的理解: 当我说时A *a = new D();,编译器想知道是否D可以将类型的对象分配给类型的指针A,但是它有两条路径可以遵循,但不能自行决定。

那么,虚拟继承如何解决问题(帮助编译器做出决定)?

Answers:


109

您需要:(可通过虚拟继承实现)

  A  
 / \  
B   C  
 \ /  
  D 

而不是:(没有虚拟继承会发生什么)

A   A  
|   |
B   C  
 \ /  
  D 

虚拟继承意味着将只有1个基A类实例,而不是2个实例。

您的类型D将具有2个vtable指针(您可以在第一个图中看到它们),一个用于虚拟继承B,一个用于C虚拟继承AD的对象大小增加了,因为它现在存储了2个指针;但是现在只有一个A

因此B::AC::A相同,因此不会有来自的歧义调用D。如果您不使用虚拟继承,则上面有第二张图。然后,对A成员的任何调用都变得模棱两可,您需要指定要采用的路径。

维基百科还有另一个很好的总结和例子


2
Vtable指针是一个实现细节。在这种情况下,并非所有编译器都会引入vtable指针。
curiousguy

19
我认为将图表垂直镜像会更好。在大多数情况下,我发现了这样的继承图,这些继承图显示了基础之下的派生类。(请参见“
下调

如何修改他的代码以改用BC的实现?谢谢!
MinhNghĩa19年

44

派生类的实例“包含”基类的实例,因此它们在内存中的外观如下:

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]

因此,如果没有虚拟继承,则类D的实例将如下所示:

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'

因此,请注意A数据的两个“副本”。虚拟继承意味着在派生类内部,在运行时设置了一个vtable指针,该指针指向基类的数据,因此B,C和D类的实例如下所示:

class B: [A fields | B fields]
          ^---------- pointer to A

class C: [A fields | C fields]
          ^---------- pointer to A

class D: [A fields | B fields | C fields | D fields]
          ^---------- pointer to B::A
          ^--------------------- pointer to C::A


43

为什么还要另一个答案?

好吧,关于SO和其他文章的许多帖子都说,钻石问题是通过创建单个实例A而不是两个实例(每个的父对象一个D)解决的,从而解决了歧义。但是,这并没有使我对过程有全面的了解,最终我遇到了更多的问题,例如

  1. 如果BC尝试创建不同的实例,A例如使用不同的参数(D::D(int x, int y): C(x), B(y) {})调用参数化构造函数,该怎么办?A将选择的哪个实例成为其一部分D
  2. 如果我将非虚拟继承用于B,而将虚拟继承用于C呢?创建Ain的单个实例是否足够D
  3. 从现在起,我是否应该始终默认使用虚拟继承作为预防措施,因为它以较小的性能成本解决了钻石问题,并且没有其他缺点?

如果不尝试代码示例就无法预测行为,这意味着无法理解该概念。以下是帮助我解决虚拟继承问题的原因。

双A

首先,让我们从没有虚拟继承的代码开始:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

让我们通过输出。执行预期的B b(2);创建A(2),与相同C c(3);

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3);同时需要BC,他们每个人的创造自己的A,所以我们有双重Ad

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

这就是d.getX()导致编译错误的原因,因为编译器无法选择A应为其调用方法的实例。仍然可以直接为选定的父类调用方法:

d.B::getX() = 3
d.C::getX() = 2

虚拟性

现在让我们添加虚拟继承。使用相同的代码示例进行以下更改:

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

让我们跳到创建d

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

可以看到,A与默认构造函数忽略从构造函数传递参数创建BC。随着歧义消失,所有调用getX()返回相同的值:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

但是,如果我们要为参数调用构造函数该A怎么办?可以通过从的构造函数中显式调用它来完成D

D(int x, int y, int z): A(x), C(y), B(z)

通常,类只能显式地使用直接父级的构造函数,但是虚拟继承的情况除外。发现这个规则对我来说是“单击”的,并帮助您大量了解虚拟接口:

代码class B: virtual A意味着,任何从其继承的类B现在都必须A自行创建,因为B这不会自动进行。

考虑到这一点,很容易回答我遇到的所有问题:

  1. D创作既不B也不C负责的参数A,它完全取决于D只。
  2. C将委托Ato的创建D,但B将创建自己的实例,A从而将钻石问题带回
  3. 在孙子类而不是直子中定义基类参数不是一个好习惯,因此,在存在钻石问题且这种措施不可避免时,应该允许这样做。

10

问题不在于编译器必须遵循的路径。问题在于该路径的终点:强制转换的结果。当涉及类型转换时,路径无关紧要,只有最终结果才重要。

如果使用普通继承,则每个路径都有其自己独特的端点,这意味着强制转换的结果是模棱两可的,这就是问题所在。

如果使用虚拟继承,则会得到菱形的层次结构:两条路径均通向相同的端点。在这种情况下,选择路径的问题不再存在(或更确切地说,不再重要),因为两条路径都导致相同的结果。结果不再是模棱两可-而是重要的。确切的路径没有。


@Andrey:编译器如何实现继承...我的意思是我得到了您的论点,也要感谢您如此清晰地解释..但是,如果您可以解释(或指向参考),那将真的有帮助编译器实际上是如何实现继承的,以及我进行虚拟继承时会发生什么变化
Bruce 2010年

8

实际上,示例应如下所示:

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

...这样输出将是正确的:“ EAT => D”

虚拟继承只能解决祖父的重复!但是您仍然需要将方法指定为虚拟方法,以便正确覆盖方法...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.