Wednesday, 9 April 2014

Multiple Inheritance - why and its problems

Multiple inheritance is one of distinguishing features of C++ comparing with other competing language, such as Java and C#. People's opinion are divided on this feature. One group absolutely love it and another group think it should be banned. Herb Sutter has a very good argument about MI in his book [1]. In my personal opinion/experience I love it and would not hesitate to use it when it is an legitimate case. Even some developers with C++ background still miss the feature of multiple inheritance when they decide to take Java and C#. (Some techniques are developed to emulate multiple inheritance in Java and C#, for instance using strategy pattern. Read more about how to use strategy pattern to emulate multiple inheritance in Java or C# on this blog entry, http://cpluspluslearning-petert.blogspot.co.uk/2014/04/design-patterns-for-scientific.html, if interested.)

1. Scenarios to use Multiple inheritance
- Inherit from multiple interface classes (what is interface class? Please read this post, http://cpluspluslearning-petert.blogspot.co.uk/2014/04/pure-virtual-functions.html, if interested), and the interface classes share no common API.
- Inherit from multiple API from third-party libraries, as have no access to source code.
The group that dislike the features of multiple inheritance argues that multiple inheritance should not be used and proposed other solutions for these two scenarios. I have to say that those proposed solutions (for instance using composition) are correct too, but this does not prove that we cannot use MI. Again please read Herb Sutter's argument in his book [1] about why multiple inheritance.

2. Problems with multiple inheritance
Situation will get complicated when the inheritance hierarchy gets into a certain shape or the multiple base classes share the some common API functions.

The diamond death:
The diamond death describes a scenario of inheritance hierarchy happening on MI. When multiple base classes inherit from the same super-base class that either has data members or has virtual functions, at least one of virtual functions from the super-base class is overridden in the base classes and not overridden in the derived class. When invoking the virtual function (overridden in the base class but not in the derived class) in the derived class, the ambiguity is caused as the derived has multiple copies of this virtual function from its multiple base classes. The same ambiguity happens when accessing the data member declared in super-base class from the derived class.

The fix is to use "virtual inheritance". More details about the diamond death please read my another blog entry, http://cpluspluslearning-petert.blogspot.co.uk/2014/04/multiple-inheritance-diamond-death.html.

The Siamese twin problem:
The Siamese twin problem happens when multiple base classes share some common API functions. And each base class has different behavior of its own. When casting from the derived class to the base classes and invoking the functions that all the base classes have (if inheriting > 2 classes, then at least two base class have), the function is required to behave as the same as its base class.

Here is the example listed by Herb Sutter in his book [1]. (Only the naming is bit of different)
*********************************************************************************
class A {
public:
    virtual ~A();
    std::string Name();
private:
   virtual std::string DoName();
};

class B1 : virtual public A {
   std::string DoName(); // private by default
};

class B2 : virtual public A {
   std::string DoName();
};

// Multiple inheritance
class D : public B1, public B2 {
   std::string DoName() {return "D";}
};
   
// Emulation
class D : public B1 {
public:
    class D2 : public B2 {
    public:
        void Set(D* d) {
            m_d = d;
        }
     private:
        std::string DoName() {return m_d->DoName();}
        D* m_d;
     };
   
     D() {m_d2.Set(this);}
     D(const D& other) : B1(other), m_d2(other.m_d2) {
         m_d2.Set(this);
     }
     D& operator=(const D& other) {
         B1::operator=(other);
         m_d2 = other.m_d2;
         return *this;
     }

     operator B2& {return m_d2;} // implicit type conversion operator
     B2& AsB2() {return m_d2;}
private:
     std::string DoName() {return "D";}
     D2 m_d2;
};
*********************************************************************************

This is an brilliant solution. The only drawback I think critical is that dynamically casting from D to B2 will fail. It has the similar memory layout like MI but does not have its complexity of initialization.

3. Replacement of MI
For those who working on C++ application but the feature of multiple inheritance is banned. Herb Sutter also provides a workaround of MI to reach nearly the same functionality as MI in his book [1]

This is an absolutely brilliant solution. I would like to copy it over to share with all. Here is the example listed by Here Sutter in his book [1].
*********************************************************************************
class BaseA {
public:
    virtual int ReadBuf(const char*);
    ......
};

class BaseB {
public:
    virtual int ReadBuf(const char*);
    ......
};

// problem: there is only one implementation of Derived::ReadBuf
// when casting from Derived to BaseA or BaseB, then only one
// behavior, which is contradictory to have one for BaseA and
// one for BaseB.
class Derived : public BaseA, public BaseB {
public:
    int ReadBuf(const char*);
    ......
};

// Solution: add and extra layer between BaseA/BaseB and Derived
// BaseA2 and BaseB2 are to bridge the gap.
class BaseA2 : public BaseA {
public:
    virtual int BaseAReadBuf(const char*) = 0;
private:
    int ReadBuf(const char* p) {
       return BaseAReadBuf(p);
    }
};

class BaseB2 : public BaseB {
public:
    virtual int BaseBReadBuf(const char*) = 0;
private:
    int ReadBuf(const char* p) {
       return BaseBReadBuf(p);
    }
};

class Derived : public BaseA2, public BaseB2 {
public:
    int BaseAReadBuf(const char*) ;
    int BaseBReadBuf(const char*)
};
*********************************************************************************


Bibliography:
[1] Herb Sutter, "More Exceptional C++ - 40 New Engineering Puzzles, Programming Problems, and Solutions", 2002

No comments:

Post a Comment