Polymorphism
In this lesson we'll learn how to execute different logic depending on the type of an object, that is, we'll learn about polymorphism.
Motivation
When creating hierarchies of structures, one of the main problems that arise is that different types of objects handle the same task differently. For example, when it comes to vehicles, the way a car accelerates is different from that of an airplane. This can make it difficult to write code that can handle different types of vehicles in a uniform way.
struct Vehicle
{
double speed;
void accelerate() {
std::cout << "Burning fuel\n"; // nothing too specific
}
};
struct Car
: Vehicle
{
void accelerate() {
std::cout << "Engaging the engine\n";
std::cout << "Pressing the gas pedal\n";
}
};
struct Airplane
: Vehicle
{
void accelerate() {
std::cout << "Engaging multiple engines\n";
std::cout << "Increasing the throttle\n";
}
};
We'll use a game as an example. Let's assume the player can use different kinds of vehicles. It's important to handle the player's input
in a consistent way, regardless of the type of vehicle player is currently using. For example, when the player presses the W
key,
it should accelerate. However, since we want each type of vehicle to run different logic to accelerate,
different implementations of the method are provided (see the code above).
Naive implementation
One could write a function that handles the player's input and calls the accelerate()
method of the vehicle:
// prism-push-types:Vehicle
void handle_accelerate_button(Vehicle const& vehicle) {
std::cout << "Handling the accelerate button...\n";
vehicle.accelerate();
}
This however, will call the accelerate()
method of the Vehicle
base class, which does not have the specific behavior
for the actual type of vehicle. This means that the player's vehicle will not accelerate as expected:
// prism-push-types:Car,Airplane
Car car;
Airplane airplane;
handle_accelerate_button(car);
handle_accelerate_button(airplane);
Virtual functions
The solution to this problem is to mark the accelerate()
as virtual in the base class.
struct Vehicle
{
double speed;
virtual void accelerate() {
std::cout << "Burning fuel\n"; // nothing too specific
}
};
With the previous code, we experienced what is called a static dispatch that resulted in a method shadowing.
This happens when a derived class introduces its own function with the same name as in the base class
and the base class' function is not marked as virtual
.
In the earlier example, the type of the object on which we called the accelerate()
as seen by the compiler was Vehicle
:
void handle_accelerate_button(Vehicle const& vehicle)
To call the proper implementation, the program has to first determine the exact type of the object, which requires a dynamic dispatch.
This is done by marking the accelerate()
method as virtual
in the base structure.
Static dispatch
During a static dispatch (default one), the compiler determines which function to call at compile time, based on the type of an object provided at the call site.
// prism-push-types:Airplane
Airplane airplane;
airplane.accelerate();
Dynamic dispatch
A dynamic dispatch causes the compiler to determine which function to call at run time, based on the actual type of the object. This comes at a small performance cost, but don't worry about it too much for now.
Assuming the accelerate()
method is marked as virtual
in the base class, the following code results in a dynamic dispatch:
// prism-push-types:Airplane,Vehicle
Airplane airplane;
// access the accelerate() method through a reference to the base class
Vehicle& vehicle = airplane;
vehicle.accelerate();