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

  • 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*) — use reinterpret_cast for 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_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

  • Consults the vtable for runtime type → walks hierarchy to verify cast. RTTI in action.

Costs:

  1. Runtime overhead — vtable lookup + hierarchy walk.
  2. Binary size — RTTI metadata per polymorphic class.
  • Embedded toolchains often disable RTTI (-fno-rtti) → no dynamic_cast or typeid on 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 (or volatile).
  • Only legitimate use: interfacing with legacy C APIs taking non-const pointers 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 const object via const_cast is UB.
const int x = 42;
int* p = const_cast<int*>(&x);
*p = 100;                // UB!
  • In well-designed modern code, const_cast should 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:

  1. Hardware / OS interop — opaque handles, raw memory pointers.
  2. Network protocols — bytes as packed structs (but bit_cast is safer for fixed-size).
  3. Hashing on byte sequences — object as byte buffer.

Caveats:

  1. Strict aliasing — accessing object through unrelated pointer is UB (narrow exceptions: char*, std::byte*, unsigned char*).
  2. reinterpret_cast<T*>(p) only safe to dereference if T is the actual type at that address (or a permitted alias).
  3. Prefer std::bit_cast (C++20) over reinterpret_cast + memcpy for 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_cast in order — picks whichever succeeds. Dangerous:
  1. Unclear intent. Casting away const? Reinterpreting? Truncating?
  2. Surprises. Code change can flip static_castreinterpret_cast (or vice versa) silently.
  3. Hard to grep. Named casts (static_cast<T>) are findable; (T)x is 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 for reinterpret_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:

  1. Same sizesizeof(T) == sizeof(U).
  2. Trivially copyable — no non-trivial copy semantics.

vs reinterpret_cast:

  1. constexpr since C++20 — usable at compile time (when neither type contains unions, pointers, member pointers, references, volatile).
  2. No strict-aliasing violation — produces a fresh object.
  3. 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 of x. 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.