Implementing Virtual Method Tables in C with Structs

Although the C programming language lacks built-in object-oriented features, developers can achieve runtime polymorphism through explicit virtual method tables (vtables). This article explains how to emulate dynamic dispatch in C by creating separate structures for function pointers and object data. By linking these structures, you can design clean, extensible abstractions that closely mirror the class hierarchies and virtual functions found in languages like C++.

Understanding the Vtable Pattern in C

In object-oriented languages like C++, a vtable is a hidden array of function pointers created for classes with virtual functions. Each object holds a pointer (often called a vptr) that references this table.

To recreate this pattern manually in C, you need three components:

  1. A Vtable Struct: Contains function pointers representing the interface methods.
  2. A Base Struct: Encapsulates the object's shared state and holds a pointer to its corresponding vtable.
  3. Derived Structs: Embed the base struct and provide concrete implementations for the function pointers.

Step 1: Define the Vtable and Base Object

First, define the function pointer signatures inside a dedicated vtable struct, followed by the base object struct that holds the vptr.

#include <stdio.h>

// Forward declarations
typedef struct Shape Shape;
typedef struct ShapeVTable ShapeVTable;

// The Vtable structure containing virtual method pointers
struct ShapeVTable {
    void (*draw)(const Shape* self);
    double (*area)(const Shape* self);
};

// The Base Object structure
struct Shape {
    const ShapeVTable* vptr;
};

Using const Shape* self as the first parameter mimics the implicit this pointer in object-oriented languages.

Step 2: Implement Derived Classes

To derive from the base object, embed the Shape struct as the first member of the new struct. This memory layout guarantees that a pointer to the derived struct can be safely cast to a pointer to the base struct.

// Derived Circle struct
typedef struct {
    Shape base;      // Inherited base class
    double radius;   // Specific attribute
} Circle;

// Circle method implementations
static void circle_draw(const Shape* self) {
    const Circle* circle = (const Circle*)self;
    printf("Drawing Circle with radius: %.2f\n", circle->radius);
}

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

// Static Vtable instance for Circle
static const ShapeVTable circle_vtable = {
    .draw = circle_draw,
    .area = circle_area
};

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

Repeat this process for another type to demonstrate alternative behavior:

// Derived Rectangle struct
typedef struct {
    Shape base;
    double width;
    double height;
} Rectangle;

static void rectangle_draw(const Shape* self) {
    const Rectangle* rect = (const Rectangle*)self;
    printf("Drawing Rectangle of size: %.2f x %.2f\n", rect->width, rect->height);
}

static double rectangle_area(const Shape* self) {
    const Rectangle* rect = (const Rectangle*)self;
    return rect->width * rect->height;
}

// Static Vtable instance for Rectangle
static const ShapeVTable rectangle_vtable = {
    .draw = rectangle_draw,
    .area = rectangle_area
};

// Rectangle constructor
Rectangle create_rectangle(double width, double height) {
    Rectangle r;
    r.base.vptr = &rectangle_vtable;
    r.width = width;
    r.height = height;
    return r;
}

Step 3: Dispatching Calls Polymorphically

To execute methods dynamically, invoke the function pointers via the object's vptr. You can also write helper wrapper functions to make the call syntax cleaner.

// Wrapper functions for dynamic dispatch
void shape_draw(const Shape* shape) {
    shape->vptr->draw(shape);
}

double shape_area(const Shape* shape) {
    return shape->vptr->area(shape);
}

int main(void) {
    Circle circle = create_circle(5.0);
    Rectangle rectangle = create_rectangle(4.0, 6.0);

    // Treat both as generic Shape pointers
    Shape* shapes[] = { (Shape*)&circle, (Shape*)&rectangle };

    for (int i = 0; i < 2; ++i) {
        shape_draw(shapes[i]);
        printf("Calculated Area: %.2f\n", shape_area(shapes[i]));
    }

    return 0;
}

Key Considerations

  • Memory Efficiency: Store vtables as static const instances so that all objects of the same type share one table in read-only memory, avoiding per-instance pointer overhead.
  • Layout Compatibility: Always keep the base struct as the first member of the derived struct. C guarantees that the address of a struct matches the address of its first member, making casts between Derived* and Base* standard-compliant.
  • Type Safety: Explicit casting bypasses C's type checks. Ensure that function pointers are only called with compatible struct instances to prevent memory corruption.