Search code examples
c++multiple-inheritancediamond-problem

Diamond inheritance with a third party library


I have a classic diamond problem like this in C++

  A
 / \
B   C
 \ /
  D

I know this would normally be solved by making B and C inherit virtually from A.

But my issue is that classes A and B come from a third party library I can't edit and B's inheritance from A is not marked virtual.

Is there a way to solve this?

Thanks for the help ;-)


Solution

  • Key design issue

    If you can't change the inheritance of A in your library to virtual, there is no way to make a diamond with a single A element a the top. The standard explicitly allows for mixing virtual and non-virtual inheritance of the same base class:

    10.1/6: for an object c of class type C, a single subobject of type V is shared by every base subobject of c that has a virtual base class of type V. (...).
    10.1/7: A class can have both virtual and non-virtual base classes of a given type.

    Example:

    namespace mylib {  // namesape just to higlight the boundaries of the library
        struct Person {                              // A
            static int counter; 
            int id; 
            Person() : id(++counter) {}
            void whoami() { cout << "I'm "<<id<<endl; }
        };  //A
        struct Friend: Person {};                    //B -> A
        int Person::counter=0; 
    }
    struct Employee : virtual mylib::Person {};      // C->A
    struct Colleague : Employee, mylib::Friend {};   // D->(B,c)
    ...
    mylib::Friend p1;   // ok !  
    p1.whoami(); 
    Employee p2;        // ok !
    p2.whoami(); 
    Colleague p3;       // Attention: No diamond ! 
    //p3.whoami();      // ouch !! not allowed: no diamond so for which base 
                        // object has the function to be called  ? 
    p3.Employee::whoami();       // first occurrence of A
    p3.mylib::Friend::whoami();  // second second occurrence of A
    

    Online demo

    Alternative design

    As you have no way to intervene in your external library you have to organize things differently. But however you'll do it, it will be sweat and tears.

    You could define C (Employee in my example) by using composition of A (Person in my example). The A subobject would either be created or in special cases taken over from another object. You'd need to undertake the effort to replicate A's interface, forwarding the calls to an A subobject.

    The general idea would look like:

    class Employee { 
        mylib::Person *a;
        bool owna;
    protected:
        Employee (mylib::Person& x) : a(&x), owna(false) { }  // use existing A
    public: 
        Employee () : a(new mylib::Person), owna(true) { }  // create A subobject   
        ~Employee () { if (owna) delete a; }
        void whoami() { a->whoami(); } // A - fowarding 
    };
    

    If you do this, you could then define D with multiple inheritance, with a trick in the constructor:

    struct Colleague : mylib::Friend, Employee { 
        Colleague () : mylib::Friend(), Employee(*static_cast<Person*>(this)) {}; 
        using Friend::whoami; 
    };
    

    The only issue would then the ambiguity of the member functions of A interface (that have been provided in C as explained above). You therefore have to tell with a using clause that for A, you go via B and not via C.

    In final, you could use this:

    Employee p2; 
    p2.whoami(); 
    
    Colleague p3;   // Artifical diamond ! 
    p3.whoami();  // YES !!  
    p3.Employee::whoami();   // first occurence of A
    p3.mylib::Friend::whoami(); // second second occurence of A
                                 // all whoami refer to the same A !!! 
    

    It works nicely: Online demo

    Conclusion

    So yes, it's possible to solve this, but it's very tricky. As I said: it will be sweat and tears.

    For instance, you have no problem to convert a Colleague to a Person. But for Employee, you'd need to provide conversion operators. You have to implement the rule of 3/5 in Employee And you have to take care of everything that could go wrong (failed allocation, etc...). It will not be a piece of cake.

    So it's really worth to reconsider your design, as Lightness Races in Orbit suggested in the comments :-)