Skip to content
C++ Better Explained
Go back

Virtual Functions and Polymorphism in C++: How Runtime Dispatch Actually Works

Edit page

Virtual Functions and Polymorphism in C++: How Runtime Dispatch Actually Works

Introduction: What Is Polymorphism and Why Should You Care?

Polymorphism—literally “many forms”—is one of the most powerful concepts in object-oriented programming. It allows you to write code that works with objects of different types through a single interface, making your code more flexible, maintainable, and extensible.

Imagine you’re building a graphics application with different shapes: circles, rectangles, triangles. Without polymorphism, you’d need separate functions for each shape type. With polymorphism, you can write a single function that draws any shape. This is the magic that virtual functions provide.

In this guide, we’ll build a deep understanding of how C++ virtual functions work, what happens under the hood, and how to use them effectively in your code.

The Problem Without Virtual Functions: Why We Need This Feature

Let’s start with a concrete example. Suppose you want to manage different employee types in a payroll system.

// Without virtual functions - the painful way
class Employee {
public:
    string name;
    double salary;

    Employee(string n, double s) : name(n), salary(s) {}

    void calculateBonus() {
        // Problem: how do we calculate bonus for different types?
        // Hourly employees get different bonuses than salaried
    }
};

class HourlyEmployee : public Employee {
public:
    double hourlyRate;
    HourlyEmployee(string n, double rate)
        : Employee(n, rate * 2000), hourlyRate(rate) {}
};

class SalariedEmployee : public Employee {
public:
    SalariedEmployee(string n, double s)
        : Employee(n, s) {}
};

// Using these classes
int main() {
    Employee* emp1 = new HourlyEmployee("Alice", 25.0);
    Employee* emp2 = new SalariedEmployee("Bob", 80000);

    // How do we know what type each employee is?
    // We'd need type checking and casting - messy!
    if (HourlyEmployee* hourly = dynamic_cast<HourlyEmployee*>(emp1)) {
        // Do hourly-specific bonus calculation
    } else if (SalariedEmployee* salaried = dynamic_cast<SalariedEmployee*>(emp1)) {
        // Do salaried-specific bonus calculation
    }

    return 0;
}

This approach has major problems:

This is exactly where virtual functions shine.

Declaring Virtual Functions: Syntax and Fundamental Rules

Virtual functions allow the correct function to be called based on the actual object type, not the pointer type. Here’s the solution using virtual functions:

// With virtual functions - the elegant way
class Employee {
public:
    string name;
    double salary;

    Employee(string n, double s) : name(n), salary(s) {}

    // Mark with 'virtual' - tells C++ this function can be overridden
    virtual void calculateBonus() {
        cout << name << " gets standard 5% bonus\n";
    }

    virtual double getGrossPay() {
        return salary;
    }

    // Virtual destructor - crucial for polymorphism!
    virtual ~Employee() {
        cout << "Employee " << name << " destroyed\n";
    }
};

class HourlyEmployee : public Employee {
public:
    double hourlyRate;

    HourlyEmployee(string n, double rate)
        : Employee(n, rate * 2000), hourlyRate(rate) {}

    // Override the base class virtual function
    void calculateBonus() override {
        cout << name << " (hourly) gets $" << (hourlyRate * 40) << " bonus\n";
    }

    double getGrossPay() override {
        return salary * 1.15; // Include benefits
    }

    ~HourlyEmployee() {
        cout << "HourlyEmployee " << name << " destroyed\n";
    }
};

class SalariedEmployee : public Employee {
public:
    SalariedEmployee(string n, double s)
        : Employee(n, s) {}

    void calculateBonus() override {
        cout << name << " (salaried) gets " << (salary * 0.10) << " bonus\n";
    }

    double getGrossPay() override {
        return salary + 5000; // Annual bonus included
    }

    ~SalariedEmployee() {
        cout << "SalariedEmployee " << name << " destroyed\n";
    }
};

// Using virtual functions - clean and extensible!
int main() {
    vector<unique_ptr<Employee>> employees;
    employees.push_back(make_unique<HourlyEmployee>("Alice", 25.0));
    employees.push_back(make_unique<SalariedEmployee>("Bob", 80000));
    employees.push_back(make_unique<HourlyEmployee>("Charlie", 30.0));

    // Single loop - automatically calls correct function for each type
    for (auto& emp : employees) {
        emp->calculateBonus();
        cout << "Gross pay: $" << emp->getGrossPay() << "\n";
    }

    return 0;
    // Destructors called automatically for each type
}

Output:

Alice (hourly) gets $1000 bonus
Gross pay: $51000
Bob (salaried) gets 8000 bonus
Gross pay: 85000
Charlie (hourly) gets $1200 bonus
Gross pay: $69000
HourlyEmployee Charlie destroyed
HourlyEmployee Alice destroyed
SalariedEmployee Bob destroyed
Employee Charlie destroyed
Employee Alice destroyed
Employee Bob destroyed

Key Virtual Function Rules

  1. Virtual is inherited: Once you declare a function virtual in the base class, all overrides in derived classes are virtual, even without the virtual keyword
  2. Signature must match: Override functions must have the same name, parameters, and return type (with a covariant return type exception)
  3. Can call base class version: Use BaseClass::functionName() to explicitly call the parent version
  4. Non-virtual functions are not polymorphic: They use compile-time binding

Runtime Dispatch: How C++ Knows Which Function to Call

This is where the magic happens. When you call a virtual function through a base class pointer or reference, C++ must somehow decide at runtime which version to call.

Employee* emp = new HourlyEmployee("Alice", 25.0);
emp->calculateBonus(); // Which calculateBonus gets called?

The type of emp is Employee*, but the actual object is HourlyEmployee. C++ must call the HourlyEmployee version. Here’s how it works:

Compile time: The compiler sees that calculateBonus() is virtual, so it doesn’t generate a direct function call. Instead, it generates code that says “look up the correct function at runtime.”

Runtime: Before calling the function, C++ looks at the actual object type and finds the correct function to call.

This is fundamentally different from non-virtual functions, where the function is determined solely by the pointer type at compile time.

Under the Hood: The Virtual Dispatch Table (vtable) Explained

So how does C++ actually store and access this information? Every class with virtual functions maintains a virtual dispatch table (vtable).

Think of a vtable as a lookup table of function pointers. Each object keeps a hidden pointer (called a vpointer or vptr) to its class’s vtable.

// Here's what happens conceptually:

class Animal {
public:
    virtual void speak() { cout << "Generic sound\n"; }
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!\n"; }
};

class Cat : public Animal {
public:
    void speak() override { cout << "Meow!\n"; }
};

// Behind the scenes, C++ creates these structures:
/*
    AnimalVtable:
    ├─ speak() -> Animal::speak
    └─ ~Animal() -> Animal::~Animal

    DogVtable:
    ├─ speak() -> Dog::speak
    └─ ~Dog() -> Dog::~Dog

    CatVtable:
    ├─ speak() -> Cat::speak
    └─ ~Cat() -> Cat::~Cat

    Every Animal/Dog/Cat object has a hidden vptr that points
    to the correct vtable for its actual type.
*/

int main() {
    Dog dog;
    Animal* animal = &dog;  // Pointer to base class

    // At runtime:
    // 1. Check animal's vptr
    // 2. It points to DogVtable
    // 3. Look up speak() in DogVtable
    // 4. Call Dog::speak()
    animal->speak();  // Prints "Woof!"

    return 0;
}

This vtable mechanism is why virtual functions have a small performance cost—there’s an extra indirection (pointer dereference) at runtime. However, this cost is typically negligible compared to the benefits.

Key vtable facts:

The Override Keyword: Always Use It

In modern C++ (C++11 and later), use the override keyword. It’s not required functionally, but it’s a best practice that helps prevent bugs.

class Animal {
public:
    virtual void speak() {}
    virtual void move(int distance) {}
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    // Good - makes intent clear, compiler checks signature matches
    void speak() override {
        cout << "Woof\n";
    }

    // Compiler ERROR! (without override, would silently create new function)
    // void move(double distance) override { }

    // Correct signature
    void move(int distance) override {
        cout << "Moving " << distance << " meters\n";
    }
};

The override keyword tells the compiler “I intend to override a base class virtual function.” If the signature doesn’t match exactly, the compiler gives an error instead of silently creating a new (non-overriding) function. This prevents subtle bugs.

Pure Virtual Functions and Abstract Classes

Sometimes a base class shouldn’t provide an implementation for a function. It defines the interface that derived classes must implement.

class Shape {
public:
    // Pure virtual function - no implementation
    virtual double area() = 0;
    virtual double perimeter() = 0;

    // Non-pure virtual function - has default implementation
    virtual void describe() {
        cout << "I am a shape\n";
    }

    virtual ~Shape() {}
};

// Circle MUST implement area() and perimeter()
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    double area() override {
        return 3.14159 * radius * radius;
    }

    double perimeter() override {
        return 2 * 3.14159 * radius;
    }
};

// Rectangle MUST implement area() and perimeter()
class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() override {
        return width * height;
    }

    double perimeter() override {
        return 2 * (width + height);
    }
};

int main() {
    // Shape shape; // ERROR! Can't instantiate abstract class

    Circle c(5.0);
    Rectangle r(4.0, 6.0);

    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>(5.0));
    shapes.push_back(make_unique<Rectangle>(4.0, 6.0));

    for (auto& shape : shapes) {
        cout << "Area: " << shape->area() << "\n";
        shape->describe();
    }

    return 0;
}

A class with at least one pure virtual function is abstract—you cannot instantiate it directly. This is powerful for defining interfaces that derived classes must implement.

Virtual Destructors: Why Forgetting Them Causes Bugs

This is critical and often overlooked. Always make destructors virtual in base classes:

// BAD - destructor is non-virtual
class BaseResource {
public:
    BaseResource() { cout << "Base allocated\n"; }
    ~BaseResource() { cout << "Base destroyed\n"; }
};

class DerivedResource : public BaseResource {
private:
    int* buffer;
public:
    DerivedResource() {
        buffer = new int[1000];
        cout << "Derived allocated\n";
    }
    ~DerivedResource() {
        delete[] buffer;  // NEVER CALLED!
        cout << "Derived destroyed\n";
    }
};

int main() {
    BaseResource* res = new DerivedResource();
    delete res;  // MEMORY LEAK!

    // Output:
    // Base allocated
    // Derived allocated
    // Base destroyed
    // (Derived destructor NEVER called - memory leak!)

    return 0;
}

The problem: When delete res; is called, C++ calls the destructor through the base pointer. Since the destructor is non-virtual, it calls BaseResource::~BaseResource() instead of DerivedResource::~DerivedResource(). The derived object’s cleanup never happens.

The fix: Make destructors virtual

// GOOD - virtual destructor
class BaseResource {
public:
    BaseResource() { cout << "Base allocated\n"; }
    virtual ~BaseResource() { cout << "Base destroyed\n"; }
};

class DerivedResource : public BaseResource {
private:
    int* buffer;
public:
    DerivedResource() {
        buffer = new int[1000];
        cout << "Derived allocated\n";
    }
    ~DerivedResource() override {
        delete[] buffer;  // NOW CALLED!
        cout << "Derived destroyed\n";
    }
};

int main() {
    BaseResource* res = new DerivedResource();
    delete res;

    // Output:
    // Base allocated
    // Derived allocated
    // Derived destroyed
    // Base destroyed

    return 0;
}

Rule: If a class has any virtual functions, give it a virtual destructor. Even if the destructor body is empty.

Multiple Inheritance and Virtual Functions: Pitfalls

Multiple inheritance with virtual functions introduces complexity. The most common issue is the diamond problem.

class Animal {
public:
    virtual void speak() { cout << "Generic sound\n"; }
    virtual ~Animal() {}
};

// Diamond inheritance problem
class Pet : virtual public Animal {
public:
    void speak() override { cout << "Pet noise\n"; }
};

class GuardDog : virtual public Animal {
public:
    void speak() override { cout << "Bark!\n"; }
};

class ServiceDog : public Pet, public GuardDog {
    // Which speak() inherited?
    // Without virtual inheritance, we'd have two Animal base objects!
};

int main() {
    ServiceDog dog;
    dog.speak();  // Compiler error - ambiguous!

    return 0;
}

The solution is virtual inheritance: When multiple classes inherit from the same base, use virtual to indicate shared ownership.

class Animal {
public:
    virtual void speak() { cout << "Generic sound\n"; }
    virtual ~Animal() {}
};

class Pet : virtual public Animal {
public:
    void speak() override { cout << "Pet noise\n"; }
};

class GuardDog : virtual public Animal {
public:
    void speak() override { cout << "Bark!\n"; }
};

class ServiceDog : public Pet, public GuardDog {
public:
    void speak() override {
        cout << "Friendly bark\n";
    }
};

int main() {
    ServiceDog dog;
    dog.speak();  // Works! Prints "Friendly bark"
    return 0;
}

With virtual inheritance, there’s only one Animal base object shared by both Pet and GuardDog. The ServiceDog explicitly overrides the ambiguity.

The Final Keyword: Preventing Further Overrides

C++11 introduced final to prevent further overriding:

class Animal {
public:
    virtual void speak() { cout << "Generic sound\n"; }
};

class Dog : public Animal {
public:
    // No class can override this implementation
    void speak() override final {
        cout << "Woof!\n";
    }
};

class SuperDog : public Dog {
public:
    // Compiler ERROR!
    // void speak() override { }
};

// Also prevent inheritance entirely
class Cat final : public Animal {
    // No class can inherit from Cat
};

Use final when you want to prevent subclasses from overriding a function, or when you want to prevent inheritance entirely. It’s useful for security and performance optimization, but use sparingly.

Covariant Return Types: A Subtle Feature

Normally, override functions must have identical return types. However, C++ allows covariant return types—a derived class can return a derived pointer/reference from an overridden function that returns a base pointer/reference.

class Animal {
public:
    virtual Animal* clone() {
        return new Animal(*this);
    }
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    // Can return Dog* instead of Animal*
    Dog* clone() override {
        return new Dog(*this);
    }
};

int main() {
    Dog dog;
    Dog* clonedDog = dog.clone();  // Type-safe - gets Dog* back!

    delete clonedDog;
    return 0;
}

This is safe because the derived type is compatible with the base type. The compiler understands that Dog* is-a Animal*.

Static vs Dynamic Polymorphism: Templates vs Virtual Functions

Virtual functions provide dynamic (runtime) polymorphism. C++ also supports static (compile-time) polymorphism through templates.

// Dynamic polymorphism (virtual functions)
class Shape {
public:
    virtual double area() = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() override { return 3.14159 * radius * radius; }
};

// Static polymorphism (templates)
template<typename T>
double getArea(const T& shape) {
    return shape.area();  // Works with any type that has area()
}

struct TemplateCircle {
    double radius;
    double area() const { return 3.14159 * radius * radius; }
};

int main() {
    Circle c(5.0);
    Shape* shape = &c;
    cout << shape->area() << "\n";  // Dynamic - function lookup at runtime

    TemplateCircle tc{5.0};
    cout << getArea(tc) << "\n";    // Static - resolved at compile-time

    return 0;
}

Dynamic polymorphism (virtual functions):

Static polymorphism (templates):

Use virtual functions when you have a stable hierarchy and need runtime flexibility. Use templates when you want maximum performance and work with different types generically.

Performance Considerations of Virtual Dispatch

Virtual functions have a small performance cost:

  1. vtable lookup: Extra pointer dereference and function pointer indirection
  2. Object overhead: Objects are slightly larger (storing the vptr)
  3. Branch misprediction: Virtual calls can confuse CPU branch prediction

However, modern CPUs and compilers are smart. The cost is usually minimal compared to the benefits. Use virtual functions unless profiling proves they’re a bottleneck.

// Performance perspective
class Shape {
public:
    virtual double area() = 0;  // Virtual call - tiny overhead
    virtual ~Shape() {}
};

// This is still faster than explicit type checking:
class ShapeNoVirtual {
public:
    enum Type { CIRCLE, RECTANGLE } type;

    double area() {
        // Explicit type checking - likely slower due to branching
        if (type == CIRCLE) return circleArea();
        if (type == RECTANGLE) return rectArea();
        return 0;
    }
};

Virtual dispatch uses the CPU’s instruction cache and prediction effectively. Explicit type checking often performs worse.

Practical Example: A Shape Hierarchy Using Polymorphism

Let’s build a complete graphics system:

#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
using namespace std;

class Shape {
protected:
    string name;
public:
    Shape(const string& n) : name(n) {}

    virtual double area() const = 0;
    virtual double perimeter() const = 0;

    virtual void print() const {
        cout << name << " - Area: " << area()
             << ", Perimeter: " << perimeter() << "\n";
    }

    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
    static const double PI;

public:
    Circle(double r) : Shape("Circle"), radius(r) {}

    double area() const override {
        return PI * radius * radius;
    }

    double perimeter() const override {
        return 2 * PI * radius;
    }
};

const double Circle::PI = 3.14159265359;

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h)
        : Shape("Rectangle"), width(w), height(h) {}

    double area() const override {
        return width * height;
    }

    double perimeter() const override {
        return 2 * (width + height);
    }
};

class Triangle : public Shape {
private:
    double a, b, c;

    double semiPerimeter() const {
        return (a + b + c) / 2.0;
    }

public:
    Triangle(double s1, double s2, double s3)
        : Shape("Triangle"), a(s1), b(s2), c(s3) {}

    double area() const override {
        // Heron's formula
        double s = semiPerimeter();
        return sqrt(s * (s - a) * (s - b) * (s - c));
    }

    double perimeter() const override {
        return a + b + c;
    }
};

int main() {
    vector<unique_ptr<Shape>> shapes;

    shapes.push_back(make_unique<Circle>(5.0));
    shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
    shapes.push_back(make_unique<Triangle>(3.0, 4.0, 5.0));
    shapes.push_back(make_unique<Circle>(3.0));

    double totalArea = 0;

    cout << "Shape Summary:\n";
    cout << "==============\n";
    for (const auto& shape : shapes) {
        shape->print();
        totalArea += shape->area();
    }

    cout << "\nTotal area of all shapes: " << totalArea << "\n";

    return 0;
}

Output:

Shape Summary:
==============
Circle - Area: 78.5398, Perimeter: 31.4159
Rectangle - Area: 24, Perimeter: 20
Triangle - Area: 6, Perimeter: 12
Circle - Area: 28.2743, Perimeter: 18.8496

Total area of all shapes: 136.814

This example demonstrates the power of polymorphism: a single loop processes different shapes, with each shape’s methods called appropriately.

Conclusion: Mastering Virtual Functions

Virtual functions are fundamental to object-oriented C++. They enable:

Key takeaways:

  1. Use virtual functions to enable runtime polymorphism
  2. Always make destructors virtual in base classes
  3. Always use the override keyword in derived classes
  4. Understand that vtables power virtual dispatch
  5. Use pure virtual functions to define interfaces
  6. Be aware of virtual function performance cost (usually negligible)
  7. Prefer virtual functions over explicit type checking

Virtual functions represent one of C++‘s greatest strengths. Master them, and you’ll write more robust, maintainable, and extensible code.

Want to deepen your C++ skills? Explore design patterns next—they show you how to structure class hierarchies effectively. What specific polymorphism challenge are you facing in your code?


Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19



Edit page
Share this post on:

Previous Post
How to Fix Undefined Reference Errors in C++ (Linker Errors Explained)
Next Post
How to Create a Lottery Program in C++ - Step by Step with Source Code