Chapter 18 left you with a cliffhanger. We built an Enemy base class and two derived classes, ChaserEnemy and ShooterEnemy, each with its own version of update. Then we put them in a container:
std::vector<Enemy*> enemies;
enemies.push_back(new ChaserEnemy(100.0f, 50.0f));
enemies.push_back(new ShooterEnemy(200.0f, 50.0f));
for (auto* e : enemies) {
e->update(0.016f);
}
Every enemy's update call goes to Enemy::update — the base version — not to the derived one that actually belongs to the object the pointer is pointing at. The chaser doesn't chase. The shooter doesn't shoot. Both drift downward, because that's what the base's default update does. The derived behaviour is still there in memory; it's just never called.
This chapter fixes that. The fix has a name — polymorphism — and it's built on a single keyword, virtual. Once you've got it, everything you built in Chapter 18 starts working properly, and you unlock a way of writing code that's as flexible as anything you've seen so far.
In this chapter, we will:
- Fix the cliffhanger with the
virtualkeyword - Use
overrideto make our intent explicit and catch mistakes - Mark destructors
virtualwhen a class is meant to be derived from - Write pure virtual functions and abstract classes
- Use
finalto close off further inheritance or overrides - Design an interface as a pure abstract class and use it in practice
- Peek under the hood at how dynamic dispatch actually works
- Revisit slicing one last time and draw the full picture
virtual and Dynamic Dispatch
Here's Enemy.h from Chapter 18, with one single change:
#pragma once
class Enemy {
protected:
float x;
float y;
int health;
float speed;
public:
Enemy(float startX, float startY, int startingHealth, float moveSpeed);
void takeDamage(int amount);
bool isAlive() const;
virtual void update(float deltaTime); // Now virtual!
float getX() const;
float getY() const;
};
The one word virtual in front of update changes everything about how the function gets called. Without virtual, the compiler picks which update to call based on the pointer's declared type — always Enemy::update if the pointer is Enemy*. With virtual, the compiler picks based on the actual object's type at runtime. This is called dynamic dispatch, and it's the whole game.
Now the derived class. ShooterEnemy.h gets a small addition too:
void update(float deltaTime) override;
We'll come back to override in a moment. With virtual on the base and a matching function in the derived class, the same loop from Chapter 18 suddenly behaves the way you always wanted it to. Each enemy does its own thing. The vector holds them via base-class pointers, but each object retains its real identity and its real behaviour.
The fancy word for this is polymorphism — literally "many shapes." The same line of code, e->update(...), takes a different shape depending on what e actually points to. One line, many behaviours. That's the promise.
One rule to carry forward: virtual only matters when you call a function through a pointer or a reference to the base class. If you have a concrete ChaserEnemy object and call update on it directly, the compiler already knows exactly what it is, and it just calls ChaserEnemy::update. virtual is about dispatching through a handle that doesn't commit to a specific derived type — exactly the situation a std::vector<Enemy*> puts you in.
override — Making Your Intent Explicit
Here's a question that genuinely comes up: how do you know a derived class's function actually overrides the base's virtual function, and isn't just quietly declaring a new one that happens to have a similar name? Consider this mistake:
class Enemy {
public:
virtual void update(float deltaTime);
};
class ChaserEnemy : public Enemy {
public:
void update(float deltaTime, float targetX, float targetY); // Three args!
};
ChaserEnemy::update does not override Enemy::update. It has a different signature, so the compiler treats it as a completely separate function. The base's update is still there, hidden by name hiding (Chapter 18), and a loop through Enemy* pointers still calls the base version. The compiler says nothing, because you haven't done anything illegal. You've just quietly failed to override the function you thought you were overriding.
The fix is override. You write it after the function's parameter list:
class ChaserEnemy : public Enemy {
public:
void update(float deltaTime, float targetX, float targetY) override; // Compiler error!
};
override tells the compiler "I am overriding a base-class virtual function with this signature — please check." Because the signatures don't match, no override is actually happening, and the compiler rejects the code with a clear error. You get told about your mistake the instant you make it.
Always use override. Every derived class function that's meant to override a base-class virtual function should say so. It costs you nothing, it documents intent, and it catches a whole category of silent bugs.
virtual Destructors
There's a subtle companion rule to virtual, and it matters the first time you write this:
Enemy* e = new ChaserEnemy(100.0f, 50.0f);
// ...
delete e;
We delete a ChaserEnemy through an Enemy* pointer. Which destructor runs? If Enemy's destructor is not virtual, the answer is only Enemy::~Enemy. The ChaserEnemy part of the object is never destroyed — none of its destructor runs, none of its members get cleaned up. This is undefined behaviour in C++, which in practice means memory leaks and half-cleaned objects.
The fix is to mark the base's destructor virtual:
class Enemy {
public:
virtual ~Enemy() = default;
// ...
};
= default tells the compiler "generate the default destructor body for me" — it saves writing virtual ~Enemy() {}. With the destructor now virtual, delete e does the right thing: it calls ChaserEnemy::~ChaserEnemy first, then Enemy::~Enemy, in reverse order.
The rule is simple and worth memorising: if a class is designed to be inherited from and used through base-class pointers, its destructor should be virtual. Every time. The overhead is tiny and the bug you'd otherwise hit is nasty.
Pure Virtual Functions and Abstract Classes
Enemy's default update drifts slowly downward. That was a reasonable starting point in Chapter 18, but does "a generic enemy" make sense as a thing you'd spawn in the game? Probably not. Every real enemy is a chaser, a shooter, a bomber, or some other specific variant. A plain Enemy drifting down is a placeholder — a default that should never actually run.
C++ lets you express this idea directly. A pure virtual function is a virtual function that has no implementation in the base class. The derived class must override it; the base class can't be instantiated as a concrete object. The syntax is = 0 after the function declaration:
class Enemy {
public:
virtual void update(float deltaTime) = 0; // Pure virtual.
virtual ~Enemy() = default;
// non-virtual members (health, takeDamage, etc.) still fine here
};
update is now pure virtual. You can still declare Enemy* pointers and Enemy& references — the class still exists as a type — but you can't create an Enemy object directly:
Enemy e(0.0f, 0.0f, 100, 10.0f); // Error: cannot instantiate abstract class.
A class with at least one pure virtual function is called an abstract class. It exists to define a shape that derived classes must fill in. Any derived class that doesn't override all the pure virtual functions is itself abstract, and also can't be instantiated. Once a derived class has overridden every pure virtual in its inheritance chain, it becomes concrete and can be instantiated.
Pure virtual functions are how you say "every derived class must provide this, and there's no sensible default." They turn a base class from a working-but-generic starting point into a contract — a promise the derived class signs by inheriting.
final
The opposite of "you must override this" is "you must not override this." C++ provides final for both uses.
On a virtual function, final stops any further class from overriding it:
class ChaserEnemy : public Enemy {
public:
void update(float deltaTime) override final; // No further overrides allowed.
};
On a whole class, final stops any inheritance from the class at all:
class ShooterEnemy final : public Enemy {
// no one can inherit from ShooterEnemy.
};
final is used sparingly in practice. The usual reasons are performance (the compiler can sometimes skip virtual dispatch on a final function) and design clarity (you're explicitly saying this is a leaf in the hierarchy). Don't reach for final unless you've got a reason.
Interfaces in C++
C++ doesn't have a separate interface keyword the way some languages do. What it has is a convention: a class made entirely of pure virtual functions, with a virtual destructor and no member data. That's what C++ programmers mean by an interface.
Here's one. Any object that can take damage — an enemy, the player, a destructible crate — might usefully be treated as an IDamageable:
#pragma once
class IDamageable {
public:
virtual void takeDamage(int amount) = 0;
virtual bool isAlive() const = 0;
virtual ~IDamageable() = default;
};
IDamageable has two pure virtual functions and nothing else — no data, no constructors, no concrete methods. Any class that inherits from it must provide takeDamage and isAlive. In return, that class can be used anywhere an IDamageable* or IDamageable& is expected.
The I prefix is a widely used convention for interface classes in C++. It's a hint to anyone reading the code that this class is meant to be implemented, not instantiated or extended with more state.
Now let's make a destructible crate that can take damage even though it isn't an Enemy:
#pragma once
#include "IDamageable.h"
class Crate : public IDamageable {
private:
float x;
float y;
int health;
public:
Crate(float startX, float startY);
void takeDamage(int amount) override;
bool isAlive() const override;
};
Crate inherits from IDamageable and implements the two required functions. It has its own data and its own constructor. It isn't an enemy — it doesn't have update, it doesn't care about a target — but it is a damageable thing, and that's all the interface asks.
For this to work with our enemy family, Enemy should also inherit from IDamageable:
class Enemy : public IDamageable {
// existing Enemy code
public:
void takeDamage(int amount) override;
bool isAlive() const override;
// ...
};
This is interface-style multiple inheritance, and it's the one situation where multiple inheritance in C++ is widely considered fine. The reason it's fine is that IDamageable has no data of its own — there's no diamond problem waiting to happen. You're just stacking a contract on top of a class.
The payoff comes in code that works with damageable things generically:
std::vector<IDamageable*> damageables;
damageables.push_back(new ChaserEnemy(100.0f, 50.0f));
damageables.push_back(new ShooterEnemy(200.0f, 50.0f));
damageables.push_back(new Crate(300.0f, 400.0f));
for (auto* d : damageables) {
if (d->isAlive()) {
d->takeDamage(10);
}
}
We loop over a collection that mixes enemies and a crate. The code doesn't know or care about the difference. It just knows each element can take damage and each element knows whether it's still alive. The interface is the contract; the implementation is each class's own business.
This is the real power of polymorphism. Not just "my enemies update themselves correctly" — though that's nice — but "I can write a system that operates on any damageable object, and add new kinds of damageable objects later without changing a line of that system." Damage handling is written once, against the interface. New concrete classes plug in through inheritance.
How Dynamic Dispatch Works — the vtable
When a class has at least one virtual function, the compiler builds a small lookup table for that class called a vtable (short for virtual table). Each entry in the vtable is a pointer to one of the class's virtual functions. Every object of that class carries an invisible hidden pointer, usually called a vptr, which points at its class's vtable. When you call a virtual function through a base pointer, the compiler generates code roughly equivalent to: "follow the object's vptr, find the entry for this function in the vtable, call the function the entry points at."
Because the vptr is set based on what the object actually is — a ChaserEnemy's vptr points at ChaserEnemy's vtable, a ShooterEnemy's points at ShooterEnemy's — the right function gets called regardless of the pointer's declared type. The lookup happens at runtime, which is where "dynamic dispatch" gets its name.
The costs: a vptr adds a few bytes to every object of a polymorphic class, and every virtual call costs an extra indirection compared to a normal call. For game code these costs are almost always invisible. Don't avoid virtual because you're worried about performance; use it where it fits the design.
Slicing, Revisited
One last pass over the slicing problem from Chapter 18, now that you've got the whole picture.
The rule is: polymorphism only works through pointers or references to the base class. A std::vector<Enemy> still slices derived objects. Passing an Enemy by value still slices. The virtual keyword doesn't change that, because by the time the slicing has happened, the derived part is already gone — the object in the vector slot really is just an Enemy, with an Enemy's vptr, pointing at Enemy's vtable.
To keep the derived identity, you have to keep the object itself intact and pass a handle to it. That's what Enemy* and Enemy& do. Nothing is copied, the original object stays where it is with all its derived parts, and the pointer or reference is what moves around.
So: for collections of polymorphic objects, use std::vector<Enemy*> rather than std::vector<Enemy>. For function parameters that should accept any kind of enemy, use Enemy& or const Enemy& rather than Enemy by value. In real code you'd use std::unique_ptr<Enemy> rather than raw new/delete — smart pointers that handle cleanup automatically.
AI Exercise
"I'm a C++ beginner learning polymorphism and interfaces. I'm designing a pickup system for a 2D game. I want an
ICollectibleinterface that various pickup types will implement —HealthPickup,AmmoPickup,KeyPickup, andCoinPickup. Please sketch: (1) theICollectibleinterface as a header file, with only the pure virtual functions you think belong on it and a virtual destructor, (2) one of the concrete classes (your choice which) as a header file that inherits fromICollectible, and (3) a short explanation of why you chose the specific functions you put on the interface and what you deliberately left off. Keep the interface minimal — only the functions that genuinely need to be overridden by every collectible type."
Notice what the prompt is doing. It names the concrete types so the AI has something to design against, but it leaves the interface open — the AI has to decide what a collectible actually is, in terms of behaviour. The "keep the interface minimal" guidance is the most important part; without it, AI-generated interfaces tend to sprawl. A bad interface piles on seven or eight functions, some of which only make sense for specific pickup types — a getHealAmount() on the interface is a red flag, since not every collectible heals. Push back where it's useful.
Summary
You've got the last big piece of OOP now. virtual functions use dynamic dispatch to call the right derived version based on the object's actual type, not the pointer's declared type. override makes your intent explicit and catches signature mistakes. Virtual destructors keep cleanup honest when derived objects are deleted through base pointers. Pure virtual functions define contracts that derived classes must fulfil, turning a base class into an abstract class. final closes off further inheritance or overrides when you want to. And interfaces — pure abstract classes — let you write systems that operate against a contract rather than a concrete type, so new implementations can plug in without changes to the code that uses them. Beneath it all, the vtable gives you a mental model for how dynamic dispatch actually works.
You now have a complete object-oriented toolkit: classes, encapsulation, inheritance, polymorphism, interfaces. The next project chapter takes the Chapter 19 Entity/Player/Enemy structure and finishes it properly with everything we just learned — virtual, override, virtual destructors, and a pair of pure abstract interfaces that let unrelated classes plug into the same loops.