C++ std::bad_cast and Dynamic Reference Conversions

This article explores how the C++ runtime uses the std::bad_cast exception to signal failed conversions when using dynamic_cast with reference types. Unlike pointer conversions that signal failure by returning a null pointer, references must always refer to a valid object. Consequently, the C++ runtime relies on std::bad_cast to enforce type safety and halt execution flow when an invalid downcast or cross-cast occurs. Below is an explanation of the underlying mechanics, the role of Run-Time Type Information (RTTI), and how to properly handle these runtime failures.

Pointer Conversions vs. Reference Conversions

In C++, dynamic_cast is used to safely navigate inheritance hierarchies at runtime. When applied to pointers, dynamic_cast evaluates whether the pointee object can be safely converted to the target derived or sibling class. If the object cannot be converted, the expression evaluates to nullptr. This allows developers to check for success using simple conditional statements:

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

References, however, fundamentally differ from pointers. In C++, a reference must always bind to an existing object and cannot be null. Because there is no concept of a "null reference," dynamic_cast cannot return a sentinel value to indicate an invalid conversion. To preserve the invariant that references are always valid, the language standard mandates that a failed dynamic_cast on a reference type must throw an exception.

The Role of std::bad_cast

Defined in the <typeinfo> header, std::bad_cast is a standard exception derived from std::exception. When a program executes a statement such as dynamic_cast<Derived&>(base_ref), the runtime inspects the actual object bound to base_ref. If the complete object is not of type Derived (nor derived from Derived), the expression evaluates as illegal and immediately throws std::bad_cast.

#include <iostream>
#include <typeinfo>

struct Base {
    virtual ~Base() = default; // Polymorphic base class
};

struct Derived : Base {};
struct AnotherDerived : Base {};

void process(Base& b) {
    try {
        Derived& d = dynamic_cast<Derived&>(b);
        // Use d safely here
    } catch (const std::bad_cast& e) {
        std::cerr << "Invalid conversion: " << e.what() << '\n';
    }
}

If the exception is not handled within an appropriate try-catch block, it propagates up the call stack. If it leaves the main function uncaught, std::terminate() is invoked, aborting the application.

How the Runtime Detects the Failure

The ability of dynamic_cast to trigger std::bad_cast relies on Run-Time Type Information (RTTI).

  1. Polymorphism Requirement: The base class must contain at least one virtual function (typically a virtual destructor). This ensures that the compiler generates a virtual method table (vtable) for the class.
  2. Type Metadata: The vtable contains a pointer to the type's std::type_info structure, which describes the layout and inheritance relationships of the dynamic type.
  3. Runtime Validation: During execution, dynamic_cast traverses this inheritance metadata. If the target type is not found in the hierarchy of the underlying instance, the runtime determines that the reference cannot be formed and executes the throw std::bad_cast() instruction.

When to Use Reference Dynamic Casts

Using dynamic_cast with references is ideal when the conversion is mandatory for subsequent execution. Unlike pointers, where checking for nullptr is an optional step that can be accidentally omitted, reference casting forces error handling into the exception-handling model. It asserts that the dynamic type must match expectations, treating any discrepancy as an exceptional runtime failure rather than an acceptable alternate execution path.