1

I'm new to c++ and have been experimenting with virtual inheritance. But there is something that really confuses me.

#include <iostream>
using namespace std;

struct A {int m = 5005;};
struct B : A {};
struct C : virtual B {};
struct D : virtual B {int m = 6006;};
struct E : C, D {};

int main () {
   E e;
   e.m = 303;

   cout << "e.A::m = " << e.A::m << endl;
   cout << "e.D::m = " << e.D::m << endl;
   cout << "e.m = " << e.m << endl;
}

The output of this is:

e.A::m = 5005
e.D::m = 303
e.m = 303

Now, what confuses me is e.D::m = 303. Shouldn't it be 6006? I know there are already quite a few questions to virtual inheritance but no one really explained why this is happening. I also think I've found another program that shows the same "problem".

#include <iostream>
using namespace std;

struct S {int m = 101;};
struct A : virtual S {int m = 202;};
struct B : virtual S {int m = 303;};
struct C : virtual A, virtual B {int m = 404;};
struct D : C {};
struct E : virtual A, virtual B, D {};

int main () {
   E e;
   e.m = 10;

   cout << "e.S::m = " << e.S::m << endl;
   cout << "e.A::m = " << e.A::m << endl;
   cout << "e.B::m = " << e.B::m << endl;
   cout << "e.C::m = " << e.C::m << endl;
   cout << "e.m = " << e.m << endl;
}

Where the output is

e.S::m = 101
e.A::m = 202
e.B::m = 303
e.C::m = 10
e.m = 10

And also here e.C::m=10 confuses me. Can someone please explain what's going on here? I acually thought I understood the principle of virtual inheritance.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
NewOasis
  • 63
  • 1
  • 7
  • 1
    You have to pour over the [name lookup logic](https://timsong-cpp.github.io/cppwp/n3337/class.member.lookup) to figure out why that's happening. – R Sahu Mar 26 '18 at 15:15
  • Or to get a quick intuition, print (or inspect in your debugger) `&e.m`, `&e.A::m`, `&e.D::m`, etc. – Useless Mar 26 '18 at 16:25

3 Answers3

2

I think the paragraph here explains this behavior:

Either way, when examining the bases from which the class is derived, the following rules, sometime referred to as dominance in virtual inheritance, are followed:

A lookup set is constructed, which consists of the declarations and the subobjects in which these declarations were found. Using-declarations are replaced by the members they represent and type declarations, including injected-class-names are replaced by the types they represent. If C is the class in whose scope the name was used, C is examined first. If the list of declarations in C is empty, lookup set is built for each of its direct bases Bi (recursively applying these rules if Bi has its own bases). Once built, the lookup sets for the direct bases are merged into the lookup set in C as follows

  • if the set of declarations in Bi is empty, it is discarded
  • if the lookup set of C built so far is empty, it is replaced by the lookup set of Bi
  • if every subobject in the lookup set of Bi is a base of at least one of the subobjects already added to the lookup set of C, the lookup set of Bi is discarded.
  • if every subobject already added to the lookup set of C is a base of at least one subobject in the lookup set of Bi, then the lookup set of C is discarded and replaced with the lookup set of Bi
  • otherwise, if the declaration sets in Bi and in C are different, the result is an ambiguous merge: the new lookup set of C has an invalid declaration and a union of the subobjects ealier merged into C and introduced from Bi. This invalid lookup set may not be an error if it is discarded later.
  • otherwise, the new lookup set of C has the shared declaration sets and the union of the subobjects ealier merged into C and introduced from Bi

The example helps illustrate the logic here:

struct X { void f(); };
struct B1: virtual X { void f(); };
struct B2: virtual X {};
struct D : B1, B2 {
    void foo() {
        X::f(); // OK, calls X::f (qualified lookup)
        f(); // OK, calls B1::f (unqualified lookup)
// C++11 rules: lookup set for f in D finds nothing, proceeds to bases
//  lookup set for f in B1 finds B1::f, and is completed
// merge replaces the empty set, now lookup set for f in C has B1::f in B1
//  lookup set for f in B2 finds nothing, proceeds to bases
//    lookup for f in X finds X::f
//  merge replaces the empty set, now lookup set for f in B2 has X::f in X
// merge into C finds that every subobject (X) in the lookup set in B2 is a base
// of every subobject (B1) already merged, so the B2 set is discareded
// C is left with just B1::f found in B1
// (if struct D : B2, B1 was used, then the last merge would *replace* C's 
//  so far merged X::f in X because every subobject already added to C (that is X)
//  would be a base of at least one subobject in the new set (B1), the end
//  result would be the same: lookup set in C holds just B1::f found in B1)
    }
};

TL;DR: Because e.m = 303; is an unqualified lookup, the compiler will recursively look up the inheritance tree for matching declarations. In this case I think it would first find A::m, but would replace this with D::m after seeing that D has A as an indirect base class. So e.m ends up resolving to e.D::m.

0x5453
  • 12,753
  • 1
  • 32
  • 61
0

I am answering you to your first question. You have just over write the value of m in your class D.

 E e;
 e.m = 303;//you just over write the value 6006, just comment out this line and check

  cout << "e.A::m = " << e.A::m << endl;
  cout << "e.D::m = " << e.D::m << endl;
  cout << "e.m = " << e.m << endl;

Here B inherited A and B has been inherited to C and D virtually so only one copy of B will be available in E finally. So you can access data member m of class A through A::m and your e.m and e.D::m is accessing the same data member i.e. m of class D.

Now see below some interesting results:-

    //e.m = 303;

    cout << "e.A::m = " << e.A::m << endl;
    cout << "e.D::m = " << e.D::m << endl;
    cout << "e.m = " << e.m << endl;

    e.m = 303;//over write D::m;

    cout << "e.A::m = " << e.A::m << endl;
    cout << "e.D::m = " << e.D::m << endl;
    cout << "e.m = " << e.m << endl;

    e.E::m = 101;//over write D::m

    cout << "e.A::m = " << e.A::m << endl;
    cout << "e.D::m = " << e.D::m << endl;
    cout << "e.m = " << e.m << endl;

    e.B::m = 202;//over write A::m through B

    cout << "e.A::m = " << e.A::m << endl;
    cout << "e.D::m = " << e.D::m << endl;
    cout << "e.m = " << e.m << endl;
Abhijit Pritam Dutta
  • 5,521
  • 2
  • 11
  • 17
0

The virtual classes 'bubble up' to the top of the hierarchy, but they are considered after the non-virtual classes during lookup.


Example 1

struct E : C, D {}; becomes (virtual classes in round brackets):

E
  C
  D
 (B) - bubbled up to E from other classes.

Next we look at what is in C and D:

E
  C      <-- continue lookup
   (B)   <-- will not continue to look for m here yet
  D
   (B)   <-- will not continue to look for m here yet either
    m    <-- m found during unqualified lookup
 (B)     <-- may continue to look for m here, but already found it above

Example 2

struct E : virtual A, virtual B, D {}; becomes:

E
  D  - first non-virtual class at top level
 (A)
 (B)
 (S) - bubbled up from other classes.

Next we look at what is in D:

E
  D      <-- continue lookup
    C    <-- continue lookup
      m  <-- m found during unqualified lookup
 (A)     <-- no further lookup, m already found
 (B)     <-- no further lookup, m already found
 (S)     <-- no further lookup, m already found
wally
  • 10,717
  • 5
  • 39
  • 72