Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Virtual inheritance crashes application

The following code crashes (Access violation error) because I used virtual inheritance.
AFAIK virtual inheritance solves the Diamond problem by forcing use of a single instance of a class. In this case the Derived class inherits only one instance of IObjectso there should be no problem, but it crashes.

class IObject
{
public:
    virtual int getType()=0;
};
class Base : public IObject
{
protected:
    int val;
public:
    Base() { val = 1; }
    virtual int getType();
};
int Base::getType() { return val; }

class Derived : public virtual Base //If I remove the virtual keyword here problem is solved.
{
public:
    Derived() { val = 2; }
};

int getVal( void* ptr ) 
{
    return ((IObject*)ptr)->getType();
}

int main()
{
    void* ptr = new Derived();
    cout << getVal(ptr) << endl;
    return 0;
}
like image 210
atoMerz Avatar asked Dec 15 '22 23:12

atoMerz


1 Answers

The problem is that the chain of casts is incorrect: Derived* -> void* -> IObject* is undefined behavior resulting from mixing C and C++ concepts. More specifically, the rules around void* are inherited from C without any adaptation for objects and hierarchy.

The solution, therefore, is to make sure that any cycle through void* is a T -> void* -> T cycle: always through the same type. Thus, in your situation, you need Derived* -> IObject* -> void* -> IObject*.


To understand why virtual inheritance causes an issue, you have to understand the specifics of how it is represented concretely (which is implementation-defined). Let's have a look at examples of possible in-memory representations (loosely based on the Itanium ABI).

A linear non-virtual hierarchy is implemented as if by composition:

struct Base { int a; };
struct Derived: Base { int b; };
struct SuperDerived: Derived { int c; };

+---+---+
| a | b |
+---+---+
^~~~~~~~~ Derived
    ^~~~~ Derived specific
^~~~~         Base

+---+---+---+
| a | b | c |
+---+---+---+
^~~~~~~~~~~~~ SuperDerived
        ^~~~~ SuperDerived specific
^~~~~~~~~     Derived
^~~~~         Base

In this case, &derived == &base and &superderived == &derived in general (note: if one layer does not have a virtual table and the next layer does, then this falls off the roof).

A hierarchy with multiple bases

struct Base1 { int a; };
struct Base2 { int b; };
struct Derived: Base1, Base2 { int c; };

+---+---+---+
| a | b | c |
+---+---+---+
^~~~~~~~~~~~~ Derived
        ^~~~~ Derived specific
    ^~~~~     Base2
^~~~~         Base1

In this case, &derived == &base1 but &derived != &base2, so already we note that a base class does not necessarily have the same address that its derived class.

And finally, let's push virtual inheritance in:

struct Object { int a; };
struct Base1: virtual Object { int b; };
struct Base2: virtual Object { int c; };
struct Derived: Base1, Base2 { int d; };

+---+---+
| b | a |
+---+---+
^~~~~~~~~ Complete Base1
^~~~~     Base1 specific
    ^~~~~ Object

+---+---+
| c | a |
+---+---+
^~~~~~~~~ Complete Base2
^~~~~     Base2 specific
    ^~~~~ Object

+---+---+---+---+
| b | c | d | a |
+---+---+---+---+
^~~~~~~~~~~~~~~~~ Complete Derived
        ^~~~~     Derived specific
^~~~~             Incomplete Base1
    ^~~~~         Incomplete Base2
            ^~~~~ Object

The challenge here is that a single instance of the virtual base should be shared between all potential bases. Since only the complete object knows which bases will be involved, a simple choice is to let the complete object be responsible for the placement of the virtual base (which it places at the tail) and have the virtual table provide the machinery to navigate, at runtime, from Object to a derived class.

However, note that in the case of how our design &base1 != &object, &base2 != &object and &derived != &object because object is placed at the tail.

That is why it is important to perform the casts using the C++ machinery which knows how to statically or dynamically (depending on the situation) compute the pointer adjustment necessary when going from one base to another.

Note: the C++ machinery knows whether the computation is static or dynamic and for example static_cast<Base1*>(&object) is a compile-time error, a dynamic_cast is necessary here.

like image 138
Matthieu M. Avatar answered Dec 26 '22 21:12

Matthieu M.