Skip to content
C++
Language
since C++98
Intermediate

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++98

Run-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 a const 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

cpp
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 stripped

Behaviour rules:

FormOn failure
Pointer castReturns a null pointer of type T*
Reference castThrows std::bad_cast
typeid(*p) with null p (polymorphic)Throws std::bad_typeid
UpcastAlways succeeds, resolved at compile time

A null pointer operand to dynamic_cast yields a null result, with no exception.

Examples

Safe downcast with fallback

cpp
#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

cpp
#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

cpp
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

cpp
#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

cpp
void* most_derived(Shape* p) { return dynamic_cast<void*>(p); } // C++98

Useful 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::variant with std::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_cast before use.
  • Use typeid for exact-type tests only. typeid(a) == typeid(b) fails for a derived type, while dynamic_cast<Base*> succeeds. Pick the one matching your intent (for example, typeid in operator== 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_cast in hot loops; cache the downcast or restructure with a type tag.
  • Prefer std::type_index over type_info* as keys (since C++11).
  • Use static_cast only when the type is guaranteed by an invariant, ideally with a debug assertion that verifies using dynamic_cast:
cpp
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 if Base has 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) and dynamic_cast reflect 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; use abi::__cxa_demangle for display, which is non-portable).
  • typeid on a dereferenced null pointer. typeid(*p) with a null polymorphic p throws std::bad_typeid. For typeid(p) (the pointer itself), no dereference occurs and the result is the static pointer type.
  • Expecting typeid to strip derived types on non-polymorphic objects. typeid(*basePtr) for a non-polymorphic Base is the static type, silently.
  • Performance assumptions. dynamic_cast can 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) and typeid then 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_info is duplicated across DSOs (hidden visibility, missing key function), dynamic_cast and typeid comparisons can fail between modules. Define a key function (the first non-inline, non-pure virtual function) or export the type consistently.
  • Constness. dynamic_cast cannot remove const. Use dynamic_cast<const Derived*> on a const Base*.

Version notes

FeatureStandard
dynamic_cast, typeid, type_info, bad_cast, bad_typeidC++98
type_info::hash_code(), std::type_indexC++11
type_info noexcept on name(), before()C++11
!= synthesized from == for type_infoC++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.

See Also