How does virtual inheritance solve the "diamond" (multiple inheritance) ambiguity?

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();
}

I understand the diamond problem, and above piece of code does not have that problem.

How exactly does virtual inheritance solve the problem?

What I understand: When I say A *a = new D();, the compiler wants to know if an object of type D can be assigned to a pointer of type A, but it has two paths that it can follow, but cannot decide by itself.

So, how does virtual inheritance resolve the issue (help compiler take the decision)?

94161 次浏览

你想要: (虚继承可实现)

  A
/ \
B   C
\ /
D

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

A   A
|   |
B   C
\ /
D

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

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

因此 B::AC::A是相同的,所以不会有来自 D的模棱两可的调用。如果你不使用虚继承,你可以看看上面的第二张图。对 A 成员的任何调用都会变得模糊不清,您需要指定要采用的路径。

维基百科在这里有另一个很好的纲要和例子

派生类的实例存储其基类的 members

如果没有虚继承, 内存布局看起来就像(请注意类 DA成员的 副本) :

class A: [A members]
class B: public A [A members|B members]
class C: public A [A members|C members]
class D: public B, public C [A members|B members|A members|C members|D members]

使用虚继承, 内存布局看起来像(请注意类 DA成员的 单身副本) :

class A: [A members]
class B: virtual public A [B members|A members]
|         ^
v         |
virtual table B


class C: virtual public A [C members|A members]
|         ^
v         |
virtual table C


class D: public B, public C [B members|C members|D members|A members]
|         |                   ^
v         v                   |
virtual table D ----------------|

对于每个派生类,编译器创建一个虚拟表,其中包含指向存储在派生类中的虚拟基类成员的指针,并在派生类中添加指向该虚拟表的指针。

问题不在于编译器必须遵循的 path。问题在于路径的 终点: 强制转换的结果。当涉及到类型转换时,路径并不重要,重要的只是最终结果。

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

If you use virtual inheritance, you get a diamond-shaped hierarchy: both paths leads to the same endpoint. In this case the problem of choosing the path no longer exists (or, more precisely, no longer matters), because both paths lead to the same result. The result is no longer ambiguous - that is what matters. The exact path doesn't.

事实上,这个例子应该如下:

#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”

Virtual inheritance only solves the duplication of the grandfather! 但是您仍然需要指定虚方法,以便正确地重写方法..。

为什么还有另一个答案?

嗯,很多关于 SO 的帖子和外面的文章都说,菱形问题是通过创建单个 A实例而不是两个(D的每个父实例一个)来解决的,从而解决了歧义。然而,这并没有给我全面的了解过程,我最终还是提出了更多的问题之类的

  1. 如果 BC尝试创建 A的不同实例,例如使用不同的参数(D::D(int x, int y): C(x), B(y) {})调用参数化构造函数,该怎么办?哪个 A实例将被选择成为 D的一部分?
  2. 如果我对 B使用非虚拟继承,而对 C使用虚拟继承会怎样?在 D中创建单个 A实例是否足够?
  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,所以我们在 d中有双 A:

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

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

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)

You can see, A is created with default constructor ignoring parameters passed from constructors of B and C. As ambiguity is gone, all calls to getX() return the same value:

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 只能显式地使用直接父类的构造函数,但是对于虚继承的情况有一个排除。发现这个规则对我来说“很有用”,对理解虚拟接口有很大帮助:

代码 class B: virtual A意味着,从 B继承的任何类现在都负责自己创建 A,因为 B不会自动这样做。

有了这句话,我很容易回答所有的问题:

  1. D的创建过程中,BC都不负责 A的参数,它完全只对 D负责。
  2. CA的创建委托给 D,但 B将创建自己的 A实例,从而将钻石问题带回来
  3. Defining base class parameters in grandchild class rather than direct child isn't a good practice, so it should be tolerated when diamond problem exists and this measure is unavoidable.