This lesson is a quick introductory tour of polymorphism within C++. It is not intended for those who are entirely new to programming. Rather, the people who may find it useful include:
It summarises several lessons from our introductory course. Anyone looking for more thorough explanations or additional context should consider Chapter 6 of that course.
When calling methods through references and pointers, C++ uses early binding by default. That means, the compiler figures out what methods to call at compile time, based on the type of reference or pointer:
#include <iostream>
class Monster {
public:
void Log(){ std::cout << "Monster\n"; }
};
class Dragon : public Base {
public:
void Log(){ std::cout << "Dragon\n"; }
};
void CallMethod(Base& Object){ Object.Log(); }
int main(){
Monster BaseObject;
std::cout << "The first object is a ";
CallMethod(BaseObject);
Dragon DerivedObject;
std::cout << "The second object is a ";
CallMethod(DerivedObject);
}
The first object is a Monster
The second object is a Monster
The output is technically correct, but perhaps not what we were expecting or wanted. The second object is a Monster
, but it is specifically a Dragon
.
Even though the second invocation of CallMethod
is passed a reference to a Dragon
object, our program logs Monster
both times.
This is because the reference in the CallMethod
function has the type of Monster
, and the Monster::Log
function uses early binding. Early binding means the compiler identifies what function to call at compile time, based on the specific type of the object.
In this case, the type within the CallMethod
function was a Monster
object, so the compiler binds the Object.Log()
call to the version of that function that is in the Monster
 class.
virtual
To flag a method for late binding, we need to add the virtual
specifier to it:
class Monster {
public:
virtual void Log(){ std::cout << "Monster\n"; }
};
This has performance implications, as our program now needs to find the correct function at run time.
But, because it happens at run time, our program can investigate exactly what is stored in that location in memory at the point our function is called. It identifies the second call as receiving not just a plain old Monster
object, but specifically a Dragon
 object.
As a result, it calls the more specific function, which was in the Dragon
class. With no further changes to our program, we get the desired output:
The first object is a Monster
The second object is a Dragon
This is an example of a powerful run-time polymorphism a powerful concept that lets our project expand in complexity, without our systems getting more complex.
Our core systems (in this case, the contrived CallMethod()
function) haven’t changed. In fact, those systems don’t even know there is a class called Dragon
. Yet, they can interact with Dragon
objects, invoking all the dragon-specific behaviors.
We could add 100 more types of Monster
, and our core systems don’t need to change at all to accommodate all that variation. They just call Monster
methods, and our specific Monster
subtypes get to decide what happens, by overriding those methods as required.
override
keywordWhen a child class is overriding a function it is inheriting, we should add the override
specifier to that function:
class Dragon : public Monster {
public:
void Log() override {
std::cout << "Dragon" << endl;
}
};
This is optional, but highly recommended. Adding the override
specifier makes our intent clear, both to other developers and the compiler.
If we attempt to override a function, but we misspell its name, or provide a different argument list, the compiler doesn’t know we’ve made a mistake. It assumes we’re just defining a different function, so it lets the bug slip through.
By adding the override
specifier, the compiler will know our intent and will throw an error if the function we’re defining on the derived class is not actually overriding anything on a base class.
dynamic_cast
When we have a pointer or reference of the base type, and we want to access a function or variable on a more derived type, we can use dynamic_cast
:
In this method, we have a parameter of the type Monster&
, but we suspect it might be a more specific Dragon
object, so we downcast it appropriately:
void CallMethod(Monster& Object) {
dynamic_cast<Dragon&>(Object);
}
To use dynamic_cast
, our type must be polymorphic, that is, it must define at least one virtual
method. If we otherwise have no need for any virtual method in the class, conventionally we just mark the destructor as virtual:
class Monster {
public:
virtual ~Monster(){}
};
Dynamic casting will succeed if the reference or pointer really is the derived type we tried to cast it to. In that successful scenario, it will return a reference or pointer to that same memory location, but with an updated type. This allows us to access derived members:
#include <iostream>
class Monster {
public:
virtual void BaseFunction(){
std::cout << "Monster ";
}
};
class Dragon : public Monster {
public:
void DerivedFunction(){
std::cout << "and Dragon";
}
};
void CallMethod(Monster& Obj){
Obj.BaseFunction();
dynamic_cast<Dragon&>(Obj).DerivedFunction();
}
int main(){
Dragon DerivedObject;
CallMethod(DerivedObject);
}
Monster and Dragon
Dynamic casting can fail. This happens when the object doesn’t actually have the type we’re trying to cast it to. Below, we’re trying to dynamic cast to a Dragon
, but what we’ve received is just a plain old Monster
:
void CallMethod(Monster& Object) {
dynamic_cast<Dragon&>(Object);
}
int main() {
Monster BaseObject;
CallMethod(BaseObject);
}
If dynamic casting a reference fails, as in the above example, our code will throw an exception. We cover exceptions and how to handle them later in this course.
If dynamic casting a pointer fails, it returns a nullptr
. We can test for null pointers by using an if
 statement:
#include <iostream>
class Monster {
public:
virtual ~Monster(){}
};
class Dragon : public Monster {};
void CallMethod(Monster* Obj){
Dragon* DragonPtr{dynamic_cast<Dragon*>(Obj)};
if (DragonPtr) {
std::cout << "That's a dragon\n";
} else {
std::cout << "That isn't";
}
}
int main(){
Dragon Scaly;
CallMethod(&Scaly);
Monster Goblin;
CallMethod(&Goblin);
}
That's a dragon
That isn't
— First Published
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games