Inheritance & virtual functions / vtable
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
- virtual enables runtime lookup: Use `virtual` in the base class to allow derived classes to override behavior.
- Polymorphism needs indirection: Virtual functions only work when calling through pointers (`*`) or references (`&`).
Common mistakes
- Forgetting the override keyword: If the base class has `virtual void print() const;` and the derived class writes `void print();` (missing the const), C++ treats this as a completely different function, hiding the base function instead of overriding it. The `override` keyword turns this silent mistake into a loud compiler error.
- Slicing by passing by value: If you write `void playSound(Animal a)`, passing a `Dog` will 'slice' the object, throwing away all the `Dog` data and making a pure `Animal` copy. Virtual functions only work through pointers and references.
Recall questions
- What happens if you call a non-virtual function on a derived object through a base class pointer?
- How does C++ achieve dynamic dispatch at runtime?
- What is the purpose of the override keyword?
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