std::static_cast
)In our lessons on inheritance and polymorphism, we talked about how an object of type Goblin
also has the type of every parent class of Goblin
. In previous examples, that included Character
.
We could demonstrate this by treating a Goblin*
(pointer to a Goblin) as a Character*
(pointer to a Character)
class Character {
public:
void TakeDamage() {};
};
class Goblin : public Character {
void Enrage() {};
};
Goblin Harry;
Character* HarryPointer { &Harry };
We then also saw, through our Character*
, we could call functions that exist on the character class:
Goblin Harry;
Character* HarryPointer { &Harry };
HarryPointer->TakeDamage();
Because TakeDamage
is a function of the Character
class, we are able to use it from our Character*
.
But what if we wanted to call the Enrage
function instead? After all, Harry
is a Goblin
, so we should be able to do that:
Goblin Harry;
Character* HarryPointer { &Harry };
HarryPointer->Enrage();
This, unfortunately, results in an error:
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 totally 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.
This is another scenario where we need to rely on casting. Specifically, we have a pointer to a Character
, and we want to convert this to be a pointer to a Goblin
.
This scenario, where we are casting a pointer down the inheritance tree to a more specific subtype, is called downcasting.
The first thing to appreciate about this type of casting is that it might not work. Specifically, when we have a pointer of type Character*
and we try to cast it to a pointer of type Goblin*
, there are three possible situations:
Goblin
does not inherit from Character
. In this scenario, the cast will never work.Goblin
does inherit from Character
, and we're certain our Character*
is pointing at a Goblin
. This is the case in our simple 3 line code example above. HarryPointer
is definitely pointing at a Goblin
so in this scenario, the cast will always work.Goblin
does inherit from Character
, but we don't know for sure that our pointer is pointing at a Goblin
. This is the most common case when dealing with polymorphism. In this scenario, the cast might work.The first two scenarios can be handled by static_cast
, which we covered in a previous lesson.
We have seen before how we can explicitly convert to different data types using static_cast
.
That works in this scenario too. Using static_cast
, we can take our Character
pointer and convert it to a Goblin
pointer.
We can then Enrage
using our Goblin
pointer.
Goblin Harry;
Character* HarryPointer { &Harry };
static_cast<Goblin*>(HarryPointer)->Enrage();
This may seem contrived - why not just use a Goblin*
to store a pointer to a Goblin?
This would work, but is rarely an option when dealing with polymorphism. After all, the point of polymorphism is that code such as our Combat
and Act
functions need to work with any type of Character
.
Therefore, their parameter list need to be using Character
pointers, not specific subtypes. We will, therefore, be dealing with Character*
that we know (or think might be) pointing to more specific subtypes.
Given the following code, how can we call the Bark
function through the MyDogPointer
variable?
class Animal {};
class Dog : public Animal {
public:
void Bark() {};
}
Dog MyDog;
Animal* MyDogPointer { &MyDog };
One of the benefits of static_cast
over C-style casting comes up in this downcasting use case.
Imagine we had the following code:
class Character {};
class Goblin {}; // No inheritance
Character Harry;
Character* HarryPointer { &Harry };
Goblin* GoblinPointer {
static_cast<Goblin*>(HarryPointer)
};
Here, Goblin
does not inherit from Character
, so it is impossible that HarryPointer
points to a Goblin. Because we're using static_cast
, we get alerted to the fact our code does not make sense:
static_cast from 'Character *' to 'Goblin *', which are not related by inheritance, is not allowed
C-style casting does no such checking. It lets our code build, and the bug be introduced:
class Goblin {};
class Character {};
Character Harry;
Character* HarryPointer { &Harry };
Goblin* GoblinPointer {
(Goblin*)HarryPointer
};
We then have a broken Goblin*
variable in our code. If we go on to use it at run time, the results are unpredictable, but most likely a crash.
In the first example, static_cast
works well, because we know ahead of time that HarryPointer
, even though it is a Character
pointer, will only always be pointing at a Goblin
.
That's the only possible outcome of our simple 3 lines of code. But, this is not always going to be the case.
A limitation of static_cast
is it only checks if Goblin
is a subclass of Character
. It does not check that this specific Character
is a Goblin
.
Lets change our earlier example slightly:
class Character {
public:
void TakeDamage() {};
};
class Goblin : public Character {
void Enrage() {};
};
Character Harry;
Character* HarryPointer { &Harry };
Goblin* GoblinPointer {
static_cast<Goblin*>(HarryPointer)
};
In this scenario, even though Harry
is no longer a Goblin
, static casting his pointer to a Goblin*
seems to have worked.
If we then go on to use that pointer, we're likely to see our application crash:
GoblinPointer->Enrage();
We saw that static_cast
will check at compile time if our inheritance tree makes such a cast possible in theory.
In this case, it is possible - Goblin
inherits from Character
so a Character*
might be pointing at a Goblin
.
But, static_cast
doesn't do any run time checks to make sure. Not checking at run time is good for performance - if we know our cast will work, checking that it did work wastes some resources. So, if we know it will work, we should use static_cast
and enjoy the performance benefits.
However, often we can't be sure. Often, the specific subtypes our functions are dealing with will depend on runtime scenarios, like user input.
So, we'll have scenarios where a Character*
could be a Goblin*
, but might not be. This is very common in polymorphism.
For those scenarios, we have dynamic casting, which we'll cover in the next lesson.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way