In our lessons on inheritance and polymorphism, we talked about how an object of a specific type also has the type of every ancestor class. Below, our Bonker
object is both a Character
and a Goblin
:
class Character {};
class Goblin : public Character {};
int main(){
Goblin Bonker;
}
We see an example of this below, where our Goblin
is passed to the Act()
function in the form of a Character*
.
We’re then able to call the inherited GetName()
function:
#include <iostream>
using namespace std;
class Character {
public:
Character(string Name) : mName{Name}{}
string GetName(){ return mName; }
private:
string mName;
};
class Goblin : public Character {
public:
Goblin(string Name) : Character{Name}{}
void Enrage(){
cout << "Getting Angry!";
Damage += 5;
}
private:
int Damage{10};
};
void Act(Character* Enemy){
cout << Enemy->GetName() << " Acting\n";
}
int main(){
Goblin Bonker{"Bonker"};
Act(&Bonker);
}
Bonker Acting
Above, we saw that if we have a Character*
pointing we can call Character
functions, as expected. But what if we wanted to call some Goblin
function, such as Enrage()
?
After all, this Character*
is pointing at a Goblin
, so we should be able to do that. But if we try, we get a compilation error:
#include <iostream>
using namespace std;
class Character {/*...*/};
class Goblin : public Character {/*...*/};
void Act(Character* Enemy){
cout << Enemy->GetName() << " Acting\n";
Enemy->Enrage();
}
int main(){
Goblin Bonker{"Bonker"};
Act(&Bonker);
}
error: no member named 'Enrage' in 'Character'
Perhaps this makes sense. When we have a variable of type Goblin
, and we call a function on that object that is defined in the Character
class, this is safe. By the rules of inheritance, all Goblins are also Characters
But, this is not true in the opposite direction. Not all Characters are guaranteed to be Goblins. Therefore, when we have a variable of type Character*
, and we try to call a function that is defined in the Goblin
class, we get a compilation error.
static_cast
To address this, we need to rely on casting. Specifically, we have a pointer to a Character
, and we want to convert this to a pointer to a Goblin
. so we can access the goblin-specific functionality.
This scenario, where we are casting a pointer down the inheritance tree to a more specific subtype, is called downcasting.
The simplest form of this involves using the static_cast
approach we introduced earlier in the chapter.
In our Act()
function, we can use static_cast
to convert our Character
pointer to a Goblin
pointer.
void Act(Character* Enemy){
Goblin* GoblinPtr{
static_cast<Goblin*>(Enemy)
};
}
We can then Enrage
using our Goblin
pointer:
#include <iostream>
using namespace std;
class Character {/*...*/};
class Goblin : public Character {/*...*/};
void Act(Character* Enemy){
cout << Enemy->GetName() << " Acting\n";
static_cast<Goblin*>(Enemy)->Enrage();
}
int main(){
Goblin Bonker{"Bonker"};
Act(&Bonker);
}
Bonker Acting
Getting Angry!
This may seem contrived - why not just use a Goblin*
to store a pointer to a Goblin?
Remember, the point of run time polymorphism is that we want functions like Act
to work across a range of types.
If we updated our parameter to be a Goblin*
instead of a Character*
, we could call Enrage()
directly, but we then couldn’t call Act()
with anything except goblins.
Given the following code, how can we call the Bark
function through the MyDogPointer
variable?
class Animal {};
class Dog : public Animal {
public:
void Bark(){}
};
int main(){
Dog MyDog;
Animal* MyDogPointer{&MyDog};
// Call Bark using MyDogPointer
}
Something important to note about downcasting is that it can fail in one of two ways.
Below, we’re trying to cast a Goblin*
to a Dragon*
. This would never work - a Goblin
cannot possibly be a Dragon
as Dragon
does not inherit from Goblin
.
static_cast
can detect this problem, and raise an error:
class Character {};
class Goblin : public Character {};
class Dragon : public Character {};
void Act(Goblin* Enemy){
static_cast<Dragon*>(Enemy);
}
error: 'static_cast': cannot convert
from 'Goblin *' to 'Dragon *'
This is one of the advantages of static_cast
over C-style casting. A C-style cast would let this error through, resulting in a bug:
void Act(Goblin* Enemy){
(Dragon*)Enemy;
}
The second, more common way a downcast will fail is when our conversion is possible in theory, but is simply not valid in this specific case.
Below, our Act()
function attempts to cast a Character*
to a Goblin*
. This is theoretically possible - Character
is a base class for Goblin
, so a Character*
could be pointing at a Goblin
.
But in this specific case, it’s not:
#include <iostream>
using namespace std;
class Character {/*...*/};
class Goblin : public Character {/*...*/};
void Act(Character* Enemy){
cout << Enemy->GetName() << " Acting\n";
static_cast<Goblin*>(Enemy)->Enrage();
}
int main(){
Character Dragon{"Dave"};
Act(&Dragon);
}
Because our Act()
function assumes that the Character*
is always pointing at a Goblin
, we have a bug. Our program will behave unpredictably, likely crashing.
For scenarios where an object may or may not be a specific subtype, we need to use dynamic casting.
In the first example, static_cast
worked well because the Character*
within Act()
was always pointing to a Goblin
. Within our simple program, that was the only possibility.
But, this is not always going to be the case. Often, the specific subtypes our functions are dealing with on each invocation will be different, and they will depend on runtime conditions such as user decisions.
A limitation of static_cast
is it only checks if Character
is a parent class of Goblin
. It does not check whether this specific Character
is a Goblin
.
Because static_cast
is done at compile time, it has no performance overhead. This is great if we know the downcast will work, however, in many scenarios, we can’t be sure.
For those scenarios, we have dynamic casting.
To use dynamic casting, our type must be polymorphic. A polymorphic type is a type with at least one virtual function.
If our type doesn’t require any virtual functions, but we still want to use dynamic_cast
with it, by convention we make the destructor virtual:
class Character {
public:
virtual ~Character() = default;
};
dynamic_cast
The way we invoke dynamic_cast
follows the same pattern as static_cast
:
void Handle(Character* Object){
Goblin* GoblinPointer{
dynamic_cast<Goblin*>(Object)
};
};
The key difference is that dynamic_cast
performs an additional run-time check to determine whether the pointer actually is pointing to the thing we’re trying to cast it to.
In this case, it’s checking whether the Object
pointer really is pointing at a Goblin
.
If the pointer was not pointing at the type we specified, dynamic_cast
returns a nullptr
,
This allows us to react to cast failures because we can detect null pointers using an if
statement in the usual way:
void Handle(Character* Object){
Goblin* GoblinPointer{
dynamic_cast<Goblin*>(Object)
};
if (GoblinPointer) {
cout << "That was a Goblin\n";
} else {
cout << "That was not a Goblin\n";
}
};
Below, we show this in action. We first pass Handle
a Goblin
, and then a basic Character
:
#include <iostream>
using namespace std;
class Character {
public:
virtual ~Character() = default;
};
class Goblin : public Character { };
void Handle(Character* Object){
Goblin* GoblinPointer{
dynamic_cast<Goblin*>(Object)
};
if (GoblinPointer) {
cout << "That was a Goblin\n";
} else {
cout << "That was not a Goblin\n";
}
};
int main(){
Goblin Enemy;
Handle(&Enemy);
Character Player;
Handle(&Player);
}
That was a Goblin
That was not a Goblin
Let's go through a slightly more complex example. Below, we have a polymorphic combat system, very similar to what we created in the previous lesson.
Two Character
objects are passed to Battle()
as pointers, and they both then Act()
upon each other:
#include <iostream>
using namespace std;
class Character {
public:
Character(string Name) : mName{Name}{}
string GetName(){ return mName; }
void TakeDamage(int Damage){
cout << mName << " Taking Damage\n";
mHealth -= Damage;
}
virtual void Act(Character* Target){
Target->TakeDamage(50);
}
protected:
string mName;
int mHealth{150};
};
void Battle(Character* A, Character* B){
B->Act(A);
A->Act(B);
}
int main(){
Character Player{"Player"};
Character EnemyGoblin{"Goblin"};
Battle(&Player, &EnemyGoblin);
cout << '\n';
Character EnemyVampire{"Vampire"};
Battle(&Player, &EnemyVampire);
}
Player Taking Damage
Goblin Taking Damage
Player Taking Damage
Vampire Taking Damage
Let's add some subclasses we can use. Below, our vampire enemy now has its own dedicated Vampire
class. It has also developed a weakness to wooden stakes, represented by the vampire-specific Stake()
function.
Our player has become a VampireHunter
which, for now, just behaves in the same way as the basic Character
:
#include <iostream>
using namespace std;
class Character {/*...*/};
void Battle(Character* A, Character* B){/*...*/};
class Vampire : public Character {
public:
Vampire(string Name) : Character{Name}{}
void Stake(){
cout << mName << " Getting Staked\n";
mHealth = 0;
}
};
class VampireHunter : public Character {
public:
VampireHunter(string Name) : Character{Name}{}
};
int main(){
VampireHunter Player{"Player"};
Character EnemyGoblin{"Goblin"};
Battle(&Player, &EnemyGoblin);
cout << '\n';
Vampire EnemyVampire{"Vampire"};
Battle(&Player, &EnemyVampire);
}
So far, not much has changed - combat is proceeding as before:
Player Taking Damage
Goblin Taking Damage
Player Taking Damage
Vampire Taking Damage
We’d like to update our VampireHunter
with the ability to Stake()
vampire enemies. But we still need to fight non-vampires, too.
So, using what we’ve learned, we can override the Act()
function and use dynamic_cast
to determine whether or not we’re fighting a Vampire
.
If we are, we Stake()
them, otherwise, we fall back to the default Character
action:
#include <iostream>
using namespace std;
class Character {/*...*/};
void Battle(Character* A, Character* B){/*...*/};
class Vampire : public Character {/*...*/};
class VampireHunter : public Character {
public:
VampireHunter(string Name) : Character{Name}{}
void Act(Character* Target) override{
Vampire* VampirePtr{
dynamic_cast<Vampire*>(Target)};
if (VampirePtr) {
VampirePtr->Stake();
} else {
Character::Act(Target);
}
}
};
int main(){/*...*/};
Player Taking Damage
Goblin Taking Damage
Player Taking Damage
Vampire Getting Staked
It’s worth reflecting that we didn’t need to change our combat system (the contrived Battle
function, and the Character
class it uses) at any point in this process.
Our combat system doesn’t know anything about vampires and vampire hunting - all of that is encapsulated away in our vampiric types.
Because of the power of polymorphism, our combat system gets richer and more dynamic, without its code needing to get more complex, or even change at all.
static_cast
for Downcasting: We explored the use of static_cast
for downcasting, which is suitable when we're certain about the type of the object at compile time. This method is efficient but lacks runtime type checking.dynamic_cast
for Safe Downcasting: We discussed dynamic_cast
, which is used when the type of the object is not known until runtime. This method provides a safe way to perform downcasting by returning a nullptr
if the cast is not possible.Get to grips with downcasting in C++, including static and dynamic casts. This lesson provides clear explanations and practical examples for beginners
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way