Coursify

Compiler Design (CD)

Compilation of Object-Oriented Features

35 mins

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:

  1. Field layout: The superclass fields come first, then the subclass fields. This means a Dog* and an Animal* pointing to the same object share the same base address — no pointer adjustment is needed during an upcast.

  2. Inherited methods: Methods defined in Animal can operate on a Dog object because Dog's layout starts with all Animal fields at their expected offsets. The inherited speak() method accesses this + 0 for age, and this works correctly whether this points to an Animal or a Dog.

  3. Method calls for non-virtual methods are resolved at compile time through name mangling — identical to regular function calls:

obj.speak()  →  Animal::speak(&obj)
  1. 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

  1. 1
    Step 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?
  2. 2
    Step 2

    speak() is the first virtual method declared in Animal. The compiler assigns it vtable index 0. Every override of speak() 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 by a."

  3. 3
    Step 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.

  4. 4
    Step 4
    Object typevptr points tovtable[0] containsResult
    AnimalAnimal's vtable&Animal::speakCalls Animal::speak
    DogDog's vtable&Dog::speakCalls Dog::speak
    CatCat's vtable&Cat::speakCalls Cat::speak

    The code that loads from [vptr + 0] is identical in all cases. Only the vptr value (set by the constructor) determines which function runs.

  5. 5
    Step 5
    1Dog* d = new Dog();

    The compiler-generated constructor does:

    1. Allocate memory for a Dog object
    2. Set d->vptr = &Dog's_vtablethis is the key line
    3. Initialize d->age = 0 (if default)
    4. Initialize d->breedCode = 0

    If Dog were further subclassed, the subclass constructor would overwrite this vptr with the subclass vtable. This ensures that a Puppy assigned to an Animal* correctly has Puppy's vtable pointer.

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:

  1. Object (not pointer/reference): obj.method()
  2. Non-virtual method call through a pointer
  3. Explicitly qualified call: obj.Dog::speak()
  4. Final method (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:

  1. The vtable entry for the destructor points to the most-derived destructor
  2. The most-derived destructor runs, then automatically chains to the base destructor
  3. 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

Question 1 of 5
Q1Single choice

In a C++ object with virtual methods, where is the vptr typically stored?

Compilation of Object-Oriented Features | Compiler Design (CD) | Coursify