lp0 on fire

My personal blog

C89-OOP: Single inheritance

In the previous article I demonstated quickly the atonomy of a class and how to emulate this in C89. Today I'll cover briefly single inheritance, aka where a class can only inherit from another class.

Implementing inheritance

The concept itself is pretty simple, the child class embeds the super class in order to contain it's functionality. Let's take this example using Foo from the previous article:

class Bar : Foo
{
    int y;

    Bar(int x, int y) : Foo(x)
    {
        this.y = y;
    }

    int getY()
    {
        return this.y;
    }
}

Bar "extends" Foo by adding a new field y and a new method getY. In order to initialize both, we call the super's constructor.

Let's try to accomplish the same in C89:

typedef struct Bar Bar;

struct Bar
{
    Foo base;
    int y;
};

void Bar_ctor(Bar* self, int x, int y)
{
    Foo_ctor(&self->base, x)
    self->y = y;
}

int Bar_getY(Bar* self)
{
    return self->y;
}

Pretty much the same as the example of the previous article, but there are some important distinctions:

struct Bar
{
    Foo base;
    int y;
};

Foo is embedded inside of Bar as it's first member. This is where our base class's fields and class properties live. The base field being the first member is important. I'll explain why in the next article.

void Bar_ctor(Bar* self, int x, int y)
{
    Foo_ctor(&self->base, x)
    self->y = y;
}

In the constructor we initialize Foo first so it has the correct values. You always want to initialize base classes first so you can use the members later in your constructor.

Calling inherited members

We can also access members of the super class from the child class:

Bar bar = new Bar();
int val = bar.getX();

Emulating this in C89 is absolutely possible, but it's not going to look pretty:

Bar bar;
int val = 0;

Bar_ctor(&bar, 1, 2);
val = Foo_getX(&bar->base);

Sure we could make it look prettier by adding a conversion function:

int Bar_getX(Bar* self)
{
    return Foo_getX(self->base);
}

int main()
{
    Bar bar;
    int val = 0;

    Bar_ctor(&bar, 1, 2);
    val = Bar_getX(&bar);

    return 0;
}

But now you have to add this conversion method for each previous inherited class in the inheritance chain. Not only will this clutter your code with many duplicate functions that take up memory, it will also result in larger call chains which is not ideal (especially on embedded systems). I would call this "code smell".

Conclusion

You've now learned how to implement a very simple inheritance stratagy, and how to access the members from the base class. Next up is casting our structs to emulate upcasting and downcasting.