Chapter 20 ended at a wall. Through an Enemy reference, or an Enemy pointer, a chaser didn't chase. It drifted, like any enemy, because the compiler chose which update to call from the type it could see, and all it could see was an Enemy. Chapter 21 lived with the wall. It kept the player and the ghosts apart, each in a variable or a container of her own type, so that each one's own update would run.
This chapter knocks the wall down. One keyword, virtual, asks C++ to choose the function by what the object really is, while the program runs. With it, one list can hold every kind of enemy, one loop can update them all, and chasers chase while shooters shoot. That's polymorphism, from the Greek for "many shapes": one line of code that takes a different shape for each kind of object it meets.
It's also where object-oriented programming comes together. Once functions can be virtual, a base class can be a promise rather than just a starting point, and a class can make several promises at once. Chapter 23 uses all of it to rebuild the runner's world, and this chapter builds it up in the sandbox first, one experiment at a time.
In this chapter, we will:
- Make a function
virtual, so that the object's real type decides which version runs - Mark every override with
override, and let the compiler catch the mistakes - Keep a whole family of enemies in one list, updated by one loop
- Give a base class a virtual destructor, and own the family with
unique_ptr - Look inside a virtual call, at the table that makes it work
- Make a class abstract, with a pure virtual function
- Write interfaces, and let one class implement two of them
- Close off a class with
final, and try an optional AI exercise
Let's start where Chapter 20 stopped.
virtual
Chapter 20's classes are still above main in your Sandbox project: Enemy, ChaserEnemy, and ShooterEnemy, then Vehicle and Car, with every change that chapter made to them. If you've tidied them away since, they're all in Chapter 20. First, run Chapter 20's experiment again, with the target twice as far away. Try this inside main, in place of the other examples:
ChaserEnemy chaser(0.0f, 0.0f);
chaser.setTarget(120.0f, 160.0f);
chaser.update(1.0f);
chaser.print();
Enemy& asEnemy = chaser;
asEnemy.update(1.0f);
chaser.print();
Enemy* pointer = &chaser;
pointer->update(1.0f);
chaser.print();
In the preceding code, the chaser is told to head for 120 across and 160 down, 200 pixels away, and it's updated three times, a second at a time: once as itself, once through an Enemy reference, and once through an Enemy pointer. The console shows this:
at 30, 40, health 50
at 30, 90, health 50
at 30, 140, health 50
In the preceding output, the first update chases, 50 pixels toward the target, which takes the chaser to 30 across and 40 down. The other two drift, 50 pixels straight down each time. Through the reference and the pointer, the compiler could only see an Enemy, so it called Enemy::update. That's Chapter 20's static binding, exactly as before.
Now for the one word. In Enemy, find these lines:
// An ordinary enemy drifts down the screen
void update(float delta)
And change them to this:
// An ordinary enemy drifts down the screen
virtual void update(float delta)
In the preceding code, virtual in front of update says two things. Derived classes may replace this function with versions of their own, and a call to it should go to the version that belongs to the object's real type, whatever type the call is made through. Run the program again, and the console shows this:
at 30, 40, health 50
at 60, 80, health 50
at 90, 120, health 50
In the preceding output, all three updates chase. Each moves the chaser another 30 across and 40 down, toward its target, whether it's called on the chaser itself, through the Enemy reference, or through the Enemy pointer. The object is a chaser, so the chaser's update runs, every time.
Choosing the function by the object's real type, while the program runs, is called dynamic binding, or dynamic dispatch, and it's the opposite of static binding. When the compiler builds asEnemy.update(1.0f), it can't know what kind of enemy the reference will refer to, and with virtual, it doesn't need to. It builds a call that looks the answer up when the line runs, and a later section shows how. Figure 22.1 puts the two kinds of binding side by side.

The chaser's update has the same name and the same parameters as Enemy’s, so it overrides it: it's the chaser's replacement for Enemy’s version. An override is virtual too, whether it says so or not, so ChaserEnemy’s update and ShooterEnemy’s are now both virtual, and so would be the update of any class derived from them. You only have to write virtual once, in the base class.
Notice that the first call didn't change. Called on a ChaserEnemy variable, chaser.update(1.0f) ran the chaser's update before, and it still does, because the compiler could see the real type all along. The word virtual only makes a difference when a function is called through a reference or a pointer to a base, and that's exactly where a family of classes needs it.
override
Overriding has a trap in it, and it's a quiet one. Here's how easily it happens. In ChaserEnemy, find these lines:
// A chaser heads straight for its target
void update(float delta)
And change them to this, as though by a slip of the fingers:
// A chaser heads straight for its target
void update(double delta)
In the preceding code, the chaser's update takes a double rather than a float. It looks almost the same, and it builds. The only complaints are two warnings, C4244: '+=': conversion from 'double' to 'float', possible loss of data, about the two lines that move the chaser, because the arithmetic is now done in doubles and squeezed back into floats. That's true, but it isn't the real problem. Run the program, and the console shows Chapter 20's numbers again: 30, 40, then 30, 90, and then 30, 140.
The chaser's update no longer has the same parameters as Enemy’s, so it doesn't override anything. It's a brand-new function that happens to share a name, and so it hides Enemy’s, as Chapter 20's name hiding showed. Called on the chaser itself, it still works, because a float turns into a double on the way in, and that's why the first line still chases. Through the reference and the pointer, the compiler looks for update in Enemy, finds the virtual one, and finds that ChaserEnemy never replaced it. So the chaser drifts.
That's a nasty kind of bug. Every test that calls the chaser directly passes, and the chaser only misbehaves in the one place that matters, the list of enemies. C++'s answer is the word override, which goes at the end of the declaration. Change the line again, to this:
void update(double delta) override
In the preceding code, override says that this function is meant to override a virtual function in a base class, and asks the compiler to check. It does, and the build stops with C3668: 'ChaserEnemy::update': method with override specifier 'override' did not override any base class methods. The mistake is caught the moment it's made, on the line that made it.
Now put the float back, and keep the override:
void update(float delta) override
In the preceding code, the parameters match Enemy’s again, so the function overrides Enemy::update, and override confirms it. The program builds, and all three updates chase again.
The shooter's update overrides too, so it deserves the same word. In ShooterEnemy, find these lines:
// A shooter drifts like any enemy, and counts down to its next shot
void update(float delta)
And change them to this:
// A shooter drifts like any enemy, and counts down to its next shot
void update(float delta) override
In the preceding code, nothing changes about what the shooter does. The override is a promise, and now the compiler holds the shooter to it.
So virtual goes in the base class, and override goes in the derived class. In older code, you'll sometimes see virtual written on the overrides too, which isn't wrong, but it doesn't ask for the check. The word override says more, and it implies virtual, so it's all a derived class needs.
Write override on every function that's meant to override one, every time. It costs nothing, it tells anyone reading the class that the function replaces one in the base, and it turns a whole family of silent bugs into build errors: a wrong parameter type, a missing const, or a misspelled name, like Update for update. Without it, each of those quietly makes a new function instead.
With the chaser and the shooter both overriding properly, the family is ready to work together.
One List for Every Enemy
Chapter 20's family needed a vector and a loop for every kind of enemy. Now one of each is enough. Try this inside main, in place of the other examples:
ChaserEnemy chaser(0.0f, 0.0f);
ShooterEnemy shooter(100.0f, 0.0f);
Enemy grunt(200.0f, 0.0f, 20, 30.0f);
chaser.setTarget(60.0f, 80.0f);
std::vector<Enemy*> enemies = { &chaser, &shooter, &grunt };
for (Enemy* enemy : enemies)
enemy->update(1.0f);
for (const Enemy* enemy : enemies)
enemy->print();
In the preceding code, there are three enemies of three different kinds: a chaser, heading for 60 across and 80 down, a shooter, and a grunt, which is a plain Enemy. The vector holds their addresses, as Enemy* pointers, filled from a list in braces as in Chapter 13, and one loop updates every one of them through its pointer. A second loop prints them, through pointers to const, from Chapter 10, since printing changes nothing. The console shows this:
at 30, 40, health 50
at 100, 20, health 30
at 200, 30, health 20
In the preceding output, each enemy did its own thing. The chaser chased, 30 across and 40 down, the shooter drifted 20 pixels and counted down its cooldown, and the grunt drifted 30, the way a plain enemy does. The loop didn't know or care which was which. It said enemy->update(1.0f) three times, and virtual sent each call to the right place.
This is the payoff. Add a bomber to the game, and it's one new class, derived from Enemy, with its own update, and its address goes into the same vector. Nothing that loops through the enemies has to change, however many kinds there are.
Notice that the vector doesn't own the enemies. They're ordinary local variables in main, and they're destroyed when main ends, whatever the vector does. The pointers only point at them, just as Chapter 19's player used a texture that runGame owned. In a real game, though, enemies are made while it runs, a wave at a time, and something has to own them. That's the next job, and it needs one more piece first.
Virtual Destructors
Chapter 20 warned that deleting a derived object through a base pointer runs only the base's destructor. Now we can see why, and fix it. Try this inside main, in place of the other examples:
Vehicle* vehicle = new Car();
delete vehicle;
In the preceding code, a Car is made on the heap, with new, and its address is kept in a Vehicle*, since a base pointer can point at a derived object. Then the car is deleted through that pointer. The console shows this:
Vehicle built
Car built
Vehicle scrapped
In the preceding output, the car was built in full, but only its Vehicle part was scrapped. A destructor is a function like any other, and delete vehicle called the one for the type it could see, Vehicle’s, by static binding. The Car part was never taken down. That's undefined behavior. Here, it only loses a message, but if Car owned a texture, or memory of its own, that would never be given back.
The fix is the same word as before. In Vehicle, find this line:
~Vehicle()
And change it to this:
virtual ~Vehicle()
In the preceding code, the destructor is virtual, so delete looks up the object's real type, just as a call to update does. Run the program again, and the console shows this:
Vehicle built
Car built
Car scrapped
Vehicle scrapped
In the preceding output, delete ran Car’s destructor, and then Vehicle’s, from the top down, just as Chapter 20's car was taken apart when it went out of scope. Figure 22.2 shows the two deletes side by side.

A unique_ptr deletes in exactly the same way. It's Chapter 10's owner that cleans up after itself, and it lives in the <memory> header, so add this below #include <vector>:
#include <memory>
In the preceding code, <memory> brings in std::unique_ptr and std::make_unique, which the rest of the chapter uses. Now try this inside main, in place of the other examples:
{
std::unique_ptr<Vehicle> vehicle = std::make_unique<Car>();
std::cout << "Driving" << std::endl;
}
std::cout << "Done" << std::endl;
In the preceding code, std::make_unique<Car>() makes a Car, and hands it back in a std::unique_ptr<Car>, which turns into a std::unique_ptr<Vehicle> as it's stored, just as a Car* turns into a Vehicle*. When the block ends, vehicle goes away, and it deletes what it owns through the only type it knows, Vehicle*. The console shows this:
Vehicle built
Car built
Driving
Car scrapped
Vehicle scrapped
Done
In the preceding output, the whole car was scrapped, because Vehicle’s destructor is virtual. Take the virtual off again, and "Car scrapped" disappears from this output too. A smart pointer doesn't make the delete any smarter. It just makes sure the delete happens. Put the virtual back before you go on.
The enemies are about to be owned the same way, so Enemy needs a virtual destructor too. Add this to the public part of Enemy, below the constructor, with a blank line in between:
// Deleting any kind of enemy through an Enemy* destroys it whole
virtual ~Enemy() = default;
In the preceding code, = default asks the compiler for the destructor it would have written anyway, just as Chapter 18's = default asked for a default constructor, and virtual makes sure that it's the real type's destructor that runs, whatever the enemy is deleted through. An enemy has nothing of its own to clean up, so the body would have been empty, and = default is a tidy way to say so.
Here's the rule to remember: if a class has any virtual function, give it a virtual destructor too. A class with virtual functions is meant to be used through pointers to it, and sooner or later, one of those pointers will be the one that deletes. A virtual destructor costs almost nothing, as a later section shows, and forgetting one is undefined behavior that Visual Studio won't warn you about at its usual settings.
With that in place, the family is ready to be owned.
Owning the Family
Now for a list that owns its enemies, the way a game's would. Try this inside main, in place of the other examples:
std::vector<std::unique_ptr<Enemy>> enemies;
std::unique_ptr<ChaserEnemy> chaser =
std::make_unique<ChaserEnemy>(0.0f, 0.0f);
chaser->setTarget(60.0f, 80.0f);
enemies.push_back(std::move(chaser));
enemies.push_back(std::make_unique<ShooterEnemy>(100.0f, 0.0f));
enemies.push_back(std::make_unique<ShooterEnemy>(200.0f, 0.0f));
In the preceding code, enemies is a vector of std::unique_ptr<Enemy>, so each element owns one enemy, of any kind. The chaser needs its target set before it goes in, and only a ChaserEnemy can do that, so it's made first, in a std::unique_ptr<ChaserEnemy> of its own, split over two lines because it's long. The arrow calls setTarget through it, just as it would through a raw pointer. Then std::move, from Chapter 10, hands the chaser over to the vector, and leaves chaser empty, holding nullptr. The two shooters need nothing set, so each goes straight in, made by std::make_unique inside the call to push_back.
Now update them, and print them. Add this below the other lines:
for (std::unique_ptr<Enemy>& enemy : enemies)
enemy->update(1.0f);
for (const std::unique_ptr<Enemy>& enemy : enemies)
enemy->print();
In the preceding code, the loops go through the vector by reference, because a unique_ptr can't be copied, as Chapter 10 found out with C2280, and a plain loop variable would be a copy. Through each unique_ptr, the arrow reaches the enemy it owns, and virtual does the rest. The console shows this:
at 30, 40, health 50
at 100, 20, health 30
at 200, 20, health 30
In the preceding output, the chaser chased, and both shooters drifted, each through a unique_ptr<Enemy>.
There's no delete anywhere. When main ends, the vector is destroyed, and it destroys each of its unique_ptrs, which delete their enemies through Enemy* pointers, and Enemy’s virtual destructor makes sure each one is destroyed whole. Taking an enemy out in the middle of a game is just as safe. Erase its element, and the unique_ptr deletes the enemy on its way out, which is exactly what Chapter 14's vector of raw pointers couldn't promise.
This is the shape of nearly every game's list of objects: a vector of unique_ptrs to a base class with virtual functions and a virtual destructor, one loop, and each object doing its own thing.
How a Virtual Call Works
A virtual call can't be decided while the program is being built, so how does it find the right function while the program runs? The answer starts with a number. Add this line to the end of main, above return 0;:
std::cout << sizeof(Enemy) << " " << sizeof(ChaserEnemy) << std::endl;
In the preceding code, sizeof measures the two classes, as it did in Chapter 20. Back then, an Enemy was 16 bytes, and a ChaserEnemy 24. Now, below the enemies, the console shows 24 and 32. Each has grown by eight bytes, which Chapter 10 said is the size of a pointer, and nothing we've written accounts for them.
The extra eight bytes are a hidden pointer, which the compiler adds to every object of a class with virtual functions. It's usually called the vptr, short for virtual pointer, and it points at a table called the vtable. The compiler builds one vtable for each class with virtual functions, one per class rather than one per object, and each entry in it is the address of one of the class's virtual functions. The table for Enemy points at Enemy::update and at its destructor, while the one for ChaserEnemy points at ChaserEnemy::update, and at the chaser's own destructor, which the compiler wrote for it.
When an object is made, its constructor sets its vptr to its class's table, so every object carries a pointer to the right table from the moment it exists. A virtual call through a pointer then takes three steps, as Figure 22.3 shows: follow the object's vptr to its table, find the entry for the function, and call whatever that entry points at. The compiler doesn't need to know what kind of enemy is there. The object knows, and its vptr says.

That also explains what virtual can't do. Try Chapter 20's slicing experiment again, inside main, in place of the other examples:
ChaserEnemy chaser(0.0f, 0.0f);
chaser.setTarget(60.0f, 80.0f);
Enemy copy = chaser;
copy.update(1.0f);
copy.print();
In the preceding code, copy is a new Enemy, made from the chaser's Enemy part, exactly as in Chapter 20, and the console still shows "at 0, 50, health 50": the copy drifts. It was built as an Enemy, so its vptr points at Enemy’s table, and virtual faithfully calls Enemy::update. Dynamic binding finds the right function for the object that's really there, and after slicing, what's really there is an ordinary enemy. A reference or a pointer is still the only way to keep a chaser a chaser.
Virtual functions cost a little. Every object carries its vptr, and every virtual call takes a detour through the table, which also stops the compiler from copying a small function's body straight into the call, as it can with ordinary functions. For a few hundred enemies, you'll never measure the difference. For a hundred thousand particles in a tight loop, it can matter, and Chapter 29 looks at how games lay out their data when it does.
Now that you know how a virtual function works, it's time to meet one with no body at all.
Pure Virtual Functions and Abstract Classes
Think back to the grunt from a few sections ago: a plain Enemy, drifting down the screen. Does a game really need plain enemies? Every real enemy is a chaser, a shooter, or a bomber, some particular kind. The class Enemy is the idea they share, rather than a kind of enemy in its own right, and it would be good if C++ could say so.
It can. Suppose every kind of enemy has to have a name, for printing. There's no sensible name for an enemy in general, so Enemy can't provide one, but it can insist that every derived class does. Add this to the public part of Enemy, above print, with a blank line in between:
// Every kind of enemy must say what it's called
virtual const char* name() const = 0;
In the preceding code, = 0 on the end makes name a pure virtual function: a virtual function with no body at all, which every derived class has to override before it can be used. It returns a const char*, which is text in double quotes, as Chapter 11's paths were, and it's const, because saying its name doesn't change an enemy.
Now print can use it. In print, find these lines:
std::cout << "at " << x_ << ", " << y_ << ", health " << health_
<< std::endl;
And change them to this:
std::cout << name() << " at " << x_ << ", " << y_ << ", health "
<< health_ << std::endl;
In the preceding code, print shows the enemy's name before its position. The function print isn't virtual, and doesn't need to be, but it calls name, which is, so the base class's own code calls a function that only the derived classes provide. Behind the scenes, the call is this->name(), through Chapter 18's this pointer, so it's a call through a pointer, and dynamic binding picks the object's own name.
Try it. Add this inside main, in place of the other examples:
ChaserEnemy chaser(0.0f, 0.0f);
ShooterEnemy shooter(100.0f, 0.0f);
chaser.print();
shooter.print();
In the preceding code, a chaser and a shooter are made, and printed. Build, though, and it stops with C2259: 'ChaserEnemy': cannot instantiate abstract class, and the same for 'ShooterEnemy'. The Output window adds notes that say why, among them 'const char *Enemy::name(void) const': is abstract.
A class with at least one pure virtual function is an abstract class, and you can't make an object of it. That now includes ChaserEnemy and ShooterEnemy, because a derived class that doesn't override every pure virtual function it inherits is abstract too. So give each of them a name. Add this to the public part of ChaserEnemy, above setTarget, with a blank line in between:
const char* name() const override
{
return "chaser";
}
In the preceding code, the chaser overrides name, with override on the end, and returns its name. Now add this to the public part of ShooterEnemy, above readyToShoot, with a blank line in between:
const char* name() const override
{
return "shooter";
}
In the preceding code, the shooter does the same. Now both classes override every pure virtual function they inherit, which makes them concrete classes, ones you can make objects of. Build and run, and the console shows this:
chaser at 0, 0, health 50
shooter at 100, 0, health 30
In the preceding output, Enemy’s print has printed each enemy's own name, which it could never have known by itself.
The base class itself is still abstract, and always will be now. Add Enemy grunt(200.0f, 0.0f, 20, 30.0f); inside main, and the build stops with C2259: 'Enemy': cannot instantiate abstract class. Take the grunt out again.
Abstract doesn't mean useless, though. You can't make an Enemy, but you can have Enemy& references, Enemy* pointers, and a vector of unique_ptr<Enemy>, and they're what the family is for. Put the owned list from earlier in the chapter back into main, and it works just as it did, except that each line now starts with "chaser" or "shooter". Figure 22.4 shows what an abstract class allows, and what it doesn't.

There's a bonus, too. Chapter 20's slicing needs an Enemy to copy into, and there can't be one anymore. Try Enemy copy = chaser; with a chaser, and the build stops with the same C2259. An abstract base class can't be sliced into by accident, because the object that slicing would make isn't allowed to exist. Take the copy out again.
A pure virtual function usually has no body, but it can have one, defined outside the class, for the derived classes to call by its full name, as the shooter calls Enemy::update. You'll rarely need that. The common case is the one here: a function the base class insists on, and can't sensibly write itself.
Interfaces
The Enemy class is now abstract, with some data and some code, and one function that every kind of enemy must provide. Take that idea as far as it goes, and you get a class with no data, no code, and nothing but pure virtual functions: a list of promises, and nothing else. That's an interface.
Why would you want one? Because a game is full of things that share an ability without being the same kind of thing.
An enemy can be hurt, but so can a crate, a door, or the player. An enemy needs updating every frame, but so does a timer that sends the next wave, or a platform that slides back and forth. Putting all of those in one family tree would be a mess, because a crate isn't an enemy, and a timer can't be hurt. What they share isn't what they are, but what they can do.
Add these two interfaces above Enemy, with a blank line in between:
// Anything that changes from frame to frame
class IUpdatable
{
public:
virtual ~IUpdatable() = default;
virtual void update(float delta) = 0;
};
// Anything that can be hurt
class IDamageable
{
public:
virtual ~IDamageable() = default;
virtual void takeDamage(int amount) = 0;
virtual bool isAlive() const = 0;
};
In the preceding code, IUpdatable promises one thing, an update, and IDamageable promises two, takeDamage and isAlive. Neither has any member variables, or any code apart from a virtual destructor, which each one needs because objects will be used, and perhaps deleted, through pointers to it. The = default gives each destructor an empty body, so an interface costs nothing to build or take down.
The I at the start of each name is a convention, not a keyword, and C++ has no interface keyword, as some languages do. The habit comes from Microsoft's Component Object Model, or COM, a 1990s system for connecting pieces of Windows software, whose interfaces were all named that way, like IUnknown. DirectX, which many Windows games use for their graphics, is built on COM, so game programmers see the I constantly. Plenty of code leaves it off.
Either way, the idea is the same: a class that says what can be done, and leaves the doing to others.
Now Enemy can make both promises. In Enemy, find this line:
class Enemy
And change it to this:
class Enemy : public IUpdatable, public IDamageable
In the preceding code, Enemy has two base classes, separated by a comma, so an enemy is an IUpdatable and an IDamageable, and can be used as either. That's the multiple inheritance Chapter 20's note warned about, and this is the one safe kind. Each interface is only a list of promises, with no member variables, so there's nothing to be duplicated, and nothing for two bases to disagree about.
Promising isn't the same as doing, though, and Enemy now has to keep its promises. Its takeDamage already does what IDamageable asks, so it only needs to say so. In Enemy, find this line:
void takeDamage(int amount)
And change it to this:
void takeDamage(int amount) override
In the preceding code, override confirms that Enemy’s takeDamage is its version of the one in the interface. The same goes for update. In Enemy, find these lines:
// An ordinary enemy drifts down the screen
virtual void update(float delta)
And change them to this:
// An ordinary enemy drifts down the screen
void update(float delta) override
In the preceding code, update isn't where the virtual function starts anymore. It's IUpdatable’s, and Enemy overrides it. The chaser's and the shooter's versions still override Enemy’s, just as before, so nothing changes about how they behave.
The last promise is isAlive, which is new. Add this to the public part of Enemy, below takeDamage, with a blank line in between:
bool isAlive() const override
{
return health_ > 0;
}
In the preceding code, an enemy is alive for as long as it has health left. Build now, and the family compiles and behaves exactly as it did, with two new ways of being used.
Now for two things that aren't enemies at all. Add this class above Vehicle, with a blank line in between:
// A crate has no health: one hit breaks it
class Crate : public IDamageable
{
public:
void takeDamage(int amount) override
{
if (amount > 0)
broken_ = true;
}
bool isAlive() const override
{
return !broken_;
}
private:
bool broken_ = false;
};
In the preceding code, a Crate is an IDamageable, and nothing else. It has no health, and no position. The first hit of any size breaks it, and it stays broken, so for a crate, being alive just means not being broken. It keeps the interface's promises in its own, completely different way, which is exactly what an interface allows.
Then add this below Crate, with a blank line in between:
const float WAVE_GAP = 2.0f; // seconds between waves of enemies
// Counts down to the next wave of enemies, over and over
class WaveTimer : public IUpdatable
{
public:
void update(float delta) override
{
timeLeft_ -= delta;
if (timeLeft_ <= 0.0f)
{
std::cout << "Here comes the next wave!" << std::endl;
timeLeft_ += WAVE_GAP;
}
}
private:
float timeLeft_ = WAVE_GAP;
};
In the preceding code, WAVE_GAP is the number of seconds between waves. A WaveTimer is an IUpdatable, and nothing else. Each update counts its time down, and when the time runs out, it announces the next wave and starts counting again. It adds WAVE_GAP to the time left, rather than setting it, so that any time left over from the last frame isn't lost. A timer can't be hurt, and it isn't even in the world.
Now put them all to work. Try this inside main, in place of the other examples:
ChaserEnemy chaser(0.0f, 0.0f);
ShooterEnemy shooter(100.0f, 0.0f);
Crate crate;
WaveTimer timer;
chaser.setTarget(60.0f, 80.0f);
std::vector<IUpdatable*> updatables = { &chaser, &shooter, &timer };
std::vector<IDamageable*> damageables = { &chaser, &shooter, &crate };
In the preceding code, there are four objects of four different types, and two vectors of pointers. The first, updatables, holds everything that promised an update: the chaser, the shooter, and the timer. The second, damageables, holds everything that promised to take damage: the chaser, the shooter, and the crate. The chaser and the shooter are in both, because an Enemy is both.
Now add this below the other lines:
// Three frames, a second each
for (int frame = 0; frame < 3; frame++)
{
for (IUpdatable* thing : updatables)
thing->update(1.0f);
}
// A bomb goes off, and everything that can be hurt is hurt
for (IDamageable* thing : damageables)
thing->takeDamage(40);
for (const IDamageable* thing : damageables)
std::cout << thing->isAlive() << " ";
std::cout << std::endl;
In the preceding code, three frames of a second each update everything that can be updated. Then a bomb goes off, and hurts everything that can be hurt by 40, and the last loop prints whether each damageable thing is still alive, as a 1 or a 0. The console shows this:
Here comes the next wave!
1 0 0
In the preceding output, the timer announced a wave after two seconds, in the second frame, and started counting again. The chaser survived the bomb with 10 of its 50 health left, while the shooter, with only 30, didn't, and the crate broke. Neither loop knows what the things in its list really are. Each knows only the one promise that its list is about, and that's all it needs, as Figure 22.5 shows.

Notice, too, that neither list owns anything. The objects belong to main, and the lists are two different views of them: one of the things that change each frame, and one of the things that can be hurt. That's a pattern worth remembering. Chapter 23 uses it to rebuild the runner's world, with one list that owns every runner, and two views, one of everything that updates and one of everything that draws, so that the HUD and the scenery can join the same two loops as the runners.
final
Sometimes a class, or a function, is finished, and shouldn't be built on. The C++ word for that is final, and it has two jobs.
On a class, final means that no class can derive from it. In ShooterEnemy, find this line:
class ShooterEnemy : public Enemy
And change it to this:
class ShooterEnemy final : public Enemy
In the preceding code, final comes straight after the class's name. Now try to derive from it anyway. Add this class below ShooterEnemy, with a blank line in between:
class SniperEnemy : public ShooterEnemy
{
};
In the preceding code, SniperEnemy tries to be a kind of shooter, and the build stops with C3246: 'SniperEnemy': cannot inherit from 'ShooterEnemy' as it has been declared as 'final'. Take SniperEnemy out again.
On a virtual function, final means that no class further down can override it. Write void update(float delta) override final in ChaserEnemy, and you can still derive a class from the chaser. But if that class tries to replace update, the build stops with C3248, which for a class called FastChaser says 'ChaserEnemy::update': function declared as 'final' cannot be overridden by 'FastChaser::update'.
Use final sparingly. Most classes don't need it, and a class that can't be extended can be a nuisance later on. It earns its place when a class or a function really is the end of the line, and it gives the compiler a small gift. When the compiler knows that nothing can override a function, it can sometimes call the function directly, without the detour through the vtable.
AI Exercise (Optional)
Designing an interface is almost all judgment. The syntax is easy: pure virtual functions, a virtual destructor, and no data. The hard part is deciding what belongs in it, and what doesn't, and that's a good thing to practice with an AI's help.
As always, use a regular chatbot, such as Claude, ChatGPT, or Gemini, in its ordinary chat window. Open it, and try this prompt:
"I'm learning C++, and I've just learned virtual functions, override, virtual destructors, pure virtual functions, abstract classes, and interfaces made of pure virtual functions, as well as std::unique_ptr. I'm designing the pickups for a 2D game: a HealthPickup, an AmmoPickup, a KeyPickup, and a CoinPickup. Please write an ICollectible interface, in a header, with only the pure virtual functions that every kind of pickup genuinely needs, and a virtual destructor. Then write one of the pickups as a class that implements it, and show how a game would keep every pickup in a std::vector<std::unique_ptr<ICollectible>>. Finally, explain why each function is on the interface, and what you deliberately left off. Keep the interface as small as you can, and put each curly brace on its own line."
Notice what the preceding prompt does. It says what you know, so the answer stays within it. It names four kinds of pickup, so the AI has something to design against, but it leaves the interface itself open. And "as small as you can" is the most important phrase in it.
Left alone, AI-written interfaces tend to sprawl, with getters, setters, and helper functions piling up until the interface isn't really an interface anymore, just a class with extra steps. Asking for the reasons makes the AI defend its choices, rather than just produce code.
When the answer comes back, check it against this chapter. Is the interface all pure virtual functions, with a virtual destructor, and no member variables? Does the pickup class put override on every function it overrides? A good ICollectible might need only two or three functions, such as an onCollect that takes the player, and an isCollected. A getHealAmount on the interface is a red flag, because not every pickup heals, and a function that only one kind can answer belongs to that kind.
Then push back. Ask: "Does every collectible really need a position? What about a reward for finishing a level, which isn't in the world at all?" Either the AI defends its choice, and you learn why, or it changes its mind, and you learn that too. Knowing what to leave off an interface is one of the most useful skills in object-oriented design, because it's easy to add a function to a small interface later, and painful to take one out of a big one.
Summary
You've knocked down the wall that Chapter 20 ran into. A virtual function is chosen by the object's real type, while the program runs, so one list of base pointers, or of unique_ptrs, can hold a whole family, and one loop can tell each of them to do its own thing. The word override asks the compiler to check that a function really replaces one in its base, and turns silent mistakes into build errors. A virtual destructor makes sure that an object deleted through a base pointer is destroyed whole. Underneath it all, each object's vptr points at its class's vtable, which is how a call finds its function, and it's also why a sliced copy, built as a base object, behaves like one.
A pure virtual function makes a base class abstract, so it can't be made, or sliced into, but it still gives the family its shape, and an interface takes that idea to its end, as a class of nothing but promises. A class can keep several promises at once, the one safe use of multiple inheritance, and final closes a class or a function off when it's truly finished. In the next chapter, the finale of Act 3, the runner's world is rebuilt on these ideas: every runner in one list that owns them, with the HUD and the scenery beside them, all updated and drawn by the same two loops. After that, Chapter 24 heads out into the C++ wilderness, to the features this book hasn't needed yet.
