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
- 5 named casts, each with specific purpose. Prefer over C-style
(T)x(silently picks one, sometimes wrong).
| 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
- Most common cast. Performs explicit implicit-conversion-ish operations.
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
- Refuses language-undefined conversions (e.g.,
int*↔double*) — usereinterpret_castfor those.
3. dynamic_cast and RTTI
- Runtime-checked downcast through a polymorphic hierarchy.
- Requires source to be polymorphic (declares ≥1 virtual function, or has a polymorphic base).
- Also handles non-polymorphic upcasts to accessible unambiguous bases, but
static_castis 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
- Consults the vtable for runtime type → walks hierarchy to verify cast. RTTI in action.
Costs:
- Runtime overhead — vtable lookup + hierarchy walk.
- Binary size — RTTI metadata per polymorphic class.
- Embedded toolchains often disable RTTI (
-fno-rtti) → nodynamic_castortypeidon polymorphic types.
When to use it
- Right tool when polymorphism + downcasting genuinely needed. Often a code smell — 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
- Reserve for: operation only makes sense on some derived classes (e.g.,
IDownloadable*in heterogeneous container).
4. const_cast
- Adds/removes
const(orvolatile). - Only legitimate use: interfacing with legacy C APIs taking non-
constpointers that don't actually modify.
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 truly
constobject viaconst_castis UB.
const int x = 42;
int* p = const_cast<int*>(&x);
*p = 100; // UB!
- In well-designed modern code,
const_castshould be vanishingly rare. If you reach for it, the design has a const-correctness bug.
5. reinterpret_cast
- Bit-level reinterpretation between pointer types or pointer ↔ integer. No conversion — just changes bit interpretation.
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
Used for:
- Hardware / OS interop — opaque handles, raw memory pointers.
- Network protocols — bytes as packed structs (but
bit_castis safer for fixed-size). - Hashing on byte sequences — object as byte buffer.
Caveats:
- Strict aliasing — accessing object through unrelated pointer is UB (narrow exceptions:
char*,std::byte*,unsigned char*). reinterpret_cast<T*>(p)only safe to dereference ifTis the actual type at that address (or a permitted alias).- Prefer
std::bit_cast(C++20) overreinterpret_cast+memcpyfor 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
- C-style cast tries
static_cast,const_cast,reinterpret_castin order — picks whichever succeeds. Dangerous:
- Unclear intent. Casting away
const? Reinterpreting? Truncating? - Surprises. Code change can flip
static_cast→reinterpret_cast(or vice versa) silently. - Hard to grep. Named casts (
static_cast<T>) are findable;(T)xis everywhere.
- Always use a named cast. Reviewers should reject C-style casts.
7. std::bit_cast (C++20)
std::bit_cast<T>(x)— reinterpret bits of one type as another, same size. Type-safe replacement forreinterpret_cast+memcpy.
#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
Requires:
- Same size —
sizeof(T) == sizeof(U). - Trivially copyable — no non-trivial copy semantics.
vs reinterpret_cast:
constexprsince C++20 — usable at compile time (when neither type contains unions, pointers, member pointers, references, volatile).- No strict-aliasing violation — produces a fresh object.
- Compile-time size check.
Use for:
- Float ↔ int bit patterns (IEEE 754 fields).
- Binary format read/write.
- Hash function input.
8. typeid and type_info
typeid(x)→const std::type_info&describing runtime type ofx. For type-introspection logging, type-keyed maps, 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());
- Requires RTTI enabled. Like
dynamic_cast, disabled in some embedded/kernel codebases.