Compilation of Object-Oriented Features
Understand how modern compilers translate the high-level concepts of object-oriented programming — classes, inheritance, and virtual dispatch — into efficient low-level machine code using virtual function tables (vtables).
Learning Goals
- Explain how class instances (objects) are represented in memory as a contiguous block of field values.
- Describe how single inheritance is compiled — subclass layout extends the superclass layout.
- Define a virtual function table (vtable) and explain how it enables runtime polymorphism (dynamic dispatch).
- Trace a virtual function call at the machine code level, showing the vtable pointer lookup.
- Describe the challenges of compiling multiple inheritance and the diamond problem.
Objects in Memory — The Compiler's View
To a compiler, an object is simply a contiguous block of memory containing its data members (fields) in declaration order. Consider:
1class Point { 2 int x; // offset 0 3 int y; // offset 4 4}; // total size: 8 bytes 5 6class ColorPoint : public Point { 7 int color; // offset 8 8}; // total size: 12 bytes
The compiler assigns each field a fixed byte offset from the start of the object. For Point, x is at offset 0 and y at offset 4. For ColorPoint, x remains at offset 0, y at 4, and the new field color at offset 8.
When the code accesses obj.color, the compiler generates a memory access at base_address + 8 — a compile-time offset with zero runtime overhead. This is fundamentally identical to how struct field access works in C.
The hidden cost: the this pointer.
Every non-static member call obj.method(arg) is rewritten by the compiler as:
Class::method(&obj, arg)
The address of obj is passed as a hidden first parameter called this. Inside the method, every unqualified field access x becomes this->x, which the compiler resolves to this + offset_of_x.
Compiling Single Inheritance
Single inheritance is the simplest case to compile. The subclass layout is a strict extension of the superclass layout:
1class Animal { 2 int age; // offset 0 3 void speak(); 4}; 5 6class Dog : public Animal { 7 int breedCode; // offset 4 (after age) 8 void fetch(); 9};
Key compilation rules for single inheritance:
-
Field layout: The superclass fields come first, then the subclass fields. This means a
Dog*and anAnimal*pointing to the same object share the same base address — no pointer adjustment is needed during an upcast. -
Inherited methods: Methods defined in
Animalcan operate on aDogobject becauseDog's layout starts with allAnimalfields at their expected offsets. The inheritedspeak()method accessesthis + 0forage, and this works correctly whetherthispoints to anAnimalor aDog. -
Method calls for non-virtual methods are resolved at compile time through name mangling — identical to regular function calls:
obj.speak() → Animal::speak(&obj)
- Constructor compilation: The compiler generates a constructor that first calls the superclass constructor (to initialize inherited fields), then initializes its own fields. Destructors run in reverse order.
Polymorphism and the Virtual Function Table (vtable)
When a method is declared virtual in C++ (or methods are virtual by default in Java), the compiler cannot simply call it by name — the correct method depends on the runtime type of the object, not the static type of the pointer.
Consider:
1class Animal { 2 int age; 3 virtual void speak(); // Virtual 4}; 5 6class Dog : public Animal { 7 int breedCode; 8 void speak() override; // Overrides Animal::speak() 9}; 10 11void makeItSpeak(Animal* a) { 12 a->speak(); // Compiler: should this call Animal::speak or Dog::speak? 13} // Answer: depends on what a actually points to — RUNTIME decision!
The compilation challenge: In makeItSpeak, the static type is Animal* but the dynamic type could be Dog, Cat, or any subclass. The compiler must emit code that selects the right method at runtime.
The solution: virtual function tables (vtables).
For every class with at least one virtual method, the compiler creates a vtable — a static array of function pointers, one per virtual method, shared by all instances of that class. Additionally, every object of such a class gets a hidden field at offset 0: the vptr (vtable pointer), which points to its class's vtable.
Note: If the vptr takes 8 bytes (64-bit system), all field offsets shift by 8. age moves from offset 0 to offset 8, etc.
Tracing Dynamic Dispatch: `a->speak()` at the Machine Level
- 1Step 1
1class Animal { 2 int age; // offset 8 (after vptr) 3 virtual void speak(); 4}; 5class Dog : public Animal { 6 int breedCode; // offset 16 7 void speak() override; 8}; 9 10Animal* a = new Dog(); 11a->speak(); // Which speak() executes? - 2Step 2
speak()is the first virtual method declared inAnimal. The compiler assigns it vtable index 0. Every override ofspeak()in any subclass also occupies index 0 in that subclass's vtable.So the call
a->speak()becomes: "call the function at vtable index 0 for the object pointed to bya." - 3Step 3
The compiler transforms
a->speak()into something equivalent to this:1; a is in register R0 2 3; Step 1: Load the vptr from the object (first 8 bytes) 4MOV R1, [R0] ; R1 = a->vptr (points to Dog's vtable) 5 6; Step 2: Load the function pointer from vtable at index 0 7MOV R2, [R1 + 0] ; R2 = vtable[0] (Dog::speak address) 8 9; Step 3: Call the function with 'this' as first argument 10CALL R2, R0 ; Dog::speak(a)This is just two memory loads and a call — extremely fast, and the same code works regardless of the actual type of object in
a. - 4Step 4
Object type vptr points to vtable[0] contains Result AnimalAnimal's vtable &Animal::speakCalls Animal::speakDogDog's vtable &Dog::speakCalls Dog::speakCatCat's vtable &Cat::speakCalls Cat::speakThe code that loads from
[vptr + 0]is identical in all cases. Only the vptr value (set by the constructor) determines which function runs. - 5Step 5
1Dog* d = new Dog();The compiler-generated constructor does:
- Allocate memory for a
Dogobject - Set
d->vptr = &Dog's_vtable← this is the key line - Initialize
d->age = 0(if default) - Initialize
d->breedCode = 0
If
Dogwere further subclassed, the subclass constructor would overwrite this vptr with the subclass vtable. This ensures that aPuppyassigned to anAnimal*correctly hasPuppy's vtable pointer. - Allocate memory for a
1Dog d; 2d.speak(); // Compiler knows d's type at compile time — it's Dog 3 // Generates: Dog::speak(&d) — direct call, NO vtable lookup
Static dispatch = function call resolved entirely at compile time.
When the compiler uses static dispatch:
- Object (not pointer/reference):
obj.method() Non-virtualmethod call through a pointer- Explicitly qualified call:
obj.Dog::speak() Finalmethod (C++11) — resolved at compile time
Generated code: Direct function call CALL Dog::speak — no memory loads, maximum speed.
Virtual Destructors — A Critical Design Rule
In C++, when you delete a derived object through a base pointer, the destructor must be virtual — or only the base destructor runs:
1Animal* a = new Dog(); 2delete a; // If ~Animal() is NOT virtual: only ~Animal() runs → Dog::breedCode LEAKED 3 // If ~Animal() IS virtual: vtable dispatch → Dog::~Dog() → Animal::~Animal()
How the compiler handles virtual destruction:
- The vtable entry for the destructor points to the most-derived destructor
- The most-derived destructor runs, then automatically chains to the base destructor
- Each destructor resets the vptr to its own class vtable before returning (prevents calling a method on a half-destroyed object)
Exam rule of thumb: If a class has any virtual method, its destructor should be virtual. This ensures correct cleanup during polymorphic deletion.
PYQ Solutions — Compilation of OOP Features
Exam Strategy: The 7-Mark OOP Compilation Question
When asked to 'discuss compilation of OOP features,' structure your answer in three clear sections:
1. Inheritance: Describe memory layout (subclass extends superclass, fields appended at end). Explain that upcasting requires no pointer adjustment for single inheritance. Mention compile-time name mangling for non-virtual method calls.
2. Polymorphism: Explain the problem — the compiler cannot determine the method at compile time when a base pointer points to a derived object. Must defer to runtime.
3. Dynamic Dispatch (vtables): This is where most marks are scored. Describe:
- Vtable: one per class, static array of function pointers
- Vptr: hidden field in every object, set by constructor
- Call mechanism:
obj→vptr→vtable[index]→function - Constructor's role in setting the vptr
Include a diagram if possible (Mermaid or hand-drawn memory layout with vptr→vtable arrow). A small assembly-like pseudocode trace scores extra marks. If the question mentions multiple inheritance, add a brief note on the diamond problem and virtual base pointers.
Knowledge Check
In a C++ object with virtual methods, where is the vptr typically stored?