Inheritance & virtual functions / vtable

RoadmapsC++

Scenario

You create a `Dog` object and pass it by reference to a function expecting an `Animal&`. When the function calls `makeSound()`, it prints generic animal noises instead of a bark.

Why did the compiler ignore the fact that the object was actually a Dog?

Mental model

By default, C++ only looks at the label on the box (the pointer's type) to decide what function to call, because reading labels is fast. The `virtual` keyword tells the compiler: 'Stop, open the box, check what's actually inside at runtime, and call the correct function.'

In C++, method binding happens at compile-time (static binding) by default. If you call a method through a base class pointer or reference, the compiler rigidly calls the base class's version. To enable polymorphism (dynamic binding), you must mark the base class method as `virtual`. This instructs the compiler to use a hidden table (the vtable) at runtime to route the call to the derived class's implementation.

Explanation

Inheritance allows a class (`Dog`) to absorb the data and behavior of a base class (`Animal`). But the entire point of OOP is polymorphism: writing code that works with `Animal*` but executes the specific logic of `Dog` or `Cat`.

Without the `virtual` keyword, C++ uses 'early binding'. The compiler sees `Animal* a` and says, 'I will compile a hardcoded jump to `Animal::makeSound()`.' It doesn't care that the pointer might actually point to a `Dog` at runtime.

When you add `virtual` to the base class method, you enable 'late binding'. Under the hood, the compiler gives the class a hidden pointer (the `vptr`) pointing to a Virtual Method Table (the `vtable`). At runtime, when `a->makeSound()` is called, the program looks up the `vptr`, follows it to the `Dog` vtable, and executes `Dog::makeSound()`. This costs a tiny amount of performance (an extra memory indirection) but enables true polymorphism.

When overriding a virtual function in a derived class, always use the `override` keyword (C++11). It forces the compiler to check that you actually matched the base class signature perfectly. Without it, a slight typo in the signature creates a brand new function instead of overriding the base one.

Crucially, polymorphism strictly requires passing objects by pointer (`*`) or reference (`&`). If you pass an object by value (`void playSound(Animal a)`), the derived object (`Dog`) is 'sliced' down to just its base `Animal` parts, and dynamic dispatch will not occur.

Code examples

The bug: Static binding misses the override

#include <iostream>

class Animal {
public:
    void makeSound() const {
        std::cout << "Generic animal sound\n";
    }
};

class Dog : public Animal {
public:
    void makeSound() const {
        std::cout << "Bark!\n";
    }
};

void playSound(const Animal& a) {
    a.makeSound(); // Compiler sees 'Animal&', binds to Animal::makeSound()
}

int main() {
    Dog myDog;
    playSound(myDog); // Prints "Generic animal sound"
    return 0;
}

Because `makeSound` is not `virtual`, the decision of which function to call is made at compile-time based purely on the reference type `Animal&`. The `Dog` implementation is ignored.

The fix: virtual and override

#include <iostream>

class Animal {
public:
    // 'virtual' enables dynamic dispatch via the vtable
    virtual void makeSound() const {
        std::cout << "Generic animal sound\n";
    }
};

class Dog : public Animal {
public:
    // 'override' ensures we exactly match the virtual signature
    void makeSound() const override {
        std::cout << "Bark!\n";
    }
};

void playSound(const Animal& a) {
    a.makeSound(); // Runtime lookup finds Dog::makeSound()
}

int main() {
    Dog myDog;
    playSound(myDog); // Prints "Bark!"
    return 0;
}

The `virtual` keyword creates the vtable. At runtime, the program detects that `a` actually references a `Dog` and dynamically routes the call to the correct override.

Key points

Common mistakes

Recall questions

Questions & answers

A candidate's code passes a `Dog` to a function `void process(Animal a)`. The `makeSound` method is correctly marked `virtual`. What will happen when `a.makeSound()` is called?

The object is passed by value, which causes 'object slicing'. The `Dog` is sliced down into a pure `Animal` copy. `Animal::makeSound()` will be called. Polymorphism only works via pointers (`*`) or references (`&`).

Approach: Identify object slicing and the requirement of pointers/references for dynamic dispatch.

Does adding a virtual function to a class increase the size of its objects?

Yes. The compiler adds a hidden pointer (the vptr) to every object of the class to point to the class's vtable. On a 64-bit system, this typically increases the object's size by 8 bytes.

Approach: Understand the memory overhead associated with the vtable implementation.

Continue learning

Previous: Classes, constructors, destructors & this

Next: Abstract classes & pure virtual functions

Return to C++ Roadmap