Downcasting

Get to grips with downcasting in C++, including static and dynamic casts. This lesson provides clear explanations and practical examples for beginners
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a fantasy monster
Ryan McCombe
Ryan McCombe
Updated

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

Why do we need Downcasting?

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.

Downcasting using 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.

Test your Knowledge

Downcasting

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
}

Downcasting Can Fail

Something important to note about downcasting is that it can fail in one of two ways.

Compile Time Failure (Impossible Downcasts)

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;
}

Run Time Failure

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.

Static vs 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.

Polymorphic Types

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;
};

Downcasting using 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

A Practical Example

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.

Summary

  • Understanding Downcasting: We learned that downcasting is a way to convert a pointer or reference from a base class to a derived class in the inheritance hierarchy.
  • 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.
  • Practical Application in Polymorphic Systems: Through examples involving characters in a combat system, we illustrated how downcasting can enrich the functionality of polymorphic systems without complicating the underlying code structure.

Was this lesson useful?

Next Lesson

Preprocessor Definitions

Explore the essential concepts of C++ preprocessing, from understanding directives to implementing macros
3D art showing a character in a bar
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Downcasting

Get to grips with downcasting in C++, including static and dynamic casts. This lesson provides clear explanations and practical examples for beginners

3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Polymorphism
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 60 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Preprocessor Definitions

Explore the essential concepts of C++ preprocessing, from understanding directives to implementing macros
3D art showing a character in a bar
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved