Skip to content

Preface: 69 Casting in C++

Casting is a method we use in C++ to convert between types, and the type system is a way C++ provides to protect our code. It's not something we must strictly adhere to, as we can freely convert between types if we wish.

1. Understanding Dynamic Casting

Dynamic casting is a safety mechanism provided when we want to perform type conversions of specific types. dynamic_cast is a C++-style type conversion and is only applicable in C++; it cannot be used in C. It performs additional work to ensure that our actual type conversion is valid.

Whether to use dynamic_cast is entirely up to you. Here are some key points to help you understand when you should use it.

It's important to recognize that dynamic_cast is more like a function. Unlike type conversions performed at compile time, it is computed at runtime, thus incurring associated runtime costs.

dynamic_cast is specifically used for casting along the inheritance hierarchy. For example, in one of my games, there is an Entity class, from which Player and Enemy classes are derived. If I want to convert a Player to an Entity, it's straightforward because a Player is inherently an Entity object and can be implicitly converted. However, if I want to convert an Entity type to a Player, the compiler will trust us. If it's not actually a Player, we would be attempting to access Player-specific data, which could cause the program to crash. For this reason, dynamic_cast is often used for validation. If we try to use it to convert an Enemy to a Player, the conversion will fail, and dynamic_cast will return a NULL pointer, i.e., 0. We can use it to check if an object is of a given type.

2. Example

class Entity  // Base class
{
public:
};

class Player : public Entity
{
public:
};

class Enemy : public Entity
{
public:
};

int main()
{
    Player* player = new Player();
    Entity* e = player;             // Implicit conversion here

    Entity* e1 = new Enemy();
    Player* p = (Player*)e1;        // Error, we need to assure the compiler this is a Player
}

However, such a forceful cast is dangerous because e1 is actually an Enemy. If we forcefully cast it to a Player, unless Player and Entity have all the members and functions of Enemy, the program will encounter issues.

dynamic_cast is only used for polymorphic types: So we need a virtual function table to indicate that this is actually a polymorphic class type:

class Entity
{
public:
    virtual void PrintName(){}  // See 28 Virtual Functions
};

Test run:

Player* player = new Player();
Entity* actuallyPlayer = player;
Entity* actuallyEnemy = new Enemy();

Player* p0 = dynamic_cast<Player*>(actuallyEnemy);
Player* p1 = dynamic_cast<Player*>(actuallyPlayer);

We can see that p0 conversion fails:

While p1 is a valid Entity:

This is what dynamic casting does: if the cast is valid, it returns the desired pointer; if it's not of the declared type, the conversion is invalid and it returns NULL.

3. How It Determines Validity

It achieves this by storing runtime type information (RTTI), which holds runtime type information for all our types. This adds some overhead, but it allows you to perform tasks like dynamic casting.

There are two things to consider here: 1. RTTI adds overhead because types need to store more information about themselves. 2. Additionally, dynamic_cast also takes time because we need to check if the type information matches. Is this entity an Enemy or a Player? What type is it? When we use it, we must perform validation at runtime, which indeed adds overhead.

You can disable RTTI in VS:

Run the code:

Output error:

Access violation reading location 0x8AADD718.

You can see there's an access violation because it didn't get the type information, so it couldn't return NULL.

Therefore, it's essential to understand the actual implications of dynamic casting because they do some extra things, and in most cases, RTTI needs to be enabled (implicit conversion won't crash the program).

Additionally, you can perform checks similar to C# or Java:

Player* p0 = dynamic_cast<Player*>(actuallyEnemy);
if(p0)  // To prevent future conversions, generally avoid writing the full conversion in the condition
{

}