C++ Virtual Functions and Overrides

Using virtual functions to implement a simple example of polymorphism
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

3D art showing a character repairing a computer
Ryan McCombe
Ryan McCombe
Posted

In the last lesson, we were trying to get our Combat function to call different versions of Act, depending on which type of Character it received.

It looked like this:

#include <iostream>
using namespace std;

class Character {
public:
  void Act() {
    cout << "Character Attacking!" << endl;
  }
};

class Goblin : public Character {
public:
  void Act() {
    cout << "Goblin Attacking!" << endl;
  }
};

void Combat(Character* A, Character* B) {
    A->Act();
    B->Act();
}

int main() {
    Character Character;
    Goblin Goblin;
    Combat(&Character, &Goblin);
}

Sadly, it wasn't working. It was calling the Character version of Act twice:

Character Attacking!
Character Attacking!

Our goal is to have A->Act() call the Character version of Act (because A is just a basic Character) and to have B->Act() call the Goblin version of Act (because B is specifically a Goblin)

It seems that, when it's the Goblin's turn to Act, the overridden function isn't being used. Our code is still calling the Character version.

Updating the combat function to set the type of B to be a Goblin* would fix the problem. But remember, our goal is that the combat function needs to work with any type of Character, without requiring modification.

Static and Dynamic Binding

The problem we have is that our call to Act is being resolved at build time, rather than at run time. The compiler sees the type is a Character* so binds the Act call to the function defined in the Character class. This approach is sometimes called early binding or static binding.

Static binding has its advantages. It happens at compile time, there is no performance cost when our application is running. However, for polymorphism, we need late binding. It's sometimes also called dynamic binding.

After all, in real games, we generally don't know what type of monsters the player is going to enter combat with. That's normally going to be based on the player's actions.

C++ Virtual Functions

To tell the compiler one of our functions needs to be dynamically bound, we mark that function as being virtual.

class Character {
public:
  virtual void Act() {
    cout << "Character Attacking!" << endl;
  }
};

Just like that, we have polymorphism:

Character Attacking!
Goblin Attacking!

The virtual specifier means, when this function is called, our code will instead traverse down the inheritance tree to find the most derived version of that function.

For our basic Character, the base function was the most derived. But for the Goblin, we had a more derived function, within the Goblin class. And that derived class had an Act function that could be used, so that function was called.

The reason that this doesn't happen by default is that the process of "finding the most derived function" happens at run time. This means that calling a virtual function has a small performance impact over calling a function that is statically bound.

Test your Knowledge

What is the effect of the virtual specifier?

The Override Specifier in C++

Because our Goblin has a function with the exact same prototype of a virtual function in a base class, the function in our Goblin class is now an override.

We can make that clearer in our code by adding the override specifier to it:

class Goblin : public Character {
public:
  void Attack() override {
    cout << "Goblin Attacking!" << endl;
  }
};

This is not required, but we should use it where applicable. This is because it has two benefits.

Firstly, it informs other developers reading our function that it is an override

Secondly, when we add the override specifier, the compiler ensures that our function is, in fact, overriding something. It ensures there is a virtual function on a base class with the same prototype.

This second point is especially useful when refactoring. For example, if we rename or change the parameters of a function that has been overridden in a child class, we want to be alerted to that. This makes sure we know to update those child classes too, and prevent any potential bugs.

Test your Knowledge

What is the effect of the override specifier?

C++ final Specifier - Preventing Overrides

When our code encounters a virtual function, the process of traversing down the inheritance tree to find the most derived override effectively treats all intermediate functions as virtual, too.

This is the case whether they are marked as virtual or not. In scenarios where we don't want that to happen, we can mark one of our functions as final, as shown on line 3 below:

class Character {
public:
  virtual void TakeDamage(int Damage) final {};
};

class Goblin : public Character {
public:
  void TakeDamage(int Damage) override {};
};

In the above example, Line 8 will fail to compile, and it will inform us that the Character's TakeDamage() function is not designed to be overridden.

Test your Knowledge

What is the effect of the final specifier on a class function?

Polymorphism without References

It is worth noting that this form of run-time polymorphism is only applicable to reference types (ie, references and pointers).

The following code creates a Goblin object. It then stores that Goblin in a variable of type Character, and a pointer to that Goblin in a variable of type Character*

1#include <iostream>
2using namespace std;
3
4class Character {
5public:
6  virtual void SayHello() {
7    cout << "Hello, I'm a Character!";
8  }
9};
10
11class Goblin : public Character {
12public:
13  virtual void SayHello() override {
14    cout << "Hello, I'm a Goblin!";
15  }
16};
17
18int main() {
19  Goblin Harry;
20
21  Character HarryObject { Harry };
22  HarryObject.SayHello();
23
24  Character* HarryPointer { &Harry };
25  HarryPointer->SayHello();
26}
27

The Character on Line 22 will say "Hello, I'm a Character!" but the Character* on line 25 will say "Hello, I'm a Goblin!".

When not using a pointer or reference, the process of using a variable as a base type has the effect of converting the object to that type.

Line 21 takes the Goblin object and makes a copy of it. The copy is converted to the Character type, as that is the type of variable we are storing it in. As such, HarryObject is not a Goblin - he is just a Character.

On the other hand, Line 24 is creating a pointer to the Goblin. This is another example of the polymorphism we have seen above. We can have a Character pointer point at a Goblin object, because all Goblin objects are also Character objects due to inheritance.

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Polymorphism
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access!

This course includes:

  • 66 Lessons
  • Over 200 Quiz Questions
  • Capstone Project
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++ Static Casting with Pointers (std::static_cast)

Learn how we can "downcast" our pointers to let us access functions in derived object types.
3D art showing a fantasy monster
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved