Simulating OOP Polymorphism in C with Function Pointers and Vtables
The article walks through a C programmer's journey from hard‑coded if‑else shape handling to embedding function pointers in structs, introducing shared function tables, automatic initialization, memory savings, inheritance via table overrides, and finally showing how C++ virtual functions automate the same mechanism.
You are an early‑80s C programmer tasked with building a graphics library that must draw and compute the area of circles, rectangles, and triangles. The initial implementation defines separate draw_circle, draw_rectangle, and draw_triangle functions and a generic draw_shape(void *shape, int type) that uses a long chain of if‑else statements to select the correct function.
Hard‑coded type checks become a nightmare
When a product manager asks for a new shape (e.g., a trapezoid) you must add another else if branch everywhere draw_shape is called. Adding an ellipse repeats the same work, and the code quickly balloons, making it error‑prone and impossible to scale.
Embedding function pointers in a base struct
To eliminate manual type checks you place a function pointer for drawing and another for area calculation inside a generic Shape struct:
typedef struct {
void (*draw)(void *);
double (*area)(void *);
} Shape;
typedef struct {
Shape base;
double radius;
} Circle;
void draw_circle(void *obj) {
Circle *c = (Circle *)obj;
printf("Drawing circle with radius %.2f
", c->radius);
}
double calc_circle_area(void *obj) {
Circle *c = (Circle *)obj;
return 3.14 * c->radius * c->radius;
}
Circle c;
c.base.draw = draw_circle;
c.base.area = calc_circle_area;
c.radius = 5.0;
c.base.draw(&c);Now all shapes can be stored in an array of Shape * and drawn without any if‑else chain.
Fatal flaw: duplicated function pointers
Each object still stores its own copies of identical function pointers. Forgetting to initialise a pointer (as shown with c2) or assigning the wrong function (e.g., setting draw_rectangle for a Circle) leads to crashes.
Automatic initialisation functions
Providing dedicated init functions ensures the pointers are set correctly at creation time:
void init_circle(Circle *c, double radius) {
c->base.draw = draw_circle;
c->base.area = calc_circle_area;
c->radius = radius;
}Using init_circle(&c, 5.0) eliminates the "forgot to set pointer" bug.
Sharing a single function table
To save memory when creating thousands of objects, move the function pointers into a shared table and let each object store only a pointer to that table:
typedef struct {
void (*draw)(void *);
double (*area)(void *);
} Function_Table;
Function_Table circle_ftable = { draw_circle, calc_circle_area };
Function_Table rectangle_ftable = { draw_rectangle, calc_rectangle_area };
typedef struct {
Function_Table *ftable;
double radius;
} Circle;
void init_circle(Circle *c, double radius) {
c->ftable = &circle_ftable;
c->radius = radius;
}
Circle c;
init_circle(&c, 5.0);
c.ftable->draw(&c);With 10 000 circles the extra memory drops by about 80 KB because each object now holds only one pointer instead of two function pointers.
Supporting inheritance and method overriding
By defining a new FilledCircle that embeds Circle and provides a different draw function while reusing the same area function, you achieve inheritance‑like behaviour:
void draw_filled_circle(void *obj) {
Circle *c = (Circle *)obj;
draw_circle(obj);
printf("Filling with color
");
}
Function_Table filled_circle_ftable = { draw_filled_circle, calc_circle_area };
typedef struct {
Circle base; // inheritance
} FilledCircle;
void init_filled_circle(FilledCircle *fc, double r) {
fc->base.ftable = &filled_circle_ftable;
fc->base.radius = r;
}The new type overrides draw while keeping the original area implementation.
C++ automates the pattern with virtual functions
When Bjarne Stroustrup designs C++, he turns the manual function‑table mechanism into language support. The following C++ code shows the same idea using virtual methods, where the compiler generates a vtable automatically:
class Shape {
public:
virtual void draw() = 0;
virtual double area() = 0;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { printf("Drawing circle with radius %.2f
", radius); }
double area() override { return 3.14 * radius * radius; }
};
Circle c(5.0);
c.draw(); // compiled to c.vtable->draw(&c)The virtual keyword creates a vtable behind the scenes, eliminating the need for explicit function‑pointer tables.
Overall, the article demonstrates how to evolve from fragile if‑else dispatch to robust polymorphic designs in C, how to share function tables for memory efficiency, how to simulate inheritance, and how these ideas culminate in C++'s built‑in virtual function mechanism.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
