Search code examples
c++inheritancevirtual-inheritancediamond-problem

Eliminating C++ diamond inheritance by passing a pointer to "this" to base constructor


I understand how C++ solves the diamond problem in multiple inheritance by using virtual inheritance. Suppose the following situation:

class A {
  int num;
public:
  int get_num() const { return num; }
};

class B : public A {
  void foob() { int x = get_num(); }
};

class C : public A {
  void fooc() { int x = get_num(); }
};

class D : public B, public C {
  void food() { int x = get_num(); }
};

The get_num() call is ambiguous inside food(). I know I can fix it either by calling A::get_num() or by virtual inheritance using virtual public A. But I can see a third approach:

class A {
  int num;
public:
  int get_num() const { return num; }
};

class B : public A {
  void foob() { int x = get_num(); }
};

class C { // won't inherit from A anymore
  const A& base; // instead keeps a reference to A
  void fooc() { int x = base.get_num(); }
public:
  explicit C(const A* b) : base(*b) { } // receive reference to A
};

class D : public B, public C {
  void food() { int x = get_num(); }
public:
  D() : C(this) { } // pass "this" pointer
};

The external code doesn't need to consider C as an A.

Considering it has no impacts on my particular class hierarchy design, are there any advantages of the third approach over the virtual inheritance way? Or, in terms of cost, it ends up being the same thing?


Solution

  • Congratulations ! You've just re-invented the principle of composition over inheritance !

    If this works with your design, it means that C was in fact not a kind of A, and there was no real justification to use inheritance in first place.

    But don't forget the rule of 5 ! While your approach should work in principle, you have a nasty bug here : with your current code, if you copy a D object, its clone uses the wrong reference to the base (it doesn't refer to it's own base, which can lead to very nasty bugs...

    Demo of the hidden problem

    Let's make A::get_num() a little bit more wordy, so that it tells us about the address of the object that invokes it:

    int get_num() const { 
        cout << "get_num for " << (void*)this <<endl; 
        return num; 
    }
    

    Let's add a member function to C, for the purpose of the demo:

    void show_oops() { fooc(); }
    

    And same for D:

    void show() { food(); }
    

    Now we can experiment the problem by running this small snippet:

    int main() {
        D d;
        cout<<"d is  "<<(void*)&d<<endl; 
        d.show();
        d.show_oops();
        D d2=d;
        cout<<"d2 is  "<<(void*)&d2<<endl; 
        d2.show();
        d2.show_oops();
    }
    

    Here an online demo. You will notice that d2 does produce inconsistent results, like here:

    d is  0x7fffe0fd11a0
    get_num for 0x7fffe0fd11a0
    get_num for 0x7fffe0fd11a0
    d2 is  0x7fffe0fd11b0
    get_num for 0x7fffe0fd11b0
    get_num for 0x7fffe0fd11a0        <<< OUCH !! refers to the A element in d !!
    

    Not only do you refer to the wrong object, but if the d object would decease, you would have a dangling reference, so UB.