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).
- 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.
- Type Metadata: The vtable contains a pointer to the
type's
std::type_infostructure, which describes the layout and inheritance relationships of the dynamic type. - Runtime Validation: During execution,
dynamic_casttraverses 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 thethrow 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.