dynamic_cast Failure on Reference Types in C++

In C++, dynamic_cast is used for safely converting pointers and references up, down, and across an inheritance hierarchy at runtime using Run-Time Type Information (RTTI). While a failed dynamic_cast on a pointer safely evaluates to nullptr, a failed dynamic_cast on a reference type throws a std::bad_cast exception. This article explains why this behavioral difference exists, what happens during the failure, and how to handle it properly in your applications.

Why Reference dynamic_cast Throws an Exception

In the C++ language specification, references must always bind to a valid object; a "null reference" does not exist in standard C++.

When you use dynamic_cast on a pointer, the compiler can signify failure by returning a null pointer:

Derived* d = dynamic_cast<Derived*>(base_ptr);
if (d == nullptr) {
    // Cast failed
}

Because a reference cannot represent a null state, dynamic_cast cannot return a null reference to indicate that the source object is not of the target type. To enforce type safety and signal failure, the runtime environment throws a std::bad_cast exception instead.

The std::bad_cast Exception

The std::bad_cast class is defined in the <typeinfo> standard header and inherits from std::exception. When dynamic casting to a reference type fails:

  1. The runtime inspects the dynamic type of the operand using the class's vtable.
  2. It detects that the referenced object cannot be cast to the target reference type.
  3. It constructs and throws an instance of std::bad_cast.

If the exception is not handled with a try/catch block, std::terminate is called, resulting in the abnormal termination of the program.

Code Example: Handling a Failed Reference Cast

To safely handle reference-based dynamic_cast operations, wrap the cast inside a try/catch block:

#include <iostream>
#include <typeinfo>

class Base {
public:
    virtual ~Base() = default; // Polymorphic base required for RTTI
};

class DerivedA : public Base {};
class DerivedB : public Base {};

void process(Base& b) {
    try {
        DerivedA& a = dynamic_cast<DerivedA&>(b);
        std::cout << "Successfully cast to DerivedA\n";
    } catch (const std::bad_cast& e) {
        std::cerr << "dynamic_cast failed: " << e.what() << '\n';
    }
}

int main() {
    DerivedB b_obj;
    process(b_obj); // Throws std::bad_cast because b_obj is DerivedB, not DerivedA
    return 0;
}

Key Differences: Pointer Cast vs. Reference Cast

Feature Pointer dynamic_cast Reference dynamic_cast
Syntax dynamic_cast<T*>(ptr) dynamic_cast<T&>(ref)
Failure Mechanism Returns nullptr Throws std::bad_cast
Required Header None (built-in language feature) <typeinfo> (for catching std::bad_cast)
Control Flow Handled with standard if checks Handled with try/catch blocks
Intended Use When failure is an expected, regular outcome When failure represents an exceptional condition

Best Practices

  • Use pointer dynamic_cast for optional interfaces: If an object may or may not implement a specific interface and casting failure is a normal execution path, use pointer casts and check for nullptr. This avoids the runtime overhead associated with exception handling.
  • Use reference dynamic_cast for assertions: If an object is contractually expected to be a specific derived type, use reference casting. An unexpected type directly triggers an exception, making it ideal for enforcing invariants.