std::dynamic_cast
)std::dynamic_cast
Lets imagine our base Character
class look like this:
class Character {
public:
virtual void Act(Character* Enemy) {
Enemy->TakeDamage(50);
}
virtual void TakeDamage(int Damage) {
Health -= Damage;
}
protected:
int Health { 200 };
}
Vampires are about to enter the battle, and they can shrug off any damage! Lets override the TakeDamage
function to make that happen:
class Vampire : public Character {
public:
void TakeDamage(int Damage) override {
cout << "Muahahaha";
}
}
They've been defeating everyone. They can be killed by wooden stakes, but nobody has uncovered this fatal weakness:
class Vampire : public Character {
public:
void TakeDamage(int Damage) override {
cout << "Muahahaha";
}
void Stake() {
Health = 0;
}
}
Finally, some Vampire Hunters show up, with the secret knowledge and a solid plan:
class VampireHunter : public Character {
public:
void Act(Character* Enemy) override {
if (EnemyIsVampire) {
Enemy->Stake();
} else {
Enemy->TakeDamage(30);
}
}
}
Lets see how we can get that working!
In the previous lesson we saw that, if we knew the Enemy
was pointing to a Vampire
, we could just use static_cast
.
void Act(Character* Enemy) override {
Vampire* VampireEnemy {
static_cast<Vampire*>(Enemy)
};
VampireEnemy->Stake();
}
This will work as long as the Enemy
really is a Vampire
, but we can't know that. Our VampireHunters
will be able to fight other types of Character
too, so we need a better solution.
When we need to try a cast, but we're not sure it will work, we should use dynamic_cast
Dynamic casts are done at run-time, so they have a performance cost. What we get for that cost is information on whether or not the cast was successful.
We use dynamic_cast
in the same way we would use static_cast
:
dynamic_cast<Vampire*>(Enemy);
However, now we can now check if the resulting pointer is valid before trying to use it. We can do that with a simple if
check on the result of calling dynamic_cast
.
1void Act(Character* Enemy) override {
2 if (dynamic_cast<Vampire*>(Enemy)) {
3 // enemy is a vampire
4 // TODO: call the stake function
5 } else {
6 // enemy is not a vampire
7 Enemy->TakeDamage(30);
8 }
9}
10
Within the if
statement, we want to call the Stake
function.
Stake
is specifically a function that exists on Vampire
. Enemy
is a Character
pointer, so we can't just do Enemy->Stake()
.
Instead, we could do the cast twice - once to check if it's a vampire, and again on line 4 to access the Stake
function.
1void Act(Character* Enemy) override {
2 if (dynamic_cast<Vampire*>(Enemy)) {
3 // enemy is a vampire
4 dynamic_cast<Vampire*>(Enemy)->Stake();
5 } else {
6 // enemy is not a vampire
7 Enemy->TakeDamage(30);
8 }
9}
10
But it would be cleaner to just do the cast once, and save it in a variable that we can reuse:
void Act(Character* Enemy) override {
Vampire* VampireEnemy {
dynamic_cast<Vampire*>(Enemy)
};
if (VampireEnemy) {
VampireEnemy->Stake();
} else {
Enemy->TakeDamage(30);
}
}
This technique only works with dynamic_cast
. Static casting does not check if the cast is valid at run time. The pointer generated from a static_cast
will always point to something.
When creating a variable using dynamic_cast
, we always want to make sure it succeeds before trying to use it.
Consider the following code:
void MakeNoise(Animal* SomeAnimal) {
static_cast<Dog*>(SomeAnimal)->Bark();
}
If we are not certain that the SomeAnimal
parameter is pointing to a Dog
, what should we change in the above code?
Something interesting might have caught our attention about the previous code example:
Vampire* VampireEnemy {
dynamic_cast<Vampire*>(Enemy)
};
if (VampireEnemy) {
VampireEnemy->Stake();
} else {
Enemy->TakeDamage(30);
}
Given how we're using the result of dynamic_cast
within the if
statement, it seems there are two possible outcomes:
dynamic_cast
returns a pointer to a Vampire
, ordynamic_cast
returns something that is false
when used as a booleanThis is indeed true. Dynamic casting always returns a pointer but, if the cast failed, what we get is something called a null pointer. A null pointer is falsey, ie, if we treat it as a boolean like in our if
statement above, it will be treated as the boolean value false
.
What if we didn't check using the if
statement? We may have ended up trying to dereferencing a null pointer using the *
or ->
operator. This will result in an error at run time - most likely a crash.
Therefore, when we create a pointer using dynamic_cast
, we should always be considering the possibility that the cast failed before using the pointer it returned.
nullptr
)Any pointer can be a null pointer - they are not just the result of casting. Therefore, when using pointers, it is generally recommended to ensure the pointer we receive is valid before trying to dereference it.
In the below example, we are just returning from the function immediately. This means our specific function will not cause a crash.
void Combat(Character* Player, Character* Enemy) {
if (!Player || !Enemy) {
cout << "Combat function received a null pointer!";
return;
};
Player->Act(Enemy);
Enemy->Act(Player);
}
In many situations, we are writing code that is working with a pointer that we expect to sometimes be null. Doing an if
check and implementing some alternative behaviour is a reasonable way to deal with such scenarios.
However, there are other times where we are operating on a pointer that we would never expect to be null. If it is null, that would mean something had fundamentally gone wrong in our program.
In these cases, we should still check if the pointer is null, because things do go wrong all the time. However, the strategy of having our code "silently fail" in such scenarios is not ideal.
If something is causing our program to get into a state we never expected to be possible, we have a bug. We'd like to be alerted to that bug so we can fix it. Having our functions try to work around the weirdness might be hiding the issue from us.
We will see some specific strategies for dealing with errors in the intermediate course. For now, handling these scenarios with silent failures, using cout
to log an error message, or just letting the program crash are resonable approaches.
Consider the following function:
void MakeNoise(Animal* Dog) {
Dog* SomeDog { dynamic_cast<Dog*>(SomeAnimal) };
}
If we are not certain that SomeAnimal
is pointing to a Dog
, what is a safe way of calling the Bark
function through the SomeDog
variable?
Null pointers are not a bad thing. Often, our pointers should be null.
For example, our character class might have a Target*
field, which is a pointer to the character that the player has currently selected.
If the player has not selected anything, having that field be a null pointer would be sensible.
We often want pointers to start off as null pointers. If we do not explicitly give a pointer an initial value, it will point to some garbage location.
int* SomePointer;
cout << SomePointer;
This code logs out an arbitrary memory address, such as 0x7ffc0a866a60
.
We use the nullptr
token to set a value to the null pointer:
int* SomePointer { nullptr };
cout << SomePointer;
This code now logs out the memory address of simply 0
, which we can think of as being "a pointer to nothing"
This would seem like very strange behaviour. If we don't specify an initial value, why doesn't the compiler just choose an appropriate default, like nullptr
?
The design of C++ follows a "zero overhead" philosophy, and initialising a variable has a small performance overhead.
Assigning a value to a variable requires a CPU instruction. Therefore, it is not done by default. If we want our variables to have some default value, we need to take care of that ourselves.
However, most compilers and code editors will warn us if our code tries to use a variable that has not been initialised, so we are afforded some help from our tools.
Having a pointer pointing to an arbitrary, unknown memory address can be a problem. Even if we run an if
check on one of these pointers before trying to use it, that check will return true.
After all, an uninitialised pointer is not a null pointer. It actually is pointing to something, just not something we intended.
As a result, the following code will compile, but will result in unpredictable results when run - most likely a crash:
class Character {
public:
void Dance() {};
};
int main() {
Character* Pointer;
// This will be true - we are pointing to something
if (Pointer) {
// but it's almost certainly not pointing to a character
Pointer->Dance(); // crash!
}
}
Because of this, if we do not have an initial value we want to use for a pointer, we should set it to be a null pointer explicitly, using the nullptr
syntax:
class Character {
public:
void Dance() {};
};
int main() {
Character* Pointer { nullptr };
// This will be false now, and our code will be safe
if (Pointer) {
Pointer->Dance();
}
}
Which of the following statements is creating a null pointer?
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way