Interfaces & Dynamic Dispatch
Runtime polymorphism means picking which area() to run only when the program runs, based on the object's concrete type. Most systems languages have no built-in interface or class hierarchy for this - you build it yourself out of plain data, either with a vtable (a struct of function pointers the object carries) or a tagged union (one value that knows its variant and is switched on). The same task in all seven languages - a Shape whose area() dispatches at runtime - lays the two approaches side by side, and the memory angle is central: a vtable is one shared, static table pointed at by every instance, so the per-object cost is a single pointer, while the objects themselves still need explicit allocation and freeing.
#include <stdio.h>
#include <stdlib.h>
/* No built-in interfaces in C. A 'vtable' is just a struct of
* function pointers; every object carries a pointer to ONE shared,
* static table, so dispatch costs one indirect call. */
struct Shape;
struct ShapeVTable {
double (*area)(const struct Shape *self); /* the virtual method */
};
struct Shape {
const struct ShapeVTable *vtable; /* points at a shared table */
};
/* Each concrete type embeds Shape as its first member, so a
* 'struct Circle *' can be treated as a 'struct Shape *'. */
struct Circle { struct Shape base; double r; };
struct Rect { struct Shape base; double w, h; };
static double circle_area(const struct Shape *s) {
const struct Circle *c = (const struct Circle *)s; /* downcast */
return 3.14159265 * c->r * c->r;
}
static double rect_area(const struct Shape *s) {
const struct Rect *r = (const struct Rect *)s;
return r->w * r->h;
}
/* ONE table per type, with static storage: shared by all instances. */
static const struct ShapeVTable CIRCLE_VT = { circle_area };
static const struct ShapeVTable RECT_VT = { rect_area };
static struct Shape *new_circle(double r) {
struct Circle *c = malloc(sizeof *c); /* heap the object */
if (!c) return NULL;
c->base.vtable = &CIRCLE_VT; /* wire up dispatch */
c->r = r;
return &c->base;
}
static struct Shape *new_rect(double w, double h) {
struct Rect *r = malloc(sizeof *r);
if (!r) return NULL;
r->base.vtable = &RECT_VT;
r->w = w; r->h = h;
return &r->base;
}
int main(void) {
struct Shape *shapes[2] = { new_circle(2.0), new_rect(3.0, 4.0) };
for (int i = 0; i < 2; i++) {
/* Runtime dispatch: the table the object points to decides. */
printf("area = %.4f\n", shapes[i]->vtable->area(shapes[i]));
}
for (int i = 0; i < 2; i++) free(shapes[i]); /* one free per object */
return 0;
}C has no interfaces, so polymorphism is hand-built: a Shape holds a pointer to a ShapeVTable (a struct of function pointers), and each concrete type embeds Shape as its first field so the pointers are interchangeable. The vtables CIRCLE_VT/RECT_VT are static - one shared, read-only table per type - so each heap object only pays for a single vtable pointer; you still malloc each object and match it with a free.
#include <iostream>
#include <memory>
#include <vector>
#include <numbers>
// C++ has interfaces built in: an abstract base class with a pure
// virtual method. The compiler emits the vtable and the vptr for you.
struct Shape {
virtual double area() const = 0; // pure virtual -> abstract
virtual ~Shape() = default; // virtual dtor: delete via base is safe
};
struct Circle final : Shape {
double r;
explicit Circle(double r) : r{r} {}
double area() const override { return std::numbers::pi * r * r; }
};
struct Rect final : Shape {
double w, h;
Rect(double w, double h) : w{w}, h{h} {}
double area() const override { return w * h; }
};
int main() {
// Own the polymorphic objects through the base class by unique_ptr;
// each holds a compiler-generated vptr to its type's shared vtable.
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rect>(3.0, 4.0));
for (const auto &s : shapes)
std::cout << "area = " << s->area() << '\n'; // virtual dispatch
// unique_ptr + virtual ~Shape() destroys each derived object
// correctly at scope exit - no manual delete, no slicing, no leak.
}C++ provides interfaces directly: an abstract Shape with a pure virtual area(), and the compiler synthesizes the per-type vtable and the per-object vptr. A std::vector<std::unique_ptr<Shape>> owns the polymorphic objects through the base pointer; the virtual destructor ensures the correct derived destructor runs, so RAII frees every object exactly once with no manual delete.
// HolyC has no virtual methods, so build a C-style vtable by hand:
// each object carries a pointer to a shared table of function pointers.
class ShapeVTable {
F64 (*area)(U8 *self); // the one virtual method
};
class Shape {
ShapeVTable *vtable; // points at a shared, static-ish table
};
// Concrete types put Shape first so a Circle* doubles as a Shape*.
class Circle { Shape base; F64 r; };
class Rect { Shape base; F64 w, h; };
F64 CircleArea(U8 *self) {
Circle *c = self; // reinterpret the bytes as a Circle
return 3.14159265 * c->r * c->r;
}
F64 RectArea(U8 *self) {
Rect *r = self;
return r->w * r->h;
}
// One table per type. (HolyC has no 'static const'; these live as
// global instances, shared by every object of that type.)
ShapeVTable circle_vt, rect_vt;
circle_vt.area = &CircleArea;
rect_vt.area = &RectArea;
Shape *NewCircle(F64 r) {
Circle *c = MAlloc(sizeof(Circle)); // per-task heap
c->base.vtable = &circle_vt; // wire up dispatch
c->r = r;
return &c->base;
}
Shape *NewRect(F64 w, F64 h) {
Rect *r = MAlloc(sizeof(Rect));
r->base.vtable = &rect_vt;
r->w = w; r->h = h;
return &r->base;
}
// Top-level code runs in TempleOS -- no main needed.
Shape *shapes[2];
shapes[0] = NewCircle(2.0);
shapes[1] = NewRect(3.0, 4.0);
I64 i;
for (i = 0; i < 2; i++)
Print("area = %5.4f\n", shapes[i]->vtable->area(shapes[i])); // dispatch
for (i = 0; i < 2; i++)
Free(shapes[i]); // one Free per object; no GC in ring-0
HolyC has no virtual methods, so the idiom is exactly C's: a Shape holds a pointer to a hand-built ShapeVTable, and each concrete class embeds Shape first so the pointers interchange. The vtables are global instances shared by every object, while each object is carved from the per-task data heap with MAlloc and returned with Free - and since ring-0 TempleOS has no memory protection, a stray cast or double-free can corrupt the system.
const std = @import("std");
// Zig has no interface keyword. The idiomatic runtime-polymorphism
// tool is a TAGGED UNION: one value that knows its own variant, with
// dispatch done by an exhaustive switch (no vtable, no heap per call).
const Shape = union(enum) {
circle: struct { r: f64 },
rect: struct { w: f64, h: f64 },
fn area(self: Shape) f64 {
return switch (self) {
.circle => |c| std.math.pi * c.r * c.r,
.rect => |r| r.w * r.h,
};
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); // reports leaks on exit
const allocator = gpa.allocator();
// Heap-allocate a slice of shapes; the allocator is explicit.
const shapes = try allocator.alloc(Shape, 2);
defer allocator.free(shapes); // paired free, runs on every exit
shapes[0] = .{ .circle = .{ .r = 2.0 } };
shapes[1] = .{ .rect = .{ .w = 3.0, .h = 4.0 } };
for (shapes) |s|
std.debug.print("area = {d:.4}\n", .{s.area()}); // switch dispatch
}Zig has no interfaces; the idiomatic runtime-polymorphism tool is a tagged union (union(enum)) whose area() switches exhaustively over the active variant - no vtable, no per-call indirection, and the compiler errors if you forget a case. The shapes here live in an explicitly allocated slice (allocator.alloc) paired with defer allocator.free, so dispatch itself allocates nothing and the leak-checking GPA confirms the single free balances the alloc. (For open-ended sets Zig also supports a manual *anyopaque + *const VTable fat pointer, as std.mem.Allocator itself uses.)
use fmt;
use math;
// Hare has no interfaces. Runtime polymorphism is a TAGGED UNION:
// the value carries its variant tag, and 'match' dispatches on it.
type circle = struct { r: f64 };
type rect = struct { w: f64, h: f64 };
type shape = (circle | rect);
fn area(s: *shape) f64 = {
match (*s) {
case let c: circle =>
return math::PI * c.r * c.r;
case let r: rect =>
return r.w * r.h;
};
};
export fn main() void = {
// Manual heap: alloc each shape onto the heap, defer the frees.
let shapes: [2]*shape = [
alloc(circle { r = 2.0 }: shape),
alloc(rect { w = 3.0, h = 4.0 }: shape),
];
defer for (let i = 0z; i < len(shapes); i += 1)
free(shapes[i]); // one free per alloc, at scope exit
for (let i = 0z; i < len(shapes); i += 1)
fmt::printfln("area = {}", area(shapes[i]))!; // match dispatch
};Hare has no interface or class system, so runtime polymorphism uses a tagged union (type shape = (circle | rect)) that area dispatches on with match, which the compiler checks for exhaustiveness. Each shape is placed on the heap with alloc and reclaimed with free (Hare has no GC); defer schedules one free per alloc so every object is released at scope exit no matter how the function returns.
package main
import "core:fmt"
import "core:math"
// Odin has no interfaces. Runtime polymorphism is a tagged 'union':
// the value knows its variant, and a type 'switch' dispatches on it.
Circle :: struct { r: f64 }
Rect :: struct { w, h: f64 }
Shape :: union { Circle, Rect }
area :: proc(s: ^Shape) -> f64 {
switch v in s^ { // type switch over the union's variants
case Circle: return math.PI * v.r * v.r
case Rect: return v.w * v.h
}
return 0 // nil-variant fallthrough
}
main :: proc() {
// new() allocates each Shape via the implicit context.allocator;
// returns ^Shape. defer the frees so every path releases them.
shapes: [2]^Shape
shapes[0] = new(Shape); shapes[0]^ = Circle{r = 2.0}
shapes[1] = new(Shape); shapes[1]^ = Rect{w = 3.0, h = 4.0}
defer for s in shapes do free(s) // one free per new
for s in shapes do
fmt.printf("area = %.4f\n", area(s)) // type-switch dispatch
}Odin offers no interface keyword; the idiom is a tagged union (Shape :: union { Circle, Rect }) that area resolves with a type switch, the union carrying its own variant tag at runtime. Each Shape is allocated with new through the implicit context.allocator (returning ^Shape) and matched by free; defer ... do free(s) runs one free per new - swap the context allocator for an arena and you could drop the per-object frees entirely.
\ Forth has no objects or interfaces. The closest idiom to a vtable
\ is an execution-token (xt) field: each object stores the xt of its
\ own 'area' word, and EXECUTE dispatches it at runtime.
\ Layout per object: cell 0 = xt of area, cell 1.. = the data fields.
0 CELLS CONSTANT .AREA \ field: execution token of the area word
1 CELLS CONSTANT .D0 \ first data field (r, or w)
2 CELLS CONSTANT .D1 \ second data field (h, for a rect)
\ Each 'method' takes the object address and returns its area*100
\ (integer Forth: we scale to avoid floats).
: CIRCLE-AREA ( obj -- n ) .D0 + @ DUP * 314 * ;
: RECT-AREA ( obj -- n ) DUP .D0 + @ SWAP .D1 + @ * 100 * ;
: NEW-CIRCLE ( r -- obj ) \ allocate from the heap word set
2 CELLS ALLOCATE THROW ( r obj )
['] CIRCLE-AREA OVER .AREA + ! ( r obj ) \ store the xt = 'vtable'
TUCK .D0 + ! ; ( obj ) \ store r
: NEW-RECT ( w h -- obj )
3 CELLS ALLOCATE THROW ( w h obj )
['] RECT-AREA OVER .AREA + ! ( w h obj ) \ store the xt
>R R@ .D1 + ! R@ .D0 + ! R> ; \ store h then w, leave obj
: AREA ( obj -- n ) DUP .AREA + @ EXECUTE ; \ fetch xt, dispatch
3 4 NEW-RECT 2 NEW-CIRCLE ( r-obj c-obj )
DUP AREA ." circle area*100 = " . CR
FREE THROW \ free the circle (paired with ALLOCATE)
DUP AREA ." rect area*100 = " . CR
FREE THROW \ free the rect; no GC in ForthForth has no objects, so the nearest idiom to dynamic dispatch is storing an execution token (xt) in each object: cell 0 holds ['] CIRCLE-AREA or ['] RECT-AREA, and the generic AREA word does @ EXECUTE to call whichever method the object carries - a one-slot vtable. Memory comes from the optional ALLOCATE/FREE heap word set (ALLOCATE THROW and FREE THROW check the error code), and each ALLOCATE must be hand-paired with a FREE since there is no GC. (Classic integer Forth has no floats, so areas are scaled by 100 here.)