Emulating inheritance in ANSI-C
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;
}
}
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.
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;
}
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;
}
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.