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

Moe*_*oeb 85 c++ inheritance multiple-inheritance virtual-inheritance diamond-problem

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(); 
} 
Run Code Online (Sandbox Code Playgroud)

我理解钻石问题,上面的代码没有那个问题.

虚拟继承究竟是如何解决问题的?

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

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

Bri*_*ndy 98

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

  A  
 / \  
B   C  
 \ /  
  D 
Run Code Online (Sandbox Code Playgroud)

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

A   A  
|   |
B   C  
 \ /  
  D 
Run Code Online (Sandbox Code Playgroud)

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

你的类型D将有2个虚表指针(你可以看到他们的第一个图表),一个用于B和一个C谁几乎继承A. D对象大小增加,因为它现在存储2个指针; 但现在只有一个A.

所以B::A并且C::A是相同的,因此不会有任何模糊的调用D.如果您不使用虚拟继承,则可以使用上面的第二个图表.然后,对A成员的任何调用都会变得模棱两可,您需要指定要采用的路径.

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

  • 我认为如果图形垂直镜像会更好看.在大多数情况下,我发现了这样的继承图,以显示基础下的派生类.(见"downcast","upcast") (19认同)
  • Vtable 指针是一个实现细节。在这种情况下,并非所有编译器都会引入 vtable 指针。 (2认同)

el.*_*ado 43

派生类的实例"包含"基类的实例,因此它们在内存中看起来像这样:

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]
Run Code Online (Sandbox Code Playgroud)

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

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'
Run Code Online (Sandbox Code Playgroud)

因此,请注意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
Run Code Online (Sandbox Code Playgroud)


nno*_*-OK 27

为什么另一个答案

好吧,SO上的很多帖子和外面的文章都说,钻石问题是通过创建单个实例A而不是两个(每个父代一个D)来解决,从而解决了模糊性问题.但是,这并没有让我全面了解过程,我最终得到了更多的问题

  1. 如果BC尝试创建不同的实例,A例如调用带有不同参数的参数化构造函数(D::D(int x, int y): C(x), B(y) {})?A将选择哪个实例成为其中的一部分D
  2. 如果我使用非虚拟继承B,但虚拟继承用于C什么?它是足以建立的单个实例AD
  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;
}
Run Code Online (Sandbox Code Playgroud)

让我们通过输出.执行按预期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) 
Run Code Online (Sandbox Code Playgroud)

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) 
Run Code Online (Sandbox Code Playgroud)

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

d.B::getX() = 3
d.C::getX() = 2
Run Code Online (Sandbox Code Playgroud)

虚拟性

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

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
...
Run Code Online (Sandbox Code Playgroud)

让我们跳转到创建d:

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 
Run Code Online (Sandbox Code Playgroud)

你可以看到,A是用默认构造函数创建的,忽略从B和的构造函数传递的参数C.由于歧义消失,所有调用都getX()返回相同的值:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Run Code Online (Sandbox Code Playgroud)

但是如果我们想调用参数化构造函数A呢?它可以通过从以下构造函数显式调用它来完成D:

D(int x, int y, int z): A(x), C(y), B(z)
Run Code Online (Sandbox Code Playgroud)

通常,类只能显式使用直接父项的构造函数,但是对于虚拟继承情况有一个排除.发现此规则为我"点击"并帮助理解虚拟接口:

代码class B: virtual A意味着,从中继承的任何类B现在都负责A自己创建,因为B它不会自动执行.

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

  1. D创作期间既不参与B也不C负责参数A,这完全取决于D.
  2. C将委托创作AD,但B会创建自己的实例A从而使钻石的问题回
  3. 在孙子类而不是直接子项中定义基类参数不是一个好习惯,因此当存在钻石问题并且这种测量是不可避免的时候应该容忍它.

  • 这个答案内容非常丰富!尤其是您对“virtual”关键字的解释为“稍后定义(在子类中)”,也就是说不是“真正”定义,而是“虚拟”定义。这种解释不仅适用于基类,也适用于方法。谢谢你! (4认同)

AnT*_*AnT 9

问题不在于编译器必须遵循的路径.问题是该路径的终点:强制转换的结果.在类型转换方面,路径无关紧要,只有最终结果才有意义.

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

如果使用虚拟继承,则会获得菱形层次结构:两个路径都指向同一个端点.在这种情况下,选择路径的问题不再存在(或者更确切地说,不再重要),因为两条路径都会产生相同的结果.结果不再含糊 - 这才是最重要的.确切的路径没有.


小智 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;
}
Run Code Online (Sandbox Code Playgroud)

......那样输出就是正确的:"EAT => D"

虚拟继承只解决了祖父的重复问题!但是你仍然需要指定虚拟方法才能正确地覆盖方法......