Chapter 20 · ~25 min read

Inheritance

Chapter 19's runner has an Animator. That's composition: one class built from another, and the relationship Chapter 18 called has-a. This chapter is about the other big relationship between classes, the one where a class is a more particular kind of another.

Think of the enemies in a game. There's rarely just one kind. A chaser runs straight at the player, a shooter hangs back and fires, and a bomber dives in and explodes. All three are enemies: each has a position, some health, and a speed, each can take damage, and each needs updating every frame. But each moves in its own way.

You could write three separate classes, and repeat the position, the health, and the damage in every one, but then every change to how an enemy takes damage has to be made three times, and one of them will be forgotten. Inheritance is C++'s answer. You write the shared part once, in one class, and every kind of enemy inherits it, and adds only what makes it different.

In this chapter, we will:

  • Meet inheritance, and the is-a test that says when to use it
  • Write a base class, and build a family of enemies on it
  • Share data with the family through protected
  • Pass arguments to a base class's constructor, and see the order objects are built and taken apart in
  • Build on a base class's function without repeating it, and avoid the trap of calling yourself
  • Meet name hiding, and bring a hidden name back with using
  • Find out which function runs when a derived object is used through its base, and what slicing loses
  • Choose between inheritance and composition, and try an optional AI exercise

The Is-A Relationship

Inheritance says that one class is a more particular version of another. A chaser is an enemy, a shooter is an enemy, and a sports car is a car. This is called the is-a relationship, and it's the test to apply before you reach for inheritance.

In C++, the general class is called the base class, and the more particular one is a derived class, which inherits from the base. You'll also hear them called the parent class and the child class, or the superclass and the subclass. Several classes can inherit from the same base, which makes them siblings, and a derived class can be the base of another in turn, so a family of classes forms a tree, like the one in Figure 20.1.

The enemy family. Enemy, the base class, holds what every enemy has and does. ChaserEnemy and ShooterEnemy inherit all of it, and each adds only what makes it different: a target to chase, or a cooldown between shots.
Figure 20.1 — The enemy family. Enemy, the base class, holds what every enemy has and does. ChaserEnemy and ShooterEnemy inherit all of it, and each adds only what makes it different: a target to chase, or a cooldown between shots.
Tip

Before you inherit, say the sentence out loud: "A ChaserEnemy is an Enemy." If it's true, and it would still be true everywhere the program uses an enemy, inheritance fits. If the honest sentence is "has an", as in "a player has an animator", you want a member instead, which is Chapter 19's composition. And if it's neither, the two classes don't belong together at all.

That's enough talk. Let's build the family.

A Base Class

As in the other theory chapters, the examples are experiments for your Sandbox project. Chapter 18 left several classes in main.cpp, one of them an Enemy of its own, so start this chapter with a clean file. Delete everything in main.cpp, and type this in its place:

#include <iostream>
#include <cmath>
#include <vector>

int main()
{
    return 0;
}

In the preceding code, <iostream> is there for printing, as usual, <cmath> is for a square root we'll need shortly, and <vector> is for a list of enemies, later on. The Player.h and Player.cpp files from Chapter 18 can stay where they are, because nothing in this chapter clashes with them.

Now the base class. Add this above main, with a blank line in between:

class Enemy
{
public:
    Enemy(float x, float y, int health, float speed)
        : x_(x), y_(y), health_(health), speed_(speed)
    {
    }

    void print() const
    {
        std::cout << "at " << x_ << ", " << y_ << ", health " << health_
                  << std::endl;
    }

protected:
    float x_;
    float y_;
    int health_;
    float speed_;
};

In the preceding code, Enemy looks like the classes from Chapter 18. Its constructor sets its four members in an initializer list, in the order they're declared, and print shows where the enemy is, and how healthy, for our experiments. The line that sends it all to std::cout is too long for the page, so it carries on below, lined up under the first <<.

The one new thing is the word above the member variables: protected, rather than private. A protected member is private to the rest of the program, but open to derived classes. Chapter 18 mentioned the word, and now it earns its place: every kind of enemy is going to need to move itself, so every kind needs to reach x_ and y_, and nothing outside the family does.

Next, movement. Add this to the public part of Enemy, below the constructor, with a blank line in between:

// An ordinary enemy drifts down the screen
void update(float delta)
{
    y_ += speed_ * delta;
}

In the preceding code, update moves the enemy down the screen by its speed times the delta time, the way everything has moved since Chapter 1. An ordinary enemy doesn't do anything cleverer than that.

Next, damage. Add this to the public part of Enemy, below the constructor, above update, with a blank line in between:

void takeDamage(int amount)
{
    health_ -= amount;
    if (health_ < 0)
        health_ = 0;
}

In the preceding code, takeDamage takes the damage off the enemy's health, and makes sure it never goes below zero. That's the sort of rule inheritance is for: written once, here, and shared by every kind of enemy there will ever be.

That rule also shows the price of protected. Because health_ is protected, a derived class could set it to -50 directly, and skip the rule altogether. A protected member is a door in Chapter 18's wall, open to the whole family, so only make protected what derived classes genuinely need. Here, they need to move, so the position and speed are protected, and it's simplest to leave health beside them, but a derived class should still change it through takeDamage.

Try the class on its own first. Add this inside main, above return 0;:

Enemy grunt(100.0f, 0.0f, 20, 30.0f);
grunt.update(1.0f);
grunt.print();

In the preceding code, grunt starts at 100 across and 0 down, with 20 health and a speed of 30 pixels a second. Updating it with a delta time of one second moves it 30 pixels down, and the console shows "at 100, 30, health 20". Using a whole second makes the numbers easy to check.

A Derived Class

Now a chaser: an enemy that heads straight for a target. Add this class above main, below Enemy:

class ChaserEnemy : public Enemy
{
public:
    ChaserEnemy(float x, float y) : Enemy(x, y, 50, 50.0f)
    {
    }

    void setTarget(float x, float y)
    {
        targetX_ = x;
        targetY_ = y;
    }

private:
    float targetX_ = 0.0f;
    float targetY_ = 0.0f;
};

In the preceding code, the first line says it all: : public Enemy after the class's name makes ChaserEnemy a derived class of Enemy. Everything Enemy has, a ChaserEnemy has too, without a word of it repeated: the four member variables, takeDamage, update, and print. The word public in front of Enemy means that everything public in Enemy stays public in a ChaserEnemy, so the rest of the program can call takeDamage on a chaser, just as it could on an enemy. That's almost always what you want.

C++ also has protected and private inheritance, which hide the base's public members from the rest of the program. They describe a class that's built with another, rather than one that is another, and you'll rarely meet them outside library code.

Then the constructor. A chaser only needs its position, because every chaser has 50 health and a speed of 50, and it passes all four values up to Enemy’s constructor in its initializer list, with Enemy(x, y, 50, 50.0f). That's how a derived class gets its base part built: it names the base class in its list, just as it would name a member, with the arguments in parentheses. Leave that out, and C++ tries to build the Enemy part with Enemy’s default constructor, which doesn't exist, so the build stops with C2512: 'Enemy': no appropriate default constructor available.

The rest is new, and belongs to chasers alone: a target to head for, with a function to set it, and two private members to hold it, starting at 0. They're private, not protected, because no class is going to derive from ChaserEnemy.

Now for the chasing. Add this to the public part of ChaserEnemy, below setTarget, with a blank line in between:

// A chaser heads straight for its target
void update(float delta)
{
    float dx = targetX_ - x_;
    float dy = targetY_ - y_;
    float distance = std::sqrt(dx * dx + dy * dy);
    if (distance > 0.0f)
    {
        x_ += dx / distance * speed_ * delta;
        y_ += dy / distance * speed_ * delta;
    }
}

In the preceding code, the chaser gets an update of its own, which works out its own move, and the members it uses, x_, y_, and speed_, are Enemy’s. It can use them directly, because they're protected. Had they been private, even a derived class couldn't touch them, and the build would stop with C2248: 'Enemy::x_': cannot access private member declared in class 'Enemy'.

The math is new, so let's take it slowly. First, dx and dy are how far the target is across and down. The straight-line distance to it is the long side of a right-angled triangle with sides dx and dy, so by Pythagoras' theorem, it's the square root of dx squared plus dy squared, and std::sqrt, from <cmath>, takes the square root.

Dividing dx and dy by the distance gives the direction to the target as two numbers, one across and one down, that make a line exactly one pixel long. Turning a direction into a length of one like that is called normalizing it. Multiply it by the speed and the delta time, and the chaser moves exactly speed_ pixels a second, straight at the target, whatever the angle. The if stops it from dividing by zero once it's arrived.

Try it. Add this inside main, in place of the other examples:

ChaserEnemy chaser(0.0f, 0.0f);
chaser.setTarget(60.0f, 80.0f);
chaser.update(1.0f);
chaser.print();

In the preceding code, the target is 60 across and 80 down, so the distance is 100, and the direction is 0.6 across and 0.8 down. At 50 pixels a second, the chaser covers half the distance in one second, and the console shows "at 30, 40, health 50". Notice that print is Enemy’s, inherited without a line of code.

Now add this below the other lines:

chaser.takeDamage(20);
chaser.print();

In the preceding code, takeDamage is inherited too, so the chaser's health drops from 50 to 30, with the same rule as every other enemy. Try chaser.x_ = 5.0f; in main, though, and the build stops with C2248: 'Enemy::x_': cannot access protected member declared in class 'Enemy'. Protected members are open to the family, not to the rest of the program. Take that line out again.

What a Derived Object Is Made Of

A derived object contains its base. Every ChaserEnemy has a whole Enemy inside it, with all four of its members, and the chaser's own members after them, as Figure 20.2 shows. Add this line to the end of main, above return 0;, to see it:

std::cout << sizeof(Enemy) << " " << sizeof(ChaserEnemy) << std::endl;

In the preceding code, sizeof gives the number of bytes an object of a type takes up, and the console shows 16 and 24. An Enemy is three floats and an int, at four bytes each, and a ChaserEnemy is those 16 bytes, plus eight more for its two targets.

A ChaserEnemy contains an Enemy. Its first 16 bytes are the Enemy part, with the four members every enemy has, and its own two members follow, making 24 bytes in all. That's why a chaser can be used anywhere an enemy can.
Figure 20.2 — A ChaserEnemy contains an Enemy. Its first 16 bytes are the Enemy part, with the four members every enemy has, and its own two members follow, making 24 bytes in all. That's why a chaser can be used anywhere an enemy can.

A derived class gets its base's member variables and member functions, but a few things aren't inherited. Each class has its own constructors, which is why ChaserEnemy had to write one and call Enemy’s. Every class also has its own destructor, and its own copy assignment operator, which the compiler writes for it, and which handles the base part by calling the base's. And a friend of the base, a function or class that's been allowed to see its private members, isn't a friend of the derived class. That's a feature we haven't needed yet, and Chapter 24 shows it.

Doing What the Base Does, and More

Sometimes a derived class wants its base's behavior, plus a little extra. A shooter drifts down the screen like any enemy, but it also counts down to its next shot. Add this class above main, below ChaserEnemy:

class ShooterEnemy : public Enemy
{
public:
    ShooterEnemy(float x, float y) : Enemy(x, y, 30, 20.0f)
    {
    }

private:
    float cooldown_ = 1.5f;
};

In the preceding code, a shooter passes its position up to Enemy, with 30 health and a speed of 20, and adds one member of its own: the seconds until it can shoot, starting at a second and a half.

Now its update. Add this to the public part of ShooterEnemy, below the constructor, with a blank line in between:

// A shooter drifts like any enemy, and counts down to its next shot
void update(float delta)
{
    Enemy::update(delta);
    cooldown_ -= delta;
}

bool readyToShoot() const
{
    return cooldown_ <= 0.0f;
}

In the preceding code, the first line of update is Enemy::update(delta), which calls the base class's update, by its full name, as Chapter 18's Player::takeDamage named its class. That drifts the shooter down the screen, exactly as any enemy drifts, without a line of Enemy’s code repeated. Then the shooter adds its own part, counting down its cooldown. The function readyToShoot says whether the cooldown has run out.

Try it inside main, in place of the other examples:

ShooterEnemy shooter(100.0f, 0.0f);
shooter.update(1.0f);
shooter.print();
std::cout << shooter.readyToShoot() << std::endl;
shooter.update(1.0f);
std::cout << shooter.readyToShoot() << std::endl;

In the preceding code, after one second, the shooter has drifted 20 pixels, and the console shows "at 100, 20, health 30", and then 0, because a bool prints as 0 or 1, and half a second of cooldown is still left. After another second, the cooldown has run out, and the console shows 1.

Warning

Leave out the Enemy:: in front of that first call, and update calls itself, because inside ShooterEnemy, plain update means the shooter's own. That call calls itself again, and so on forever, until the program crashes with 0xC00000FD, Chapter 8's stack overflow. Visual Studio does warn you, with C4717: 'ShooterEnemy::update': recursive on all control paths, function will cause runtime stack overflow, but a warning doesn't stop the build.

That pattern, calling the base's version and then adding to it, is one you'll use constantly, and Chapter 21's runners use it too.

Built from the Bottom Up

When a derived object is made, its base part is built first. It has to be: the derived class's constructor might use the base's members, so they must already exist. Try this pair of classes above main, below ShooterEnemy:

class Vehicle
{
public:
    Vehicle()
    {
        std::cout << "Vehicle built" << std::endl;
    }

    ~Vehicle()
    {
        std::cout << "Vehicle scrapped" << std::endl;
    }
};

In the preceding code, a Vehicle announces its construction and its destruction, just as Chapter 18's Noisy did. Now add a derived class below it:

class Car : public Vehicle
{
public:
    Car()
    {
        std::cout << "Car built" << std::endl;
    }

    ~Car()
    {
        std::cout << "Car scrapped" << std::endl;
    }
};

In the preceding code, Car’s constructor doesn't name Vehicle in an initializer list, and it doesn't need to, because Vehicle has a default constructor, and the compiler calls it. Now try this inside main, in place of the other examples:

{
    Car car;
    std::cout << "Driving" << std::endl;
}
std::cout << "Done" << std::endl;

In the preceding code, the car lives in a block of its own, so we can see exactly when it goes. The console shows this:

Vehicle built
Car built
Driving
Car scrapped
Vehicle scrapped
Done

In the preceding output, the Vehicle part is built before the Car part, and scrapped after it. A derived object is built from the bottom up: the base first, then the derived class's members, in the order they're declared, and then the derived constructor's body. It's taken apart in exactly the reverse order, from the top down, so the derived class's destructor can still rely on its base, as Figure 20.3 shows. The members slot in between, just as Chapter 18's knight built his sword and shield before his own constructor ran.

Built from the bottom up, taken down from the top. The Vehicle part is built first, then the Car part on top of it. At the end, the Car part goes first, while the Vehicle part it stands on is still there, and the Vehicle part goes last.
Figure 20.3 — Built from the bottom up, taken down from the top. The Vehicle part is built first, then the Car part on top of it. At the end, the Car part goes first, while the Vehicle part it stands on is still there, and the Vehicle part goes last.

Name Hiding

Here's a surprise that catches out nearly everyone who uses inheritance. Suppose a critical hit does double damage to a chaser. Add this to the public part of ChaserEnemy, above the comment // A chaser heads straight for its target, with a blank line in between:

// A critical hit does double damage
void takeDamage(int amount, bool critical)
{
    if (critical)
        amount *= 2;
    Enemy::takeDamage(amount);
}

In the preceding code, the chaser gets a second takeDamage, with an extra parameter that says whether the hit was critical. If it was, the damage doubles, and then the base's takeDamage does the rest, with Enemy:: in front, as in the shooter's update. Now try the ordinary kind of damage, inside main, in place of the other examples:

ChaserEnemy chaser(0.0f, 0.0f);
chaser.takeDamage(20);
chaser.print();

In the preceding code, chaser.takeDamage(20) looks like a call to the takeDamage that every enemy inherits. But the build stops with C2660: 'ChaserEnemy::takeDamage': function does not take 1 arguments.

This is name hiding. When a derived class declares a function with the same name as one in its base, it hides every base function of that name, whatever their parameters. Chapter 8's overloading only works between functions in the same place, so the compiler looks for takeDamage in ChaserEnemy, finds one, stops looking, and finds that it wants two arguments. The base's Enemy::takeDamage is still there, but it can no longer be seen from a ChaserEnemy.

To bring it back, add this line to the public part of ChaserEnemy, above the constructor, with a blank line in between:

using Enemy::takeDamage;

In the preceding code, the using declaration says that the name takeDamage from Enemy should be visible in ChaserEnemy too, alongside its own. Now both versions are overloads of one another, and the build succeeds: the chaser's health drops to 30. Add chaser.takeDamage(10, true); and another chaser.print();, and the critical hit does 20, leaving 10.

Only give a derived function the same name as a base function when that's exactly what you mean. The chaser's update has the same name and the same parameters as Enemy’s, which hides Enemy’s on purpose, so that a chaser chases rather than drifts. A using declaration can also bring in a base's constructors, as using Enemy::Enemy;, when a derived class wants to be built exactly as its base is.

A Family at Work

Let's put a few enemies to work, the way a game would. Try this inside main, in place of the other examples:

std::vector<ChaserEnemy> chasers = { ChaserEnemy(0.0f, 0.0f),
                                     ChaserEnemy(120.0f, 0.0f) };
std::vector<ShooterEnemy> shooters = { ShooterEnemy(100.0f, 0.0f) };

for (ChaserEnemy& chaser : chasers)
    chaser.setTarget(60.0f, 80.0f);

In the preceding code, the enemies live in two vectors, one for each kind, filled from lists in braces, as Chapter 13 showed. Every chaser is told where the player is: 60 across and 80 down. The first chaser starts at the top-left corner, and the second 120 pixels to its right.

Now run two frames of the game, a second each. Add this below the other lines:

for (int frame = 0; frame < 2; frame++)
{
    for (ChaserEnemy& chaser : chasers)
        chaser.update(1.0f);
    for (ShooterEnemy& shooter : shooters)
        shooter.update(1.0f);
}

for (const ChaserEnemy& chaser : chasers)
    chaser.print();
for (const ShooterEnemy& shooter : shooters)
    shooter.print();

In the preceding code, each frame updates every chaser and then every shooter, each through a reference of its own type, so every chaser chases, and every shooter drifts and counts down. Both chasers are 100 pixels from the player, so after two seconds at 50 pixels a second, both have caught her, and the console shows "at 60, 80, health 50" twice. The shooter has drifted 40 pixels, to "at 100, 40, health 30". The last two loops use const references, because print is const, and printing changes nothing.

This works, and for a small game it's perfectly good, and Chapter 21's project works this way too.

But look at the price. Every kind of enemy needs a vector of its own, and a loop of its own, in every place that updates or draws them. Add a bomber, and every one of those places needs another loop. What you'd really like is one list of enemies, and one loop that tells each of them to update, whatever kind it is. The next section shows why inheritance alone can't give you that.

Which update Runs?

A chaser is an enemy, so C++ lets you refer to one through an Enemy reference, or point at one with an Enemy pointer. That's what makes inheritance so useful: code written for enemies works on every kind of enemy. But it has a catch, and it's the most important one in this chapter. Try this inside main, in place of the other examples:

ChaserEnemy chaser(0.0f, 0.0f);
chaser.setTarget(60.0f, 80.0f);
chaser.update(1.0f);
chaser.print();

Enemy& asEnemy = chaser;
asEnemy.update(1.0f);
chaser.print();

In the preceding code, the first update is called on the chaser as a ChaserEnemy, and it chases, so the console shows "at 30, 40, health 50", as before. Then asEnemy is a reference to the very same chaser, as an Enemy. Calling update through it doesn't chase. It drifts, and the console shows "at 30, 90, health 50": 50 pixels straight down, which is Enemy::update.

Here's why. When the compiler meets asEnemy.update(1.0f), it chooses which function to call from the type it can see, and all it can see is an Enemy. It doesn't matter that the object is really a chaser, because the choice is made while the program is being built, long before there's an object at all. Choosing like this is called static binding, and a pointer does exactly the same. Add these lines below the others:

Enemy* pointer = &chaser;
pointer->update(1.0f);
chaser.print();

In the preceding code, pointer points at the chaser, with the arrow from Chapter 10, and its type is Enemy*, so pointer->update is Enemy::update again. The chaser drifts another 50 pixels down, to "at 30, 140, health 50". The object is a whole, perfectly good chaser, with its target intact, as Figure 20.4 shows. The compiler just never asks.

Which update runs? One chaser, reached three ways. Called on the chaser itself, update chases. Through an Enemy reference, or an Enemy pointer, it's Enemy's update that runs, because the compiler chooses by the type it can see, even though the object is still a whole chaser.
Figure 20.4 — Which update runs? One chaser, reached three ways. Called on the chaser itself, update chases. Through an Enemy reference, or an Enemy pointer, it's Enemy's update that runs, because the compiler chooses by the type it can see, even though the object is still a whole chaser.

This is the limit of inheritance on its own. You'd like to keep all your enemies in one list, loop through it, and tell each one to update, and have chasers chase and shooters shoot. With what we have so far, every one of them would drift. The fix is a single keyword, virtual, which asks C++ to choose the function by the object's real type, while the program runs, and it's the subject of Chapter 22. Until then, keep each kind of object in a variable, or a container, of its own type, as Chapter 21's project does.

Slicing

There's a second trap, and it's often confused with the first, so let's look at it separately. Try this 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 an Enemy, made as a copy of the chaser. An Enemy variable has room for an Enemy, 16 bytes, and no more, so only the chaser's Enemy part is copied, and its target is left behind. This is called slicing, because the derived part is sliced off, as Figure 20.5 shows. The copy really is just an enemy now, so its update drifts, and the console shows "at 0, 50, health 50".

Slicing. Copying a ChaserEnemy into an Enemy copies only the Enemy part, 16 bytes, and leaves the target behind. The copy isn't a chaser that behaves badly. It's an ordinary enemy, and nothing can turn it back.
Figure 20.5 — Slicing. Copying a ChaserEnemy into an Enemy copies only the Enemy part, 16 bytes, and leaves the target behind. The copy isn't a chaser that behaves badly. It's an ordinary enemy, and nothing can turn it back.

Slicing happens wherever a derived object is copied into something of its base type: an Enemy variable, a parameter of type Enemy passed by value, or a std::vector<Enemy>, which holds Enemys and nothing else, so every chaser pushed into it becomes an ordinary enemy on the way in. The difference from static binding is what's lost. Static binding calls the wrong function on the right object, and Chapter 22's virtual fixes it. Slicing makes the wrong object, and no keyword can put back what was never copied.

The cure for slicing is not to copy: use a reference or a pointer, as in the previous section, and the object stays whole. That's why Chapter 22 will keep a mixed list of enemies as pointers, never as Enemys.

Warning

One more trap waits for pointers. If a base pointer owns a derived object, deleting it runs only the base's destructor. Make a Car with Vehicle* vehicle = new Car();, then delete vehicle;, and the console says "Vehicle scrapped", but never "Car scrapped". That's undefined behavior, and a std::unique_ptr<Enemy> holding a chaser would do it too. Until Chapter 22's virtual destructors, don't own a derived object through a base pointer.

With static binding and slicing both in view, you know exactly where inheritance alone stops, and where Chapter 22 takes over.

Inheritance or Composition?

Now that you have inheritance, it's tempting to use it everywhere. Resist. Inheritance models is-a: a chaser is an enemy. Composition models has-a: Chapter 19's player has an animator, a car has an engine, and a game has a map. Figure 20.6 puts the two side by side.

Is-a and has-a. A ChaserEnemy is an Enemy, so it inherits from Enemy, and contains an Enemy part. Chapter 19's Player has an Animator, so the Animator is one of her members. Both classes are built from another, but only one of them is a kind of it.
Figure 20.6 — Is-a and has-a. A ChaserEnemy is an Enemy, so it inherits from Enemy, and contains an Enemy part. Chapter 19's Player has an Animator, so the Animator is one of her members. Both classes are built from another, but only one of them is a kind of it.

The rule of thumb is to prefer composition, and use inheritance only when the is-a sentence is honestly true. The classic mistake is inheriting just to share code: a FlyingEnemy base class, say, so that several enemies can "inherit flying", when what they really need is a Flight member they can each own. A class can only have one base, in most designs, but it can have as many members as it likes, and members can be swapped for others as the game changes. No player inherits from her inventory: she has one.

Real games use both, side by side. A Character base class might have an inventory, an animator, and a sound player as members, while Player, Villager, and Enemy inherit from it. Each tool is in its proper place.

Note

C++ does let a class inherit from more than one base at once, as in class Amphibian : public Car, public Boat. That's multiple inheritance, and it brings trouble, most famously the diamond problem, when two bases share a base of their own, and the derived class ends up with two copies of it. Games rarely need it, with one safe exception, the interfaces of Chapter 22.

For the rest of this book, one base per class is plenty.

AI Exercise (Optional)

Designing a class family well is more about judgment than syntax. By now you can write a base and a derived class in your sleep; the skill is knowing which family is the right shape, and whether a family is the right answer at all. A chatbot makes a good sparring partner for that: you describe the problem, it proposes a design, and you test its reasoning.

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 have just met inheritance: base and derived classes, protected members, calling a base constructor from a derived class's initializer list, calling a base function by its full name, and name hiding. I haven't learned virtual functions yet. I'm designing the pickups for a 2D game: a HealthPickup that restores health, an AmmoPickup that adds ammo, and a ShieldPickup that gives a shield for a few seconds. All three have a position, and whether they've been collected, and something happens when the player touches one. Please write the classes as a family, in one header, with a Pickup base class, public inheritance, and protected members only where the derived classes need them. Explain each design choice in a sentence, and then tell me honestly whether inheritance is the right tool here, or whether composition would be better."

Notice what the preceding prompt does. It says exactly what you know, including what you don't, so the answer doesn't lean on virtual, and it names the three kinds of pickup, and what they share, so the AI has enough to design with. The last sentence asks the AI to question its own design, which stops it from producing a family without ever asking whether a family was the right answer.

When the answer comes back, check it against this chapter. Is the base class sensible, with a position and a collected flag? Does each derived class add only what's its own, like a heal amount, an ammo count, or a shield time? If the base has a member that only one kind of pickup uses, that's a design smell worth asking about. And look for the static binding trap: without virtual, a function called through a Pickup& runs Pickup’s version, so if the design depends on calling each pickup's own function through the base, it won't work yet, and a good answer will say so.

The most interesting part is its answer to the last question. There's a case for inheritance, since pickups really are a family, and a case for composition: one Pickup class that has an effect as a member, so a new kind of pickup is a new effect rather than a new class. Neither is wrong. The reasoning is what you're judging, and if the AI picks one without weighing the other, ask it: "What are the downsides of your design?"

Summary

You can now build a family of classes. A derived class inherits everything its base has, adds only what makes it different, and reaches the base's protected members as its own, while the rest of the program can't. It passes arguments to the base's constructor in its initializer list, calls the base's version of a function by its full name, with Enemy::, and is built from the bottom up, and taken apart from the top down. You've met name hiding, and brought a hidden name back with using.

You've also found the edges. Through a base reference or pointer, the compiler calls the base's function, even for a chaser, because it chooses by the type it can see: static binding. And copying a derived object into a base slices it, leaving an ordinary base object behind. In the next chapter, the runner from Chapter 19 gets a base class of her own, and a crowd of rival runners are built on it too. Then Chapter 22 brings virtual, and the family finally behaves like itself, whichever way you reach it.