Skip to content

Instantly share code, notes, and snippets.

@MerijnHendriks
Last active October 17, 2021 19:20
Show Gist options
  • Save MerijnHendriks/98520f0af426a0b3cc24f6c8a334cb7d to your computer and use it in GitHub Desktop.
Save MerijnHendriks/98520f0af426a0b3cc24f6c8a334cb7d to your computer and use it in GitHub Desktop.
Inheritance in ANSI-C

Inheritance in ANSI-C

Emulating inheritance in ANSI-C

Introduction

Sometimes the cleanest implementation is by taking advantage of inheritance despite the cost of the technique. In this article I'll discuss various implementations with their characteristics. We'll emulate the following code from C# in ANSI-C:

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;
    }
}

Anatomy of a class

A class is nothing more than a type-defined struct containing:

  • the derived class (if the class inherits from another)
  • a pointer to a virtual table (if the class uses virtual methods)
  • pointers to derived interfaces (if the class uses interfaces)
  • field members (if it has any fields)

Non-virtual methods are just ANSI-C functions with the self parameter as first argument in a function as a replacement for this.

In order to initialize memory and set the members of the struct to our desired values, we implement a constructor function. Here we call the base constructor and initialize virtual methods, interfaces, field values.

Techniques

By copy-paste

The simplest and most straight-foward way is by copy-pasting the members of the base struct into the derived struct. This allows you to get the best alignment and remove the additional cost imposed by embedding structs. It is the most straight-forward to work with as you can access all properties from the derived struct.

However, this means that alignment of the properties is not garuanteed which in return doesn't allow you to cast the derived struct. In addition, copy-pasting also results in a lot of code duplication. Because of the limitations opposed on this technique, it's not very practical to use.

#include <stdio.h>
#include <stdlib.h>

typedef struct A A;

struct A
{
    int x;
};

struct B
{
    int x;
    int y;
};

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

static void B_ctor(B* self, int x, int y)
{
    self->x = x;
    self->y = y;
}

int main()
{
    A a;
    B b;
    
    A_ctor(&a, 1);
    B_ctor(&b, a.x, 2);
    printf("a: %d\n", a.x);
    printf("b: %d, %d\n", b.x, b.y);

    return EXIT_SUCCESS;
}

By embedding

A more advanced technique is by embedding the base class into the derived class as the first member of the struct. This allows for code-reuse, and the first member is garuanteed to keep alignment 1 which allows for type casting. However, the code is a bit harder to follow as you need to keep track of base struct members.

#include <stdio.h>
#include <stdlib.h>

struct A A;
struct B B;

struct A
{
    int x;
};

struct B
{
    A base;
    int y;
};

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

static void B_ctor(B* self, int x, int y)
{
    A_ctor(&self->base, x);
    self->y = y;
}

static void run_upcast()
{
    A a;
    B b;
    
    B_ctor(&b, 1, 2);
    a = *(struct A*)&b;
    printf("a: %d\n", a.x);
}

static void run_downcast()
{
    A a;
    B b;
    
    A_ctor(&a, 1);
    b = *(struct B*)&a;
    printf("b: %d, %d\n", b.base.x, b.y);
}

int main()
{
    run_upcast();
    run_downcast();

    return EXIT_SUCCESS;
}

Conclusion

You've now learned two techniques for implementing inheritance in ANSI-C, as well as the advantages and disadvantages of each technique. Next time I will explain how composition can be implemented in ANSI-C.

References

Footnotes

  1. ISO/IEC 9899:TC2 section 6.7.2.1.13

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment