Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Safe Downcasting with dynamic_cast
Learn dynamic casting and type safety in C++.
Dynamic casting
In the lesson on explicit type conversion (casting) and static_cast, we examined the concept of casting and the use of static_cast to convert variables from one type to another.
In this lesson, we'll continue by examining another type of cast: dynamic_cast.
The need for dynamic_cast
When dealing with polymorphism, you'll often encounter cases where you have a pointer to a base class, but you want to access some information that exists only in a derived class.
Consider the following (slightly contrived) program:
#include <iostream>
#include <string>
#include <string_view>
class Storage
{
protected:
int m_capacity{};
public:
Storage(int capacity)
: m_capacity{capacity}
{
}
virtual ~Storage() = default;
};
class CloudStorage : public Storage
{
protected:
std::string m_provider{};
public:
CloudStorage(int capacity, std::string_view provider)
: Storage{capacity}, m_provider{provider}
{
}
const std::string& getProvider() const { return m_provider; }
};
Storage* allocateStorage(bool useCloud)
{
if (useCloud)
return new CloudStorage{1000, "AWS"};
else
return new Storage{500};
}
int main()
{
Storage* s{ allocateStorage(true) };
// how do we print the CloudStorage object's provider here, having only a Storage pointer?
delete s;
return 0;
}
In this program, function allocateStorage() always returns a Storage pointer, but that pointer may be pointing to either a Storage or a CloudStorage object. In the case where the Storage pointer is actually pointing to a CloudStorage object, how would we call CloudStorage::getProvider()?
One way would be to add a virtual function to Storage called getProvider() (so we could call it with a Storage pointer/reference, and have it dynamically resolve to CloudStorage::getProvider()). But what would this function return if you called it with a Storage pointer/reference that was actually pointing to a Storage object? There isn't really any value that makes sense. Furthermore, we would be polluting our Storage class with things that really should only be the concern of the CloudStorage class.
We know that C++ will implicitly let you convert a CloudStorage pointer into a Storage pointer (in fact, allocateStorage() does just that). This process is sometimes called upcasting. However, what if there was a way to convert a Storage pointer back into a CloudStorage pointer? Then we could call CloudStorage::getProvider() directly using that pointer, and not have to worry about virtual function resolution at all.
dynamic_cast
C++ provides a casting operator named dynamic_cast that can be used for just this purpose. Although dynamic casts have a few different capabilities, by far the most common use for dynamic casting is for converting base-class pointers into derived-class pointers. This process is called downcasting.
Using dynamic_cast works just like static_cast. Here's our example main() from above, using a dynamic_cast to convert our Storage pointer back into a CloudStorage pointer:
int main()
{
Storage* s{ allocateStorage(true) };
CloudStorage* cs{ dynamic_cast<CloudStorage*>(s) }; // use dynamic cast to convert Storage pointer into CloudStorage pointer
std::cout << "The provider of the CloudStorage is: " << cs->getProvider() << '\n';
delete s;
return 0;
}
This prints:
The provider of the CloudStorage is: AWS
dynamic_cast failure
The above example works because s is actually pointing to a CloudStorage object, so converting s into a CloudStorage pointer is successful.
However, we've made quite a dangerous assumption: that s is pointing to a CloudStorage object. What if s wasn't pointing to a CloudStorage object? This is easily tested by changing the argument to allocateStorage() from true to false. In that case, allocateStorage() will return a Storage pointer to a Storage object. When we try to dynamic_cast that to a CloudStorage, it will fail, because the conversion can't be made.
If a dynamic_cast fails, the result of the conversion will be a null pointer.
Because we haven't checked for a null pointer result, we access cs->getProvider(), which will try to dereference a null pointer, leading to undefined behavior (probably a crash).
In order to make this program safe, we need to ensure the result of the dynamic_cast actually succeeded:
int main()
{
Storage* s{ allocateStorage(true) };
CloudStorage* cs{ dynamic_cast<CloudStorage*>(s) }; // use dynamic cast to convert Storage pointer into CloudStorage pointer
if (cs) // make sure cs is non-null
std::cout << "The provider of the CloudStorage is: " << cs->getProvider() << '\n';
delete s;
return 0;
}
Always ensure your dynamic casts actually succeeded by checking for a null pointer result.
Note that because dynamic_cast does some consistency checking at runtime (to ensure the conversion can be made), use of dynamic_cast does incur a performance penalty.
Also note that there are several cases where downcasting using dynamic_cast will not work:
- With protected or private inheritance.
- For classes that do not declare or inherit any virtual functions (and thus don't have a virtual table).
- In certain cases involving virtual base classes.
Downcasting with static_cast
It turns out that downcasting can also be done with static_cast. The main difference is that static_cast does no runtime type checking to ensure that what you're doing makes sense. This makes using static_cast faster, but more dangerous. If you cast a Storage* to a CloudStorage*, it will "succeed" even if the Storage pointer isn't pointing to a CloudStorage object. This will result in undefined behavior when you try to access the resulting CloudStorage pointer (that is actually pointing to a Storage object).
If you're absolutely sure that the pointer you're downcasting will succeed, then using static_cast is acceptable. One way to ensure that you know what type of object you're pointing to is to use a virtual function. Here's one (not great) way to do that:
#include <iostream>
#include <string>
#include <string_view>
// Class identifier
enum class StorageID
{
storage,
cloud
// Others can be added here later
};
class Storage
{
protected:
int m_capacity{};
public:
Storage(int capacity)
: m_capacity{capacity}
{
}
virtual ~Storage() = default;
virtual StorageID getStorageID() const { return StorageID::storage; }
};
class CloudStorage : public Storage
{
protected:
std::string m_provider{};
public:
CloudStorage(int capacity, std::string_view provider)
: Storage{capacity}, m_provider{provider}
{
}
const std::string& getProvider() const { return m_provider; }
StorageID getStorageID() const override { return StorageID::cloud; }
};
Storage* allocateStorage(bool useCloud)
{
if (useCloud)
return new CloudStorage{1000, "AWS"};
else
return new Storage{500};
}
int main()
{
Storage* s{ allocateStorage(true) };
if (s->getStorageID() == StorageID::cloud)
{
// We already proved s is pointing to a CloudStorage object, so this should always succeed
CloudStorage* cs{ static_cast<CloudStorage*>(s) };
std::cout << "The provider of the CloudStorage is: " << cs->getProvider() << '\n';
}
delete s;
return 0;
}
But if you're going to go through all of the trouble to implement this (and pay the cost of calling a virtual function and processing the result), you might as well just use dynamic_cast.
Also consider what would happen if our object were actually some class that is derived from CloudStorage (let's call it S2). The above check s->getStorageID() == StorageID::cloud will fail because getStorageId() would return StorageID::S2, which is not equal to StorageID::cloud. Dynamic casting S2 to CloudStorage would succeed though, since a S2 is a CloudStorage!
dynamic_cast and references
Although all of the above examples show dynamic casting of pointers (which is more common), dynamic_cast can also be used with references. This works analogously to how dynamic_cast works with pointers.
#include <iostream>
#include <string>
#include <string_view>
class Storage
{
protected:
int m_capacity;
public:
Storage(int capacity)
: m_capacity{capacity}
{
}
virtual ~Storage() = default;
};
class CloudStorage : public Storage
{
protected:
std::string m_provider;
public:
CloudStorage(int capacity, std::string_view provider)
: Storage{capacity}, m_provider{provider}
{
}
const std::string& getProvider() const { return m_provider; }
};
int main()
{
CloudStorage aws{1000, "AWS"}; // create an aws
Storage& s{ aws }; // set storage reference to object
CloudStorage& cs{ dynamic_cast<CloudStorage&>(s) }; // dynamic cast using a reference instead of a pointer
std::cout << "The provider of the CloudStorage is: " << cs.getProvider() << '\n'; // we can access CloudStorage::getProvider through cs
return 0;
}
Because C++ does not have a "null reference", dynamic_cast can't return a null reference upon failure. Instead, if the dynamic_cast of a reference fails, an exception of type std::bad_cast is thrown. We talk about exceptions later in this tutorial.
dynamic_cast vs static_cast
New programmers are sometimes confused about when to use static_cast vs dynamic_cast. The answer is quite simple: use static_cast unless you're downcasting, in which case dynamic_cast is usually a better choice. However, you should also consider avoiding casting altogether and just use virtual functions.
Downcasting vs virtual functions
There are some developers who believe dynamic_cast is evil and indicative of a bad class design. Instead, these programmers say you should use virtual functions.
In general, using a virtual function should be preferred over downcasting. However, there are times when downcasting is the better choice:
- When you can not modify the base class to add a virtual function (e.g. because the base class is part of the standard library)
- When you need access to something that is derived-class specific (e.g. an access function that only exists in the derived class)
- When adding a virtual function to your base class doesn't make sense (e.g. there is no appropriate value for the base class to return). Using a pure virtual function may be an option here if you don't need to instantiate the base class.
A warning about dynamic_cast and RTTI
Run-time type information (RTTI) is a feature of C++ that exposes information about an object's data type at runtime. This capability is leveraged by dynamic_cast. Because RTTI has a pretty significant space performance cost, some compilers allow you to turn RTTI off as an optimization. Needless to say, if you do this, dynamic_cast won't function correctly.
Summary
Upcasting: Converting a derived class pointer into a base class pointer is called upcasting. C++ will implicitly perform this conversion. This process is safe and commonly used in polymorphic code.
Downcasting: Converting a base class pointer into a derived class pointer is called downcasting. This process is potentially unsafe and requires explicit casting.
dynamic_cast: The dynamic_cast operator can be used for downcasting base class pointers to derived class pointers. Dynamic_cast performs runtime type checking to ensure the conversion is valid. If the conversion fails, dynamic_cast returns a null pointer (for pointer casts) or throws a std::bad_cast exception (for reference casts).
Checking dynamic_cast results: Always check the result of a dynamic_cast to ensure it succeeded. For pointer casts, check for null. This prevents undefined behavior from dereferencing an invalid pointer.
dynamic_cast requirements: Dynamic_cast will not work with protected or private inheritance, for classes without virtual functions (no virtual table), or in certain cases involving virtual base classes.
static_cast for downcasting: Downcasting can also be done with static_cast, which is faster but performs no runtime type checking. Only use static_cast for downcasting when you are absolutely certain the conversion will succeed, as an invalid cast will result in undefined behavior.
dynamic_cast vs static_cast: Use static_cast unless you're downcasting, in which case dynamic_cast is usually a better choice. However, consider avoiding casting altogether and using virtual functions instead.
Downcasting vs virtual functions: In general, using virtual functions should be preferred over downcasting. However, downcasting is appropriate when: you cannot modify the base class to add a virtual function, you need access to derived-class-specific functionality, or adding a virtual function to the base class doesn't make sense.
RTTI dependency: Dynamic_cast relies on Run-Time Type Information (RTTI), a C++ feature that exposes object type information at runtime. If RTTI is disabled for optimization, dynamic_cast will not function correctly.
Dynamic casting provides a safe way to perform downcasting with runtime type checking, though it comes with a performance cost. While virtual functions are generally preferred for achieving polymorphic behavior, dynamic_cast is a valuable tool for situations where downcasting is necessary and appropriate.
Safe Downcasting with dynamic_cast - Quiz
Test your understanding of the lesson.
Practice Exercises
Dynamic Casting
Practice using dynamic_cast to safely convert base class pointers/references to derived class pointers/references at runtime. Learn when dynamic_cast succeeds and fails.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!