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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2659116/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 00:15:32  来源:igfitidea点击:

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

c++inheritancemultiple-inheritancevirtual-inheritancediamond-problem

提问by Moeb

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 Dcan be assigned to a pointer of type A, but it has two paths that it can follow, but cannot decide by itself.

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

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

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

回答by Brian R. Bondy

You want:(Achievable with virtual inheritance)

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

  A  
 / \  
B   C  
 \ /  
  D 

And not:(What happens without virtual inheritance)

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

A   A  
|   |
B   C  
 \ /  
  D 

Virtual inheritance means that there will be only 1 instance of the base Aclass not 2.

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

Your type Dwould have 2 vtable pointers (you can see them in the first diagram), one for Band one for Cwho virtually inherit A. D's object size is increased because it stores 2 pointers now; however there is only one Anow.

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

So B::Aand C::Aare the same and so there can be no ambiguous calls from D. If you don't use virtual inheritance you have the second diagram above. And any call to a member of A then becomes ambiguous and you need to specify which path you want to take.

所以B::AC::A是一样的,所以不会有来自D. 如果你不使用虚拟继承,你有上面的第二个图。对 A 成员的任何调用都会变得不明确,您需要指定要采用的路径。

Wikipedia has another good rundown and example here

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

回答by el.pescado

Instances of derived classes "contain" instances of base classes, so they look in memory like that:

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

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

Thus, without virtual inheritance, instance of class D would look like:

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

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

So, note two "copies" of A data. Virtual inheritance means that inside derived class there is a vtable pointer set at runtime that points to data of base class, so that instances of B, C and D classes look like:

因此,请注意 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

回答by nnovich-OK

Why another answer?

为什么是另一个答案?

Well, many posts on SO and articles outside say, that diamond problem is solved by creating single instance of Ainstead of two (one for each parent of D), thus resolving ambiguity. However, this didn't give me comprehensive understanding of process, I ended up with even more questions like

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

  1. what if Band Ctries to create different instances of Ae.g. calling parametrized constructor with different parameters (D::D(int x, int y): C(x), B(y) {})? Which instance of Awill be chosen to become part of D?
  2. what if I use non-virtual inheritance for B, but virtual one for C? Is it enough for creating single instance of Ain D?
  3. should I always use virtual inheritance by default from now on as preventive measure since it solves possible diamond problem with minor performance cost and no other drawbacks?
  1. 如果BC尝试创建不同的实例,A例如使用不同的参数 ( D::D(int x, int y): C(x), B(y) {})调用参数化构造函数会怎样?A将选择哪个实例成为 的一部分D
  2. 如果我对 使用非虚拟继承B,而对 使用虚拟继承C呢?创建Ain 的单个实例是否足够D
  3. 从现在开始,我是否应该始终默认使用虚拟继承作为预防措施,因为它以较小的性能成本解决了可能的钻石问题并且没有其他缺点?

Not being able to predict behavior without trying code samples means not understanding the concept. Below is what helped me to wrap head around virtual inheritance.

不尝试代码示例就无法预测行为意味着不理解这个概念。以下是帮助我了解虚拟继承的内容。

Double A

双A

First, lets start with this code without virtual inheritance:

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

#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;
}

Lets go through output. Executing B b(2);creates A(2)as expected, same for C c(3);:

让我们通过输出。按预期执行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);needs both Band C, each of them creating its own A, so we have double Ain d:

D d(2, 3);两者都需要Band C,它们每个都创建自己的A,所以我们有 double Ain d

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

That's the reason for d.getX()to cause compilation error as compiler can't choose which Ainstance it should call method for. Still it's possible to call methods directly for chosen parent class:

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

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

Virtuality

虚拟性

Now lets add virtual inheritance. Using same code sample with the following changes:

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

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
...

Lets jump to creation of d:

让我们跳到创建d

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

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

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

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

But what if we want to call parametrized constructor for A? It can be done by explicitly calling it from constructor of D:

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

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

Normally, class can explicitly use constructors of direct parents only, but there is an exclusion for virtual inheritance case. Discovering this rule "clicked" for me and helped understanding virtual interfaces a lot:

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

Code class B: virtual Ameans, that any class inherited from Bis now responsible for creating Aby itself, since Bisn't going to do it automatically.

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

With this statement in mind it's easy to answer all questions I had:

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

  1. During Dcreation neither Bnor Cis responsible for parameters of A, it's totally up to Donly.
  2. Cwill delegate creation of Ato D, but Bwill create its own instance of Athus bringing diamond problem back
  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.
  1. D创建过程中既不负责B也不C负责 的参数A,完全取决于D
  2. C将委托Ato 的创建D,但B会创建自己的实例,A从而将钻石问题带回来
  3. 在孙子类而不是直接子类中定义基类参数不是一个好的做法,因此当存在菱形问题时应该容忍并且这种措施是不可避免的。

回答by AnT

The problem is not the paththe compiler must follow. The problem is the endpointof that path: the result of the cast. When it comes to type conversions, the path does not matter, only the final result does.

问题不在于编译器必须遵循的路径。问题是该路径的终点:演员表的结果。说到类型转换,路径无关紧要,只有最终结果才重要。

If you use ordinary inheritance, each path has its own distinctive endpoint, meaning that the result of the cast is ambiguous, which is the problem.

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

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.

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

回答by enger

Actually the example should be as follows:

其实例子应该是这样的:

#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;
}

... that way the output is gonna be the correct one: "EAT=>D"

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

Virtual inheritance only solves the duplication of the grandfather! BUT you still need to specify the methods to be virtual in order to get the methods correctly overrided...

虚拟继承只解决爷爷的重复!但是您仍然需要将方法指定为虚拟方法才能正确覆盖这些方法......