Simulating Inheritance in C Using Struct Composition

The C programming language does not provide native object-oriented features such as classes or inheritance. However, developers can emulate single inheritance, polymorphism, and dynamic dispatch through disciplined patterns using struct composition, precise memory layout guarantees, and function pointers. This article explains how to implement inheritance-like behavior in standard C using first-member struct embedding, type punning via pointers, and virtual method tables.

1. First-Member Struct Embedding

The most common and standard-compliant pattern for simulating single inheritance in C is embedding the "base" struct as the first member of the "derived" struct.

According to the C standard (ISO/IEC 9899), a pointer to a structure object, suitably converted, points to its initial member, and vice versa. There is no padding before the first member. This guarantee allows safe upcasting from a derived pointer to a base pointer through simple pointer casting.

typedef struct {
    int id;
} Shape;

typedef struct {
    Shape base; // Must be the first member
    int width;
    int height;
} Rectangle;

Because base is at offset zero inside Rectangle, a pointer to Rectangle has the same memory address as a pointer to its Shape component:

Rectangle rect = { .base = { .id = 1 }, .width = 10, .height = 20 };
Shape *shape_ptr = (Shape *)▭ // Safe upcast

Functions accepting a Shape* can accept a pointer to a Rectangle* (cast appropriately) and access the common fields safely.

2. Method Overriding via Function Pointers

To attach behavior to data structures, function pointers can be stored directly inside the base structure. This enables individual instances to specify their own implementations of methods.

typedef struct Animal Animal;

struct Animal {
    void (*speak)(Animal *self);
};

typedef struct {
    Animal base;
    const char *name;
} Dog;

void dog_speak(Animal *self) {
    Dog *dog = (Dog *)self; // Downcast
    printf("%s says: Woof!\n", dog->name);
}

Dog create_dog(const char *name) {
    Dog d;
    d.base.speak = dog_speak;
    d.name = name;
    return d;
}

When invoking animal->speak(animal), the program resolves the correct implementation at runtime, mimicking virtual methods.

3. Virtual Method Tables (Vtables)

Placing function pointers directly inside the base struct introduces memory overhead when working with thousands of instances, as each instance duplicates the pointers. The Vtable pattern separates dynamic method pointers into a shared, static structure.

typedef struct Shape Shape;

typedef struct {
    double (*area)(const Shape *self);
    void (*draw)(const Shape *self);
} ShapeVTable;

struct Shape {
    const ShapeVTable *vptr;
};

typedef struct {
    Shape base;
    double radius;
} Circle;

double circle_area(const Shape *self) {
    const Circle *c = (const Circle *)self;
    return 3.14159 * c->radius * c->radius;
}

static const ShapeVTable circle_vtable = {
    .area = circle_area,
    .draw = NULL
};

Circle create_circle(double radius) {
    Circle c;
    c.base.vptr = &circle_vtable;
    c.radius = radius;
    return c;
}

To invoke a polymorphic method, caller code accesses the function through the table:

Shape *s = (Shape *)&my_circle;
double a = s->vptr->area(s);

4. Macro-Based Field Inheritance (Alternative)

An older alternative involves defining common fields inside a preprocessor macro and expanding that macro at the beginning of each struct.

#define SHAPE_FIELDS \
    int id;          \
    int color;

typedef struct {
    SHAPE_FIELDS
} Shape;

typedef struct {
    SHAPE_FIELDS
    int width;
    int height;
} Rectangle;

While this enables direct member access without typing .base, it bypasses pointer safety guarantees and makes type conversions prone to alignment issues. First-member embedding remains the preferred, standard-compliant pattern.