Polymorphism is one of the most important features of inheritance, but it’s fairly challenging to describe without showing some examples. To start, let’s go back to using examples related to robotics programming.
You’ll see later that it can be helpful to group robot code into Command
classes, where Command
is a base class that has multiple derived classes representing different types of commands. For example, there may be a MoveForward
command, or a Rotate
command, both of which inherit from the Command
class. Here’s what that could look like:
#include <iostream> class Command { public: virtual void run() = 0; }; class MoveForward : public Command { private: float distance_ft; public: MoveForward(float distance_ft) { this->distance_ft = distance_ft; } void run() override { std::cout << "Moving forward " << distance_ft << " feet...\n"; } }; class Rotate : public Command { private: float angle_deg; public: Rotate(float angle_deg) { this->angle_deg = angle_deg; } void run() override { std::cout << "Rotating " << angle_deg << " degrees...\n"; } }; int main() { MoveForward move(2.5); Rotate rotate(90); Command *moveCommand = &move; Command *rotateCommand = &rotate; moveCommand->run(); rotateCommand->run(); return 0; }
Program output:
Moving forward 2.5 feet... Rotating 90 degrees...
Now, this code uses polymorphism, so I’ll start by showing what polymorphism is and how it works. After that, I’ll show you when it’s useful.
Let’s start in the main
function:
- We first create instances of the
MoveForward
andRotate
classes, just like we’ve done previously.- The
move
object is constructed with adistance_ft
value of 2.5, and therotate
object is constructed with anangle_deg
value of 90.
- The
- The next two lines are where polymorphism comes into play.
- Polymorphism allows you to represent the data type of an object as a pointer to the object’s base class. This means we can create the pointer “
moveCommand
” with an underlying data type of “Command
“, even though themove
object actually has the data type “MoveForward
“. This works becauseMoveForward
is aCommand
. The same applies to therotate
object.- Note that polymorphism only works when using pointers (or references).
- Polymorphism allows you to represent the data type of an object as a pointer to the object’s base class. This means we can create the pointer “
- Lastly, we use the
run
function on both of theCommand
pointers.- Here, you may see that the
run
function inside theCommand
class doesn’t really do much. When using polymorphism, the function used depends on the derived class that the pointer represents. Therefore,moveCommand->run();
calls therun
function from theMoveForward
class, not from theCommand
class.
- Here, you may see that the
Now let’s look at the code in each of the classes:
- The
Command
class contains a function called “run
” that looks quite a bit different from what we’ve seen before.- The word “
virtual
” means that the function can be “overriden” by functions in derived classes.- You can see that both the
MoveForward
and theRotate
classes have their ownrun
functions with the wordoverride
after their parentheses. This means that these two functions “override” (replace) therun
function in their base class (theCommand
class).
- You can see that both the
- The “
= 0;
” following theCommand
class’srun
function turns it into a “pure virtual function“. This means that the function has no “implementation” (functionality), so it can never be called. This is fine, though, because the function is overriden in its two derived classes.- This function is made a pure virtual function because it doesn’t make sense to “run” just a “
Command
“. Running theMoveForward
orRotate
commands, however, makes sense because those commands perform a specific task.
- Any class that has at least one pure virtual function is called an “abstract class.” Abstract class instances are not allowed (so I cannot create a
Command
object). However, as you saw in themain
function, abstract class pointers are allowed, as long as they point to an object of a non-abstract derived class (such asMoveForward
orRotate
).
- This function is made a pure virtual function because it doesn’t make sense to “run” just a “
- The word “
So what?
A normal initial reaction to polymorphism is thinking that it’s overly complex and doesn’t provide any benefits (at least that’s what I first thought).
Generally, polymorphism is useful because it allows us to store objects of different (but related) data types all in one place (e.g. an array or vector). By using polymorphism with robot commands, we can store a sequence of different commands all inside a single vector. This wouldn’t work without polymorphism because vectors can only store values of a single data type. With polymorphism, that single data type is a base class pointer:
#include <iostream> #include <vector> class Command { public: virtual void run() = 0; }; class MoveForward : public Command { private: float distance_ft; public: MoveForward(float distance_ft) { this->distance_ft = distance_ft; } void run() override { std::cout << "Moving forward " << distance_ft << " feet...\n"; } }; class Rotate : public Command { private: float angle_deg; public: Rotate(float angle_deg) { this->angle_deg = angle_deg; } void run() override { std::cout << "Rotating " << angle_deg << " degrees...\n"; } }; int main() { MoveForward move(2.5); Rotate rotate(90); std::vector<Command*> commandSequence = {&move, &rotate}; for (int i = 0; i < commandSequence.size(); i++) { commandSequence[i]->run(); } return 0; }
Program output:
Moving forward 2.5 feet... Rotating 90 degrees...
When structuring our code like this, we can also easily add new commands to the commandSequence
vector later on (if a button on a controller is pressed, for example). Of course, this only works if the command added inherits from the Command
class.
A Deeper Look into “virtual
“
In case you don’t fully understand how the keyword “virtual
” affects functions, let’s look at some other cases:
Non-Virtual Function in Base Class
By changing the Command
class to this…
class Command { public: void run() { std::cout << "Default command running...\n"; } };
…the program won’t compile because the other two run
functions use the “override
” keyword. When a function in the base class is not virtual, the function cannot be overriden by derived classes.
Let’s remove the word “override
” from the other classes and see what happens:
#include <iostream> #include <vector> class Command { public: void run() { std::cout << "Default command running...\n"; } }; class MoveForward : public Command { private: float distance_ft; public: MoveForward(float distance_ft) { this->distance_ft = distance_ft; } void run() { std::cout << "Moving forward " << distance_ft << " feet...\n"; } }; class Rotate : public Command { private: float angle_deg; public: Rotate(float angle_deg) { this->angle_deg = angle_deg; } void run() { std::cout << "Rotating " << angle_deg << " degrees...\n"; } }; int main() { MoveForward move(2.5); Rotate rotate(90); std::vector<Command*> commandSequence = {&move, &rotate}; for (int i = 0; i < commandSequence.size(); i++) { commandSequence[i]->run(); } return 0; }
Program output:
Default command running... Default command running...
Now the Command
class’s run
function is used because it is not overriden by its derived classes.
Virtual (but not Pure) Function in Base Class
Let’s now add the word “virtual
” back to the run
function in the Command
class and add the word “override
” back to the other two run
functions. However, rather than making the Command
class’s run
function “pure virtual” (set equal to 0), let’s keep its implementation the same:
class Command { public: virtual void run() { std::cout << "Default command running...\n"; } };
Even though run
is a virtual function, the Command
class is not an abstract class in this case (because it doesn’t have at least one pure virtual function). This means that a Command
object can be created:
#include <iostream> #include <vector> class Command { public: virtual void run() { std::cout << "Default command running...\n"; } }; class MoveForward : public Command { private: float distance_ft; public: MoveForward(float distance_ft) { this->distance_ft = distance_ft; } void run() override { std::cout << "Moving forward " << distance_ft << " feet...\n"; } }; class Rotate : public Command { private: float angle_deg; public: Rotate(float angle_deg) { this->angle_deg = angle_deg; } void run() override { std::cout << "Rotating " << angle_deg << " degrees...\n"; } }; int main() { MoveForward move(2.5); Rotate rotate(90); Command genericCommand; std::vector<Command*> commandSequence = {&move, &rotate, &genericCommand}; for (int i = 0; i < commandSequence.size(); i++) { commandSequence[i]->run(); } return 0; }
Program output:
Moving forward 2.5 feet... Rotating 90 degrees... Default command running...
In this program, objects of the Command
class can be created that have their own run
function. Since that run
function is virtual, however, it can be overriden by the other two classes.
Challenge Problem
Write a program that follows the specifications below:
- Create a
Shape
class that is abstract- The class should contain a pure virtual function called “
draw
“
- The class should contain a pure virtual function called “
- Create an
EquilateralTriangle
class that inherits fromShape
(using public inheritance).- The class should include a constructor and an override of the
draw
function. - The constructor should include one parameter for the triangle’s side length.
- The
draw
function should print a triangle to the screen that corresponds to the triangle’s side length.
- The class should include a constructor and an override of the
- Create a
Rectangle
class that inherits fromShape
(using public inheritance).- The class should include a constructor and an override of the
draw
function. - The constructor should include two parameters for the rectangle’s side lengths.
- The
draw
function should print a rectangle to the screen that corresponds to the rectangle’s width and height.
- The class should include a constructor and an override of the
- Create a
Square
class that inherits fromRectangle
(using public inheritance).- The class should include a constructor with one parameter for the square’s side length.
- The constructor should also call
Rectangle
‘s constructor.
- The
main
function should include a vector containing multiple shapes.- Loop through the vector and call the
draw
function for each shape.
- Loop through the vector and call the
- Drawing the shapes could be done with any character you’d like (I chose the asterisk).
Sample output:
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
This output was produced using a triangle with a side length of 5, a 4×6 rectangle, and a square with a side length of 4.
If you get stuck, check out my solution here.