开发者

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 com开发者_JS百科piler take the decision)?


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 A class not 2.

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

So B::A and C::A are 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.

Wikipedia has another good rundown and example here


Why another answer?

Well, many posts on SO and articles outside say, that diamond problem is solved by creating single instance of A instead 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

  1. what if B and C tries to create different instances of A e.g. calling parametrized constructor with different parameters (D::D(int x, int y): C(x), B(y) {})? Which instance of A will 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 A in 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?

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

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);:

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

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

D d(2, 3); needs both B and C, each of them creating its own A, so we have double A in 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 A instance it should call method for. Still it's possible to call methods directly for chosen parent class:

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:

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

But what if we want to call parametrized constructor for A? It can be done by explicitly calling it from constructor of 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 A means, that any class inherited from B is now responsible for creating A by itself, since B isn't going to do it automatically.

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

  1. During D creation neither B nor C is responsible for parameters of A, it's totally up to D only.
  2. C will delegate creation of A to D, but B will create its own instance of A thus 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.


Instances of derived classes store the members of their base classes.

Without virtual inheritance, the memory layouts look like (note the two copies of the A members in class D):

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]

With virtual inheritance, the memory layouts look like (note the single copy of the A members in class D):

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

For each derived class, the compiler creates a virtual table holding pointers to the members of its virtual base classes stored in the derived class, and adds a pointer to that virtual table in the derived class.


The problem is not the path the compiler must follow. The problem is the endpoint of 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.


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"

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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜