RTTI and dynamic_cast
Runtime type information in C++: dynamic_cast, typeid, std::type_info, their costs, failure modes, and when to prefer virtual dispatch or variants.
RTTI (dynamic_cast / typeid)since C++98Run-time type information is the language facility that lets a program query the dynamic type of an object of polymorphic class type via typeid and perform checked conversions within a class hierarchy via dynamic_cast.
Overview
RTTI consists of three parts, all available since C++98:
dynamic_cast<T>(expr): a checked conversion that inspects the most-derived object at run time.typeid(expr-or-type): yields a reference to aconst std::type_info(header<typeinfo>).std::type_info: an opaque descriptor supporting equality,before(),name(), and (since C++11)hash_code().
The central restriction: both dynamic_cast (for source operands) and the dynamic behaviour of typeid apply only to polymorphic types, meaning classes with at least one virtual function. For non-polymorphic types typeid is resolved statically and dynamic_cast downcasts do not compile.
Unlike static_cast, which trusts the programmer and uses only compile-time information, dynamic_cast consults the actual object. It also supports conversions that static_cast cannot do: cross-casts (sideways between unrelated bases of a multiply-derived object) and casts to void* (yielding the address of the most-derived object).
Syntax
dynamic_cast<T*>(p) // p is pointer to polymorphic class; failure yields nullptr
dynamic_cast<T&>(r) // failure throws std::bad_cast
dynamic_cast<void*>(p) // address of most-derived object (p must be polymorphic)
typeid(expr) // expr of polymorphic glvalue type: dynamic type
typeid(type) // static type; cv-qualifiers and references strippedBehaviour rules:
| Form | On failure |
|---|---|
| Pointer cast | Returns a null pointer of type T* |
| Reference cast | Throws std::bad_cast |
typeid(*p) with null p (polymorphic) | Throws std::bad_typeid |
| Upcast | Always succeeds, resolved at compile time |
A null pointer operand to dynamic_cast yields a null result, with no exception.
Examples
Safe downcast with fallback
#include <iostream>
#include <memory>
#include <vector>
struct Shape { virtual ~Shape() = default; };
struct Circle : Shape { double r = 1.0; };
struct Rect : Shape { double w = 2, h = 3; };
double area(const Shape& s) {
if (auto* c = dynamic_cast<const Circle*>(&s)) // C++98
return 3.14159265 * c->r * c->r;
if (auto* r = dynamic_cast<const Rect*>(&s))
return r->w * r->h;
return 0.0;
}
int main() {
std::vector<std::unique_ptr<Shape>> v; // C++11
v.push_back(std::make_unique<Circle>()); // C++14
v.push_back(std::make_unique<Rect>());
for (auto& s : v) std::cout << area(*s) << '\n';
}Since C++17 you can scope the result with an init-statement: if (auto* c = dynamic_cast<Circle*>(p); c) { ... }.
Reference cast and exceptions
#include <typeinfo>
void render(Shape& s) {
try {
Circle& c = dynamic_cast<Circle&>(s);
// use c
} catch (const std::bad_cast&) {
// s is not a Circle
}
}Cross-cast
struct Serializable { virtual ~Serializable() = default; virtual void save() = 0; };
struct Drawable { virtual ~Drawable() = default; virtual void draw() = 0; };
struct Sprite : Drawable, Serializable { void draw() override {} void save() override {} };
void persist(Drawable& d) {
// sideways: Drawable -> Serializable, impossible with static_cast
if (auto* s = dynamic_cast<Serializable*>(&d))
s->save();
}typeid and type_info
#include <typeindex> // C++11
#include <typeinfo>
#include <unordered_map>
#include <functional>
void log_type(const Shape& s) {
std::cout << typeid(s).name() << '\n'; // implementation-defined, often mangled
}
bool same_dynamic_type(const Shape& a, const Shape& b) {
return typeid(a) == typeid(b); // exact match, ignores inheritance
}
// std::type_index (C++11) is copyable, hashable and ordered: usable as a map key
std::unordered_map<std::type_index, std::function<void(const Shape&)>> handlers;
void dispatch(const Shape& s) {
if (auto it = handlers.find(typeid(s)); it != handlers.end()) // C++17 init-if
it->second(s);
}Compare type_info objects with operator==, never by comparing addresses: the standard does not guarantee one object per type. type_info is not copyable; use std::type_index for containers. Since C++20, operator!= is synthesized from ==, and type_info has no operator<=>; use before() or type_index for ordering.
Most-derived address
void* most_derived(Shape* p) { return dynamic_cast<void*>(p); } // C++98Useful for identity comparison across different base subobjects of the same object.
Best Practices
- Prefer virtual functions. If you frequently downcast, the base interface is likely too thin. Add a virtual operation, use the Visitor pattern, or use
std::variantwithstd::visit(C++17) for closed type sets. - Use the pointer form when failure is expected, and the reference form when failure is a contract violation.
- Always check the result of a pointer
dynamic_castbefore use. - Use
typeidfor exact-type tests only.typeid(a) == typeid(b)fails for a derived type, whiledynamic_cast<Base*>succeeds. Pick the one matching your intent (for example,typeidinoperator==implementations to prevent slicing-style asymmetric equality). - Give polymorphic bases a virtual destructor. This makes the class polymorphic, enables RTTI, and makes deletion through the base well-defined.
- Cast once, then reuse. Avoid repeated chains of
dynamic_castin hot loops; cache the downcast or restructure with a type tag. - Prefer
std::type_indexovertype_info*as keys (since C++11). - Use
static_castonly when the type is guaranteed by an invariant, ideally with a debug assertion that verifies usingdynamic_cast:
template <class D, class B>
D* checked_down(B* b) {
assert(dynamic_cast<D*>(b) == static_cast<D*>(b)); // debug-only validation
return static_cast<D*>(b);
}Common Pitfalls
- Non-polymorphic source type.
dynamic_cast<Derived*>(basePtr)is ill-formed ifBasehas no virtual functions. Adding a virtual destructor fixes it. - Ignoring null. Dereferencing the result of a failed pointer cast is undefined behaviour.
- Private or ambiguous bases. A cast to an inaccessible or ambiguous base fails at run time (null or
bad_cast), not at compile time. With repeated non-virtual inheritance, a downcast to an ambiguous base yields null. - Using it during construction/destruction. Within a constructor or destructor, the dynamic type is the class currently being constructed or destroyed, so
typeid(*this)anddynamic_castreflect that class, not the eventual most-derived one. - Relying on
name(). The string is implementation-defined, not guaranteed unique or human-readable (GCC and Clang return Itanium-mangled names; useabi::__cxa_demanglefor display, which is non-portable). typeidon a dereferenced null pointer.typeid(*p)with a null polymorphicpthrowsstd::bad_typeid. Fortypeid(p)(the pointer itself), no dereference occurs and the result is the static pointer type.- Expecting
typeidto strip derived types on non-polymorphic objects.typeid(*basePtr)for a non-polymorphicBaseis the static type, silently. - Performance assumptions.
dynamic_castcan involve walking the inheritance graph and string comparisons of type names on some ABIs (notably across shared-library boundaries). Deep hierarchies and multiple inheritance are slower. Measure before replacing it, but avoid it in tight loops. -fno-rtti//GR-. Compilers can disable RTTI to save binary size.dynamic_cast(for polymorphic downcasts) andtypeidthen become unavailable or ill-formed, and libraries built with different settings may fail to link or misbehave. Exceptions still work, as the compiler retains the type data it needs for them.- Shared libraries and visibility. If a class's
type_infois duplicated across DSOs (hidden visibility, missing key function),dynamic_castandtypeidcomparisons can fail between modules. Define a key function (the first non-inline, non-pure virtual function) or export the type consistently. - Constness.
dynamic_castcannot removeconst. Usedynamic_cast<const Derived*>on aconst Base*.
Version notes
| Feature | Standard |
|---|---|
dynamic_cast, typeid, type_info, bad_cast, bad_typeid | C++98 |
type_info::hash_code(), std::type_index | C++11 |
type_info noexcept on name(), before() | C++11 |
!= synthesized from == for type_info | C++20 |
std::bad_cast / bad_typeid remain in <typeinfo> | all versions |
Reflection proposals for later standards aim at compile-time introspection that could replace many RTTI use cases, but they are not part of any published standard as of C++23; do not depend on them in portable code.