C++ Casts and RTTI
- Description: A note on the five C++ casts (
static_cast,dynamic_cast,const_cast,reinterpret_cast,bit_cast), the C-style cast and why to avoid it, and runtime type information (typeid,type_info) - My Notion Note ID: K2A-B1-10
- Created: 2018-09-25
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. The Five Casts
- 2.
static_cast - 3.
dynamic_castand RTTI - 4.
const_cast - 5.
reinterpret_cast - 6. C-Style Cast (and Why to Avoid It)
- 7.
std::bit_cast(C++20) - 8.
typeidandtype_info
1. The Five Casts
C++ has five named casts, each with a specific purpose and clear semantics. Prefer them over the C-style (T)x cast, which silently picks one of these (sometimes the wrong one).
| Cast | When to use |
|---|---|
static_cast<T>(x) |
Well-defined, compile-time-checkable conversions (numeric types, base/derived pointers without runtime check, void*) |
dynamic_cast<T>(x) |
Safe downcast in a polymorphic class hierarchy; runtime check |
const_cast<T>(x) |
Add or remove const / volatile. Almost always a code smell |
reinterpret_cast<T>(x) |
Bit-level reinterpretation between unrelated pointer types or pointer ↔ integer |
std::bit_cast<T>(x) (C++20) |
Type-safe bit-level reinterpretation between same-sized types (replaces reinterpret_cast + memcpy) |
2. static_cast
The most common cast. Performs implicit-conversion-ish operations explicitly.
double d = 3.14;
int i = static_cast<int>(d); // 3 (truncation)
void* p = malloc(100);
char* buf = static_cast<char*>(p); // void* -> char*
// Up- and downcasts in a class hierarchy (no runtime check!)
struct Base {};
struct Derived : Base {};
Derived d;
Base* bp = static_cast<Base*>(&d); // upcast — always safe
Derived* dp = static_cast<Derived*>(bp); // downcast — UB if bp doesn't actually point to Derived
// Enum ↔ underlying type
enum class E { A = 1, B = 2 };
int n = static_cast<int>(E::A); // 1
E e = static_cast<E>(2); // E::B
static_cast will refuse conversions that the language doesn't define (e.g., int* ↔ double*). For those, you'd need reinterpret_cast.
3. dynamic_cast and RTTI
dynamic_cast is a runtime-checked downcast through a polymorphic class hierarchy. The runtime check (and casts to void*) require the source type to be polymorphic — i.e., it (or a base) declares at least one virtual function. Non-polymorphic upcasts to an accessible, unambiguous base class are also allowed by dynamic_cast, though static_cast is conventional for those.
struct Animal { virtual ~Animal() = default; };
struct Dog : Animal { void bark(); };
struct Cat : Animal { void meow(); };
Animal* a = make_animal(); // could be Dog or Cat
// Pointer cast — returns nullptr on failure
if (Dog* d = dynamic_cast<Dog*>(a)) {
d->bark();
}
// Reference cast — throws std::bad_cast on failure
try {
Dog& d = dynamic_cast<Dog&>(*a);
d.bark();
} catch (const std::bad_cast&) {
// not a Dog
}
How it works
dynamic_cast consults the vtable to find the runtime type of the object, then walks the class hierarchy to verify the cast is valid. This is runtime type information (RTTI) in action.
Costs:
- Runtime overhead (a virtual table lookup plus a hierarchy walk).
- Binary size overhead (RTTI metadata for every polymorphic class).
Some embedded toolchains disable RTTI (-fno-rtti); if you target those, you can't use dynamic_cast or typeid on polymorphic types.
When to use it
dynamic_cast is the right tool when polymorphism + downcasting is genuinely needed. But it's often a code smell — usually a virtual function is cleaner:
// Avoid dynamic_cast where possible:
struct Animal { virtual void speak() = 0; };
struct Dog : Animal { void speak() override; };
struct Cat : Animal { void speak() override; };
Animal* a = make_animal();
a->speak(); // virtual dispatch — no cast needed
Use dynamic_cast mainly when the operation only makes sense for some derived classes (e.g., IDownloadable* for objects that happen to be downloadable in a heterogeneous container).
4. const_cast
Adds or removes const (or volatile). The only legitimate use is when interfacing with legacy C APIs that take non-const pointers but logically don't modify the data.
void legacy_print(char* msg); // non-const, but doesn't actually modify
void better(const std::string& s) {
legacy_print(const_cast<char*>(s.c_str())); // OK if legacy_print really doesn't modify
}
Modifying a const object via const_cast is undefined behavior. Don't do it:
const int x = 42;
int* p = const_cast<int*>(&x);
*p = 100; // UB!
In well-designed modern C++, const_cast should be vanishingly rare. If you're casting away const, you're working around a design issue (incorrect const-correctness somewhere).
5. reinterpret_cast
Bit-level reinterpretation between pointer types or pointer ↔ integer. The compiler does no conversion; it just changes how the bits are interpreted.
int n = 0x12345678;
char* p = reinterpret_cast<char*>(&n);
// *p reads one byte of n's representation (little- vs big-endian dependent)
uintptr_t addr = reinterpret_cast<uintptr_t>(&n); // pointer → integer
int* p2 = reinterpret_cast<int*>(addr); // integer → pointer
Mostly used for:
- Hardware / OS interop — converting opaque handles, raw memory pointers.
- Network protocols — reading bytes as packed structs (although
bit_castis safer for fixed-size types). - Hash tables on byte sequences — treating an object as a byte buffer.
Caveats:
- Strict aliasing rules say accessing an object through an unrelated pointer type is undefined behavior (with narrow exceptions for
char*,std::byte*, andunsigned char*). - The result of
reinterpret_cast<T*>(p)is only safe to dereference ifTis the actual type at that address (or a permitted alias). - Use
std::bit_cast(C++20) instead ofreinterpret_cast+memcpyfor type-safe value reinterpretation (see § 7).
6. C-Style Cast (and Why to Avoid It)
double d = 3.14;
int i = (int)d; // C-style cast
const char* s = (const char*)0; // nullptr cast
A C-style cast tries each of static_cast, const_cast, reinterpret_cast in order until one works. The result depends silently on which combination succeeded — which makes it dangerous:
- Unclear intent. Is the author casting away
const? Reinterpreting bits? Truncating? - Surprises. A code change can flip the cast from
static_casttoreinterpret_cast(or vice versa) without warning. - Hard to grep. Named casts (
static_cast<T>) are easy to find;(T)xis everywhere.
Always use a named cast. Modern code reviewers should reject C-style casts.
7. std::bit_cast (C++20)
std::bit_cast<T>(x) reinterprets the bits of an object of one type as another, when both types have the same size. It's the type-safe replacement for reinterpret_cast + memcpy for value reinterpretation.
#include <bit>
#include <cstdint>
float f = 1.5f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(f); // raw IEEE 754 bits
// Reverse:
float back = std::bit_cast<float>(bits); // 1.5f
bit_cast requires:
- Same size:
sizeof(T) == sizeof(U). - Trivially copyable: both types have no non-trivial copy semantics.
Compared to reinterpret_cast:
bit_castisconstexprsince C++20 — usable at compile time when neither type contains a union, pointer, member pointer, reference, or volatile member.bit_castdoesn't violate strict aliasing — it produces a fresh object, not an alias.bit_castchecks the size at compile time.
Use bit_cast for:
- Float ↔ int bit patterns (extracting / setting IEEE 754 fields)
- Reading / writing binary formats
- Hash function input
8. typeid and type_info
typeid(x) returns a const std::type_info& describing the runtime type of x. Useful for type-introspection logging, type-keyed maps, and serialization frameworks.
#include <typeinfo>
const char* name = typeid(int).name(); // implementation-defined
// (e.g. "i" on gcc; demangle for human form)
struct Base { virtual ~Base() = default; };
struct Derived : Base {};
Base* b = new Derived;
const std::type_info& t = typeid(*b); // runtime: Derived
bool same = (t == typeid(Derived)); // true
// For non-polymorphic types, typeid is evaluated at compile time
typeid(int) == typeid(int); // true, no runtime cost
Common uses
// Type-keyed cache:
std::unordered_map<std::type_index, std::any> registry;
registry[std::type_index(typeid(MyClass))] = my_value;
// Type-name logging:
std::cout << boost::core::demangle(typeid(*ptr).name());
typeid requires RTTI to be enabled. Like dynamic_cast, it's disabled in some embedded / kernel codebases.