Thursday 3 April 2014

Multiple inheritance - the diamond death problem

1. Problem
The issue with multiple inheritance happens when the class hierarchy appears like a diamond as in Figure 1. The common data or functions appearing in top base class, A, will cause ambiguity when accessed or called by the bottom class, D.
                                                        Figure 1 [1]

See the snippet in Microsoft Visual C++ 2010 Express
*********************************************************************************
#include "stdafx.h"

#include <iostream>
#include <string>

class Base{
public:
    Base(int x) : m_x(x)
    {}
    virtual std::string GetName() const {
        return "Base";  
    }
    virtual std::string GetID() const {
        return "BASE";
    }
    void SetX(int x) {
        m_x = x;
    }

    int GetX() const {
        return m_x;
    }

public:
    int m_x;
};

class Foo : public Base {
public:
    Foo(int x, int y) : Base(x), m_y(y)
    {}

    virtual std::string GetName() const {
        return "Foo";
    }

    virtual std::string GetID() const {
        return "FOO";
    }

private:
    int m_y;
};

class Bar : public Base {
public:
    Bar(int x, int z) : Base(x), m_z(z)
    {}

    virtual std::string GetName() const {
        return "Bar";
    }

    virtual std::string GetID() const {
        return "BAR";
    }

private:
    int m_z;
};

class FooBar : public Foo, public Bar {
public:
    FooBar(int x, int y, int z, int a) 
        : Foo(x, y), Bar(x, z), m_a(a)
    {}

    virtual std::string GetName() const {
        return "FooBar";
    }

private:
    int m_a;
};
*********************************************************************************
Here is a few things that are worth of paying attention.
The memory size of classes:
    sizeof(Base)        : 8
    sizeof(Foo)         : 12
    sizeof(Bar)          : 12
    sizeof(FooBar)    : 28
Explanation of their size:
    sizeof(Base)        : 8    - m_x and the virtual pointer
                              : 8 = 4 + 4 (win32)
    sizeof(Foo)         : 12  - m_x, m_y and the virtual pointer
                              : 12 = 4 + 4 + 4 (win32)
    sizeof(Bar)          : 12  - m_x, m_z and the virtual pointer
                              : 12 = 4 + 4 + 4 (win32)
    sizeof(FooBar)    : 28  - sizeof(Foo), sizeof(Bar), m_a and virtual pointer
                              : 28 = 12 + 12 + 4 + 4 (win32)
This means the FooBar will have memory footprint of Foo and Bar, and both has the memory allocated for m_x.

The memory map of bottom class - FooBar:
It includes:
    - virtual pointer if virtual function defined - here yes.
    - The 1st base class - Foo
    - The 2nd base class - Bar
    - Any other base class ...... 
    - Data members of its own
************************************************
| Memory map of FooBar
************************************************
|| Virtual pointer of FooBar
************************************************
|| Memory map of Foo
************************************************
||| Virtual pointer of Foo
************************************************
||| Memory map of Base (for Foo)
||| m_x
************************************************
||| m_y (for Foo)
||| End of Foo
************************************************
|| Memory map of Bar
************************************************
||| Virtual pointer of Bar
************************************************
||| Memory map of Base (for Bar)
||| m_x
************************************************
||| m_z (for Bar)
||| End of Bar
************************************************
|| m_a (for FooBar)
************************************************

The ambiguity:
    FooBar fb(1, 2, 3, 4);
    Base& bRef = fb;  // Error: ambiguity 
    Foo& fooRef = fb;
    fooRef.SetX(20);
    Bar& barRef = fb;
    barRef.SetX(30);    
This upward casting fails, because of the ambiguity. Base ref have no idea about which copy is to take m_x and GetID from Foo or Bar?
    fooRef.GetX() = 20;
    barRef.GetX() = 30;
    fb.SetX(40); // Error: ambiguity
    fb.GetX(); // Error: ambiguity
This comes to shock as well. You would expect fooRef and barRef operate on the same m_x, but actually not. Each of them has their own copy as the memory map illustrated above. And SetX()/GetX() cannot directly called on FooBar object because the compiler have no idea which memory this function should operate on Foo::m_x or Bar::m_x.
    fooRef.GetID();
    barRef.GetID();
    fb.GetID(); // Error: ambiguity
Again here fails as well because FooBar have no idea which version it should take.
    fooRef.GetName();
    barRef.GetName();
    fb.GetName(); // OK
This virtual function is overridden in FooBar as well, so its own version will be called.

2. Solution provided by C++ standard
In order to remove the ambiguity existing on the common data and functions from the top base class (in the above example m_x, GetX()/SetX() and GetID()), virtual inheritance have to replace the normal inheritance.

See the snippet in Microsoft Visual C++ 2010 Express
*********************************************************************************
class Base{
public:
    Base(int x) : m_x(x)
    {}
    virtual std::string GetName() const {
        return "Base";  
    }
    virtual std::string GetID() const {
        return "BASE";
    }
    void SetX(int x) {
        m_x = x;
    }

    int GetX() const {
        return m_x;
    }

public:
    int m_x;
};


class FooV : virtual public Base {
public:
    FooV(int x, int y) : Base(x), m_y(y)
    {}

    virtual std::string GetName() const {
        return "FooV";
    }

    virtual std::string GetID() const {
        return "FOOV";
    }

private:
    int m_y;
};

class BarV : virtual public Base {
public:
    BarV(int x, int z) : Base(x), m_z(z)
    {}

    virtual std::string GetName() const {
        return "BarV";
    }

    virtual std::string GetID() const {
        return "BARV";
    }

private:
    int m_z;
};

class FooBarV : public FooV, public BarV {
public:
    // more complex initialization order
    // start from the most base classes
    // and in the order of inheritance
    FooBarV(int x, int y, int z, int a) 
        : Base(x), FooV(x, y), BarV(x, z), m_a(a)
    {}

    virtual std::string GetName() const {
        return "FooBarV";
    }

    // has to present otherwise compilation error
    virtual std::string GetID() const {
        return "FOOBARV";
    }

private:
    int m_a;
};
*********************************************************************************

It is worth mentioning that FooBarV has to override GetID() as well, otherwise compiler will complain about it as we see that this function causes ambiguity in Section 1. Here we are going to see how the problems are solved.
The memory size of classes:
    sizeof(Base)          :  8
    sizeof(FooV)        : 20
    sizeof(BarV)         : 20
    sizeof(FooBarV)   : 32
This comes to shock especially for the size of FooV and BarV. This is due to the specific implementation/handling of virtual inheritance in the compiler. A common practice is to have a base class copy lying around the virtual inherited sub-class. And the implementation-specific behavior causes the size bloating of the virtual inheriting classes.

The memory map of classes:
     Base:  m_x and the virtual pointer : 8 = 4 + 4
     FooV: a copy of Base, m_y, virtual pointer, a pointer to Base
                20 = 8 + 4 + 4 + 4
     BarV: a copy of Base, m_z, virtual pointer, a pointer to Base
                20 = 8 + 4 + 4 + 4
     FooBarV: m_x for Base
                   : m_y, FooV's pointer to Base, FooV's virtual pointer
                   : m_z, BarV's pointer to Base, BarV's virtual pointer
                   : m_a, FooBarV's virtual pointer
                36 = 4 + (4 + 4 + 4) + (4 + 4 + 4) + 4 + 4
In my mind FooBarV should be 36. Scott Meyers has more explanation about virtual inheritance memory map in [2].

************************************************
| Memory map of FooBar
************************************************
|| Virtual pointer of FooBar
************************************************
************************************************
|| Memory map of FooV
************************************************
||| Virtual pointer of Foov
************************************************
||| pointer of FooV to Base
************************************************
||| m_y (for Foo)
||| End of Foo
************************************************
|| Memory map of BarV
************************************************
||| Virtual pointer of BarV
************************************************
||| pointer of BarV to Base
************************************************
||| m_z (for Bar)
||| End of Bar
************************************************
|| Memory map of Base
************************************************
|| m_x
************************************************
|| m_a (for FooBar)
************************************************

The ambiguity:
    FooBarV fbv(1, 2, 3, 4);
    Base& bvRef = fbv; // OK
    FooV& fvRef = fbv;
    BarV& barvRef = fbv;
GetID() has to be overridden in FooBarV in order to get green lights from compiler. Otherwise the compiler complains about FooBarV's the ambiguous inheritance of GetID from Base. Therefore now the upward-casting from FooBarV to Base is working OK.
    bvRef.GetX() =  fvRef.GetX() = barvRef.GetX() =  fbv.GetX() = 1;
    bvRef.SetX(10);
    bvRef.GetX() =  fvRef.GetX() = barvRef.GetX() =  fbv.GetX() = 10;
    fvRef.SetX(20);
    bvRef.GetX() =  fvRef.GetX() = barvRef.GetX() =  fbv.GetX() = 20;
    barvRef.SetX(30);
    bvRef.GetX() =  fvRef.GetX() = barvRef.GetX() =  fbv.GetX() = 30;
    fbv.SetX(40);
    bvRef.GetX() =  fvRef.GetX() = barvRef.GetX() =  fbv.GetX() = 40;
As shown in FooBarV's memory map, it shares the same copy of Base with its two base classes. Any operation on the common data will be reflected on all upward-casting references. This is what we are expecting and no surprise.

GetName() and GetID() work fine without any ambiguity.

As Meyer stated on Iterm 40 on [2] that virtual inheritance impose costs on class size, code speed and complexity of initialization and assignment, virtual inheritance will be most practical when the virtual base class has no data member. And Herb Sutter also shows a few techniques/workaround to replace multiple inheritance to avoid diamond death problem in [3].

Bibliography:
[1] http://en.wikipedia.org/wiki/Multiple_inheritance
[2] Scott Meyers, "Effective C++", Third Edition, 
[3] Herb Sutter, "More Exceptional C++", 40 New Engineering Puzzles, Programming Problems, and Solutions 

No comments:

Post a Comment