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:
- A Vtable Struct: Contains function pointers representing the interface methods.
- A Base Struct: Encapsulates the object's shared state and holds a pointer to its corresponding vtable.
- 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 constinstances 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*andBase*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.