⚙️ Unit 12 • C++ Systems Engineering

Polymorphism & Virtual Function Architecture

Dynamic and static behavioral dispatch in C++: compile-time polymorphism (function & operator overloading) vs runtime polymorphism (virtual functions & late binding), the vtable/vptr memory mechanism, pure virtual functions, abstract base classes (interfaces), and the virtual destructor contract.

⚡ Static Dispatch: Early Binding at Compile-Time
🎯 Dynamic Dispatch: Late Binding via vptr & vtable
📐 Abstract Interface: Pure Virtual Functions (= 0)
🛡️ Safety: Virtual Destructors in Base Classes
01

Polymorphism Fundamentals: Compile-Time vs Runtime

Polymorphism ("many forms") enables a single interface to control operations across different underlying types. In C++, polymorphism is cleanly split into two distinct execution strategies:

Feature Compile-Time Polymorphism (Static Binding) Runtime Polymorphism (Dynamic Binding)
Mechanisms Function Overloading, Operator Overloading, Templates Virtual Functions, Method Overriding via Base Pointers
Binding Time Compile-time (Early Binding) Runtime execution (Late Binding)
Resolution Speed Instantaneous; zero runtime latency (inlined direct jumps) Slight latency (single pointer dereference through vtable)
Memory Cost Zero per-object memory overhead 8 bytes per object for vptr + shared class vtable
Flexibility Types must be known at compile time Allows heterogeneous runtime plugins and collections
Early Binding (Static Linkage)

The compiler resolves the exact memory address of the target function during compilation. The generated machine code contains a direct assembly call:

call 0x00401120 ; Direct address jump
Late Binding (Dynamic Dispatch)

The exact function address cannot be determined until runtime because a Base* could point to any derived instance. The compiler issues an indirect lookup:

call [rax + 8]   ; Indirect call via vtable
02

Compile-Time Polymorphism: Function & Operator Overloading

Compile-time polymorphism delivers maximum throughput by eliminating runtime branch predictions and pointer indirection overheads:

compile_time_polymorphism.cpp
#include <iostream>

// 1. Function Overloading: Resolved via Compiler Name Mangling
class MathEngine {
public:
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
    int add(int a, int b, int c) { return a + b + c; }
};

// 2. Operator Overloading: Providing native arithmetic syntax for custom types
class Complex {
public:
    double real, imag;

    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

    // Overloading the binary '+' operator
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    void print() const {
        std::cout << real << " + " << imag << "i\n";
    }
};

int main() {
    MathEngine math;
    std::cout << "Int add: " << math.add(10, 20) << "\n";       // Calls add(int, int)
    std::cout << "Double add: " << math.add(2.5, 4.2) << "\n";  // Calls add(double, double)

    Complex c1(3.0, 4.0), c2(1.5, 2.5);
    Complex c3 = c1 + c2; // Equivalent to: c1.operator+(c2);
    std::cout << "Complex sum: ";
    c3.print();

    return 0;
}
03

Virtual Functions & The vtable / vptr Architecture

Prefixing a member function with the virtual keyword instructs the compiler to enable Dynamic Dispatch. When a function is called through a base class pointer or reference, the program resolves the actual derived type of the object at runtime rather than relying on the pointer's static type:

The Virtual Method Table (vtable) Resolution Pipeline:
RAM: Derived Object (0x1000)
[Offset 0]: vptr = 0x7000
[Offset 8]: int derivedData
*vptr added automatically by compiler
Static Memory: vtable for Derived (0x7000)
[Slot 0]: &Derived::render() → 0x00401150
[Slot 1]: &Derived::calculate() → 0x00401180
[Slot 2]: &Derived::~Derived() → 0x00401200
virtual_dispatch_demo.cpp
#include <iostream>
#include <vector>

class BaseRenderer {
public:
    // Virtual function enables runtime late-binding
    virtual void render() const {
        std::cout << "[BaseRenderer] Default render pass.\n";
    }

    // MANDATORY: Virtual Destructor for Polymorphic Base Classes!
    virtual ~BaseRenderer() {
        std::cout << "[BaseRenderer] Destroyed.\n";
    }
};

class VulkanRenderer : public BaseRenderer {
public:
    // C++11 'override' specifier verifies exact matching signature at compile-time
    void render() const override {
        std::cout << "[VulkanRenderer] Executing hardware SPIR-V pipeline.\n";
    }

    ~VulkanRenderer() override {
        std::cout << "[VulkanRenderer] GPU command buffers released.\n";
    }
};

class DirectXRenderer : public BaseRenderer {
public:
    void render() const override {
        std::cout << "[DirectXRenderer] Executing DirectX 12 command queue.\n";
    }

    ~DirectXRenderer() override {
        std::cout << "[DirectXRenderer] Direct3D device released.\n";
    }
};

int main() {
    // Heterogeneous polymorphic collection:
    std::vector<BaseRenderer*> pipeline;
    pipeline.push_back(new VulkanRenderer());
    pipeline.push_back(new DirectXRenderer());

    std::cout << "--- Executing Dynamic Dispatch Loop ---\n";
    for (const BaseRenderer* engine : pipeline) {
        engine->render(); // Dynamically dispatches to Vulkan or DirectX via vtable!
    }

    std::cout << "\n--- Polymorphic Deallocation ---\n";
    for (BaseRenderer* engine : pipeline) {
        delete engine; // Correct derived destructor executed due to virtual destructor!
    }

    return 0;
}
🚨 The Virtual Destructor Mandate

If a class contains at least one virtual function, its destructor MUST be declared virtual (virtual ~Base() {}). Deleting a derived object through a Base* pointer when the base destructor is non-virtual results in Undefined Behavior: the runtime executes only the base class destructor, skipping the derived class destructor entirely and leaking all derived heap allocations!

04

Pure Virtual Functions & Abstract Base Classes (Interfaces)

A Pure Virtual Function is declared with the trailing syntax = 0, signifying that the base class provides no default implementation and mandates that all concrete derived classes must provide an override:

Pure Virtual Syntax

virtual void execute() = 0;

In the class's vtable, the slot for a pure virtual function is populated with a special null or runtime error handler stub (such as __cxa_pure_virtual).

Abstract Class Invariant

Any class containing at least one pure virtual function is an Abstract Class. The compiler forbids instantiating abstract classes directly:

// Shape s; // COMPILE ERROR! Cannot instantiate abstract class
1
Interface Design: class ILogger { virtual void log(msg) = 0; };
Defines pure behavioral contract. Instantiation is blocked at compile time.
2
Concrete Realization: class CloudLogger : public ILogger
Must override log(msg). If skipped, CloudLogger remains abstract and unusable.
3
Polymorphic Decoupling
Client modules accept const ILogger&, completely decoupled from underlying cloud or file storage mechanics.
05

Viva Voce & Engineering Interview Questions

Q1 • Architecture What are the internal mechanics of a vtable and vptr?
The vtable (Virtual Method Table) is a static array of function pointers constructed once per class at compile-time for any class declaring or inheriting virtual functions. Each object instance of such a class contains an invisible pointer called the vptr (usually at offset 0), initialized by the constructor to point to that class's specific vtable. When a virtual function is invoked through a base pointer, the CPU dereferences vptr, indexes into the table, and calls the resolved function pointer in late-binding fashion.
Q2 • Critical Safety Why is a virtual destructor mandatory when deleting derived objects via base class pointers?
If the base class destructor is non-virtual, calling delete basePtr resolves via static early-binding to the base destructor only. The derived class destructor is completely skipped, leaving derived heap buffers, file descriptors, or open mutexes unfreed. Making the destructor virtual ensures that late-binding dynamic dispatch invokes the derived destructor first, followed safely by the base destructor.
Q3 • Language Design What does the C++11 override specifier accomplish, and why is it best practice?
override instructs the compiler to verify that a member function is indeed overriding an identical virtual function in an ancestor base class. If there is a subtle typo in the function name, a mismatched parameter type, or a missing const qualifier, the compiler halts with an explicit error instead of silently creating a new, separate overload that fails to participate in dynamic dispatch.
Q4 • Performance What is the exact performance cost of a virtual function call compared to a non-virtual call?
A non-virtual call executes in a single direct jump (call address). A virtual call requires: (1) loading the object's vptr from memory, (2) indexing into the vtable array, and (3) performing an indirect jump (call [reg + offset]). This introduces memory latency, potential CPU L1 cache misses if the vtable is not cached, and prevents the compiler from performing aggressive inline optimization.
Q5 • Abstract Classes Can an abstract class have a constructor in C++?
Yes! Although an abstract class cannot be instantiated directly by clients, its constructor is executed whenever any derived concrete class instance is constructed (top-down construction sequence). Abstract class constructors are commonly used to initialize base state fields (e.g., IDs, configuration records) shared by all derived classes.
Q6 • Modern C++ What does the final keyword do when applied to a virtual function or class?
When applied to a virtual function (virtual void render() final;), it prevents any subsequent derived classes from overriding that function. When applied to a class (class Engine final { ... };), it prevents the class from being inherited altogether. It also allows the compiler to perform devirtualization, converting virtual calls into direct, inlinable static calls when the concrete type is known.
Q7 • Function Overloading How does the compiler distinguish between overloaded functions with the same name?
During compilation, the compiler performs Name Mangling: it encodes the function name along with the types, order, and count of its parameters into a unique internal symbol name (e.g., add(int, int) becomes _Z3addii). Because the mangled symbol names are unique, the linker binds calls to their exact matching implementations statically.
Q8 • Pure Virtual Implementation Can a pure virtual function (= 0) have a function body definition in C++?
Yes! In C++, a pure virtual function can provide an implementation defined outside the class body (e.g., void Base::clean() { ... }). Derived classes must still explicitly override the function, but can invoke the base implementation explicitly via Base::clean();. This is frequently used for pure virtual destructors (virtual ~Base() = 0;), which must have a body to prevent linker errors during stack unwinding.