lp0 on fire

My personal blog

C89-OOP: Virtual inheritance

While most OOP languages opt for implementing interfaces only, there is another lesser-known technique which I think is the pure way of implementing multiple inheritance: virtual inheritance. Using this technique, you don't have to implement everything twice.

Suppose we have this construction:

class A
{
    int x;

    A(int x)
    {
        this.x = x;
    }
}

class B : A
{
    int y;

    B(int x, int y) : A(x)
    {
        this.y = y;
    }
}

class C : A
{
    int z;

    C(int x, int z) : A(x)
    {
        this.z = z;
    }
}

class D : B, C
{
    int v;

    D(int x, int y, int z, int v) : B(x, y), C(x, z)
    {
        this.v = v;
    }
}

Both B and C contain a copy of A, so when D is constructed which of the two instances of A is the original copy? The answer with interfaces is "none because you're not allowed to inherit from two classes".

In C++, you would mark A in both B and C as virtual:

class B : virtual A
{
    // ...
}

class C : virtual A
{
    // ...
}

What C++ does internally is to me a mystery. But what we can do is creating an instance of A separately, then makes each class in the inheritance tree inheriting virually from A gets the address of this instance assigned to it.

Here is how I would emulate this in C89:

typedef struct A A;
typedef struct B B;
typedef struct C C;
typedef struct D D;

struct A
{
    int x;
};

struct B
{
    A* virt_a;
    int y;
};

struct C
{
    A* virt_a;
    int z;
};

struct D
{
    B base_b;
    C base_c;
    int v;
};

void A_ctor(A* self, int x)
{
    self->x = x;
}

void B_ctor(B* self, A* virtA, int y)
{
    self->virt_a = virtA;
    self->y = y;
}

void C_ctor(C* self, A* virtA, int z)
{
    self->virt_a = virtA;
    self->z = z;
}

void D_ctor(D* self, A* virtA, int y, int z, int v)
{
    B_ctor(&self->base_b, virtA, y);
    C_ctor(&self->base_c, virtA, z);
    self->v = v;
}

The interesting part from this implementation is that we change how the class base is stored.

struct B
{
    A* virt_a;
    int y;
};

By making it a pointer, we force forwarding of initialization so we can obtain the right instance of A when it's available. This does mean that we have to create an instance of A and initialize it separately.

Here is how you successfully initialize D:

A a;
D d;

A_ctor(&a, 1);
D_ctor(&d, &a, 2, 3, 4);

Casting works the same as with all other class-like constructs:

A a;
D d;
C* c;

A_ctor(&a, 1);
D_ctor(&d, &a, 2, 3, 4);

c = (C*)&d;
d = (*(D*)c);

Compared to interfaces

There is obviously no right answer which one is better, as it depends on your use-case. It's clear that interfaces take less memory overall since you can share the same VMT accross all instances of that class, but you end up with (much) more code in your application compared to virtual inheritance as you need to reimplement that functionality for each class inheriting it.

Conclusion

You now know how to solve the diamont problem through virtual inheritance, how to implement it and how it compares to interfaces. In the next article I'll cover polymorphism and abstract methods.