Understanding C++ Object Layout and Simulating Polymorphism in C

Memory Composition and Inheritance

At the compiler level, C++ classes are fundamentally organized as contiguous memory blocks, closely resembling C structures. When inheritance is introduced, the memory layout of a derived class is constructed by appending its own member variables directly after the inherited base class members. This linear stacking ensures that a pointer to a derived object can be safely treated as a pointer to its base type without breaking memory alignment or access patterns.

The Mechanics of Virtual Dispatch

Runtime polymorphism in C++ relies on an implicit lookup mechanism managed entirely by the compiler. When a class declares at least one virtual method, the compiler injects a hiddan pointer (commonly called the vptr) into the object's memory layout. This pointer references a statically generated virtual function table (vtable), which is essentially an array of function pointers. Each entry in the table corresponds to a virtual method, and derived classes receive their own vtables where overridden methods replace the base addresses. Because the vtable is resolved at runtime through the vptr, virtual calls introduce a slight indirection overhead compared to direct function invocation.

Inspecting Object Memory and Vpointers

The following example demonstrates how the vptr affects object size and how raw memory casting can be used to observe and manipulate the underlying layout.

#include <iostream>

class BaseEntity {
protected:
    int alpha;
    int beta;
public:
    virtual void display() {
        std::cout << "alpha: " << alpha << ", beta: " << beta << std::endl;
    }
};

class ExtendedEntity : public BaseEntity {
private:
    int gamma;
public:
    ExtendedEntity(int a, int b, int c) {
        alpha = a;
        beta = b;
        gamma = c;
    }
    void display() override {
        std::cout << "alpha: " << alpha << ", beta: " << beta << ", gamma: " << gamma << std::endl;
    }
};

struct RawLayout {
    void* vptr;
    int alpha;
    int beta;
    int gamma;
};

int main() {
    std::cout << "BaseEntity size: " << sizeof(BaseEntity) << std::endl;
    std::cout << "ExtendedEntity size: " << sizeof(ExtendedEntity) << std::endl;

    ExtendedEntity obj(10, 20, 30);
    RawLayout* raw = reinterpret_cast<RawLayout*>(&obj);

    std::cout << "--- Initial State ---" << std::endl;
    obj.display();

    raw->alpha = 100;
    raw->beta = 200;
    raw->gamma = 300;

    std::cout << "--- Modified via Raw Layout ---" << std::endl;
    obj.display();

    return 0;
}

Replicating Object-Oriented Pattterns in C

C lacks native support for classes, but the three pillars of object-oriented design can be manually engineered using standard language features:

  • Encapsulation: Achieved by exposing only opaque pointers (void*) in public headers, hiding internal structure definitions in implementation files.
  • Inheritance: Simulated by embedding the parent structure as the very first member of the child structure, guaranteeing identical initial memory offsets.
  • Polymorphism: Implemented by manually constructing vtables using structures of function pointers, linking them to instances during initialization, and routing calls through these pointers.

Public Interface Definition

#ifndef CALC_API_H
#define CALC_API_H

typedef void CalcBaseRef;
typedef void CalcAdvRef;

CalcBaseRef* CalcBase_New(int x, int y);
int CalcBase_GetX(CalcBaseRef* self);
int CalcBase_GetY(CalcBaseRef* self);
int CalcBase_Compute(CalcBaseRef* self, int offset);
void CalcBase_Delete(CalcBaseRef* self);

CalcAdvRef* CalcAdv_New(int x, int y, int z);
int CalcAdv_GetZ(CalcAdvRef* self);
int CalcAdv_Compute(CalcAdvRef* self, int offset);

#endif

Implementation and Vtable Simulation

#include "calc_api.h"
#include <stdlib.h>

static int Base_ComputeImpl(void* self, int offset);
static int Adv_ComputeImpl(void* self, int offset);

typedef struct {
    int (*compute)(void*, int);
} CalcVTable;

struct CalcBase {
    const CalcVTable* vtable;
    int x;
    int y;
};

struct CalcAdv {
    struct CalcBase base;
    int z;
};

static const CalcVTable Base_VTable = { Base_ComputeImpl };
static const CalcVTable Adv_VTable  = { Adv_ComputeImpl };

CalcBaseRef* CalcBase_New(int x, int y) {
    struct CalcBase* obj = (struct CalcBase*)malloc(sizeof(struct CalcBase));
    if (obj) {
        obj->vtable = &Base_VTable;
        obj->x = x;
        obj->y = y;
    }
    return (CalcBaseRef*)obj;
}

int CalcBase_GetX(CalcBaseRef* self) { return ((struct CalcBase*)self)->x; }
int CalcBase_GetY(CalcBaseRef* self) { return ((struct CalcBase*)self)->y; }

static int Base_ComputeImpl(void* self, int offset) {
    struct CalcBase* obj = (struct CalcBase*)self;
    return obj->x + obj->y + offset;
}

int CalcBase_Compute(CalcBaseRef* self, int offset) {
    struct CalcBase* obj = (struct CalcBase*)self;
    return obj->vtable->compute(self, offset);
}

void CalcBase_Delete(CalcBaseRef* self) { free(self); }

CalcAdvRef* CalcAdv_New(int x, int y, int z) {
    struct CalcAdv* obj = (struct CalcAdv*)malloc(sizeof(struct CalcAdv));
    if (obj) {
        obj->base.vtable = &Adv_VTable;
        obj->base.x = x;
        obj->base.y = y;
        obj->z = z;
    }
    return (CalcAdvRef*)obj;
}

int CalcAdv_GetZ(CalcAdvRef* self) { return ((struct CalcAdv*)self)->z; }

static int Adv_ComputeImpl(void* self, int offset) {
    struct CalcAdv* obj = (struct CalcAdv*)self;
    return obj->z + offset;
}

int CalcAdv_Compute(CalcAdvRef* self, int offset) {
    struct CalcAdv* obj = (struct CalcAdv*)self;
    return obj->base.vtable->compute(self, offset);
}

Execution and Polymorphic Dispatch

#include <stdio.h>
#include "calc_api.h"

void execute_compute(CalcBaseRef* instance, int val) {
    int res = CalcBase_Compute(instance, val);
    printf("Computed result: %d\n", res);
}

int main() {
    CalcBaseRef* b = CalcBase_New(5, 10);
    CalcAdvRef* d = CalcAdv_New(2, 4, 8);

    printf("Base direct: %d\n", CalcBase_Compute(b, 3));
    printf("Adv direct: %d\n", CalcAdv_Compute(d, 3));

    execute_compute(b, 3);
    execute_compute((CalcBaseRef*)d, 3);

    CalcBase_Delete(b);
    CalcBase_Delete((CalcBaseRef*)d);
    return 0;
}

Architectural and Performance Considerations

Manual vtable construction in C mirrors exactly what C++ compilers automate behind the scenes. Inheritance reduces to predictable memory concatenation, while polymorphism depends entirely on function pointer indirection. Because virtual dispatch requires fetching the vptr, reading the table, and jumping to the resolved address, it prevents inlining and introduces a measurable runtime cost compared to statically bound member functions. Understanding this layout clarifies why cross-language ABI compatibility often relies on pure virtual interfaces and why embedded systems sometimes avoid heavy virtual hierarchies in favor of explicit function pointer tables.

Tags: cpp-memory-model vtable-internals c-oop-simulation virtual-dispatch compiler-code-generation

Posted on Mon, 24 Aug 2026 16:02:42 +0000 by nikky_d16