lp0 on fire

My personal blog

C89-OOP: Interface inheritance

Multiple inheritance always has been a rough topic since there is a lot that needs to be taken into account. So... why instead not force single inheritance and give programmers the burden of re-implementing all the code for each class instead? Welcome to interfaces. Like C's prototypes, interfaces contain declarations that define which definitions to exist when a class inherits the interface.

interface IFoo
{
    int getX();
}

class Foo : IFoo
{
    int x;

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

    int getX()
    {
        return this.x;
    }
}

To translate this to C89:

typedef struct IFoo IFoo;
typedef struct Foo Foo;

struct IFoo
{
    int (*getX)(IFoo* self);
};

struct Foo
{
    IFoo* ifoo;
    int x;
};

static int Foo_getX(IFoo* self)
{
    Foo* foo = (Foo*)self;
    return foo->x;
}

void Foo_ctor(Foo* self, int x)
{
    static IFoo ifoo = {
        Foo_getX
    };

    self->ifoo = &ifoo;
    self->x = x;
}

We can emulate it as close as possible by using a technique called Virtual Method Table (VMT):

struct IFoo
{
    int (*getX)(IFoo* self);
};

Here IFoo is a VMT that contains all methods and property getters/setters that a class inheriting from IFoo needs to contain. While we cannot enforce an interface to be respected at compile time in C89, we can at least have the error handling at runtime.

struct Foo
{
    IFoo* ifoo;
    int x;
}

Foo_ctor(Foo* self, int x)
{
    static IFoo ifoo = {
        Foo_getX
    };

    self->ifoo = &ifoo;
    self->x = x;
}

Foo.ifoo is a pointer so we can define a single VMT (ifoo) that we share across all instances of Foo. This means we can save up quite a bit on memory compared to having all instances use their own VMT instance. The reason why ifoo is a static variable inside the function, is to have a static instance of the VMT only changable in Foo_ctor's scope.

static int Foo_getX(IFoo* self)
{
    Foo* foo = (Foo*)self;
    return foo->x;
}

Foo_getX will only ever get accessed from the source file, so we don't need to expose it outside. It also allows for more compiler optimizations. We have to cast IFoo to Foo in order to be able to access Foo's members.

Usage

To call a method from a VMT, we can simply do this:

Foo foo;
int val;

Foo_ctor(&foo, 1);
val = foo.ifoo->getX((IFoo*)&foo);

We can use IFoo also for casting the same way we did it in the previous article:

Foo foo;
IFoo* ifoo;

Foo_ctor(&foo, 1);
ifoo = (IFoo*)&foo;
foo = (*(Foo*)ifoo);

Conclusion

We explored what interfaces look like in OOP, how to implement this in C89, and how to call and cast them! In the next article we'll explore an alternative way to deal with inheritance; virtual inheritance.