C++ Casts
C++ cast operators — static_cast, dynamic_cast, const_cast, reinterpret_cast, std::bit_cast — what each does, when to use it, and what can go wrong.
C++ Named Castssince C++98Named cast operators — static_cast, dynamic_cast, const_cast, reinterpret_cast (C++98), and std::bit_cast (C++20) — replace C-style casts with explicit, searchable, semantically distinct operators that make the programmer's intent clear and expose misuse at compile time.
Overview
A C-style cast (T)expr silently attempts a sequence of coercions and succeeds in ways that obscure bugs. Named casts each have one well-defined job: the compiler enforces the semantics, and any reader immediately knows what kind of conversion is happening.
Selection order for any given conversion:
static_cast— any well-defined, compile-time-checked conversiondynamic_cast— verified downcast or cross-cast through a polymorphic hierarchy at runtimeconst_cast— add or removeconst/volatilequalifiers onlyreinterpret_cast— raw pointer or integer reinterpretationstd::bit_cast(C++20) — type-pun between same-size trivially-copyable types
Use named casts exclusively. Enable -Wold-style-cast (GCC/Clang) to catch any that slip through.
static_cast
The default cast. Performs conversions that are valid according to the type system at compile time, with no runtime overhead.
// Numeric narrowing — truncates, no UB for in-range float→int
double d = 3.7;
int i = static_cast<int>(d); // 3
// Integer overflow is implementation-defined in C++17 and earlier;
// well-defined two's-complement since C++20
unsigned u = 0xFFFF'FFFFu;
int n = static_cast<int>(u); // defined since C++20
// Enum ↔ underlying type (scoped enums: C++11)
enum class Dir : uint8_t { North, South, East, West };
uint8_t raw = static_cast<uint8_t>(Dir::East); // 2
Dir dir = static_cast<Dir>(3); // West
// Upcast — redundant (implicit conversion exists) but communicates intent
struct Base { virtual ~Base() = default; };
struct Derived : Base { int x = 42; };
Derived obj{};
Base* bp = static_cast<Base*>(&obj);
// Downcast — no runtime check; UB if dynamic type is wrong
Derived* dp = static_cast<Derived*>(bp); // safe only if bp really is Derived*
// void* round-trip — required for C API interop
void* vp = static_cast<void*>(&obj);
Derived* again = static_cast<Derived*>(vp); // must restore same original type
// Invoke an explicit conversion operator (C++11)
struct Proxy {
explicit operator int() const { return 7; }
};
int val = static_cast<int>(Proxy{}); // calls explicit operator int()static_cast does not perform runtime type checking. A downcast to the wrong type produces undefined behavior. For verified downcasts, use dynamic_cast. static_cast is usable in constexpr contexts since C++11.
dynamic_cast
Performs a runtime-checked navigation of a polymorphic hierarchy using RTTI. The hierarchy must contain at least one virtual function; otherwise the cast is ill-formed.
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle : Shape {
double r;
double area() const override { return 3.14159265 * r * r; }
};
struct Colored { virtual int rgb() const = 0; };
struct ColoredCircle : Circle, Colored {
int rgb() const override { return 0xFF6600; }
};
Shape* shape = new ColoredCircle{{}, 5.0};
// Pointer form: returns nullptr on failure — prefer in production code
if (auto* cc = dynamic_cast<ColoredCircle*>(shape)) {
std::cout << cc->rgb(); // safe
}
// Cross-cast: navigate to an unrelated base that the dynamic type satisfies
// static_cast cannot do this — it has no knowledge of the full object layout
if (auto* col = dynamic_cast<Colored*>(shape)) {
std::cout << col->rgb(); // works via RTTI walk through ColoredCircle
}
// Reference form: throws std::bad_cast on failure
try {
Circle& c = dynamic_cast<Circle&>(*shape); // ok
// Cast to some other unrelated type would throw here
} catch (const std::bad_cast& e) { /* handle */ }
// Cast to void* yields the address of the most-derived object
void* base_addr = dynamic_cast<void*>(shape);Cost: dynamic_cast traverses the RTTI graph at runtime; cost scales with hierarchy depth. It can be disabled with -fno-rtti, which makes any dynamic_cast in that translation unit ill-formed.
Design signal: Frequent dynamic_cast branching on concrete types is a smell. If the base class interface cannot express the required behavior through virtual dispatch, redesign it rather than accumulating casts.
const_cast
The only cast that modifies cv-qualifiers (const and volatile). Adding const is implicit; const_cast exists primarily to remove it when calling APIs that lack proper const-correctness.
// Safe: underlying object is genuinely non-const
int value = 42;
const int& cref = value;
int& mref = const_cast<int&>(cref);
mref = 99; // ok — 'value' was not const
// UB: object was declared const; the optimizer may place it in read-only memory
const int immutable = 42;
int& bad = const_cast<int&>(immutable);
bad = 99; // undefined behavior
// Legitimate use: const-incorrect legacy C API
void legacy_write(char* buf, size_t len); // doesn't modify buf in practice
void call_legacy(const std::string& s) {
// Safe only if we know legacy_write will not write through buf
legacy_write(const_cast<char*>(s.data()), s.size());
}
// volatile removal (rare — embedded or signal-handler contexts)
volatile uint32_t hw_status = 0;
uint32_t snap = const_cast<uint32_t&>(hw_status); // one-time snapshotconst_cast has zero runtime cost. If you own the API, fix its const-correctness instead of using this cast.
reinterpret_cast
Reinterprets the address or bit pattern of a pointer or integer type. It does not inspect or transform the stored bits — it instructs the compiler to treat the same address as a different type. The strict aliasing rule (C++ [basic.lval]) makes most dereferences through a reinterpreted pointer undefined behavior.
// Pointer ↔ integer (round-trip only; use uintptr_t for pointer-sized storage)
int x = 42;
uintptr_t addr = reinterpret_cast<uintptr_t>(&x);
int* back = reinterpret_cast<int*>(addr); // ok: same original type
// Memory-mapped hardware register (common in embedded/systems code)
volatile uint32_t* const UART0_DR =
reinterpret_cast<volatile uint32_t*>(0x101F1000u);
*UART0_DR = 'A';
// POSIX dlsym pattern — the only standard-blessed function-pointer reinterpretation
// POSIX mandates that void* ↔ function pointer works on conforming platforms
using TransformFn = int(*)(int);
void* sym = dlsym(handle, "transform");
TransformFn fn = reinterpret_cast<TransformFn>(sym);
// Reading raw bytes via unsigned char* or std::byte* — aliasing-exempt types
// char*, unsigned char*, and std::byte* may alias any object type
int n = 0x01020304;
const auto* bytes = reinterpret_cast<const unsigned char*>(&n);
// bytes[0] is the least-significant byte on little-endian platforms
// WRONG — strict aliasing violation; reading *ip is UB
float f = 3.14f;
int* ip = reinterpret_cast<int*>(&f);
int bits = *ip; // UB; use std::bit_cast<int>(f) in C++20 insteadThe aliasing-exempt types (char, unsigned char, std::byte, and their signed variants) are the only safe targets for pointer reinterpretation when reading the object representation.
std::bit_cast — Type Punning Without UB (C++20)
std::bit_cast<To>(from) copies the bit representation of from into a new To object. Both types must be the same size and trivially copyable. Unlike reinterpret_cast + dereference, this is well-defined behavior and is constexpr.
#include <bit> // C++20
// Inspect IEEE 754 float layout — defined on any platform
float f = -1.0f;
uint32_t bits = std::bit_cast<uint32_t>(f); // 0xBF800000
uint32_t sign = bits >> 31; // 1
uint32_t exponent = (bits >> 23) & 0xFF; // 127 (biased exponent for -1.0)
uint32_t mantissa = bits & 0x7FFFFF; // 0
// Round-trip is exact
assert(std::bit_cast<float>(bits) == f);
// constexpr — evaluatable at compile time
constexpr uint32_t kQNaN =
std::bit_cast<uint32_t>(std::numeric_limits<float>::quiet_NaN());
// Size mismatch: caught at compile time
// std::bit_cast<int64_t>(3.14f); // error: sizeof(float)==4 != sizeof(int64_t)==8For C++17 and earlier, std::memcpy is the defined alternative — compilers optimize it to a register move:
// C++17: defined type punning via memcpy
float f = 3.14f;
uint32_t bits;
static_assert(sizeof(f) == sizeof(bits));
std::memcpy(&bits, &f, sizeof(bits)); // no actual copy generated; defined behaviorC-Style Cast — Never Use
A C-style cast (T)expr tries conversions in this order until one succeeds: const_cast, static_cast, static_cast+const_cast, reinterpret_cast, reinterpret_cast+const_cast. It silently picks whichever works first:
struct A {};
struct B {};
A* a = new A;
// C-style: silently falls through to reinterpret_cast when static_cast fails
B* b1 = (B*)a; // compiles without warning; almost certainly wrong at runtime
// Named cast: compile error — intent is unambiguous
B* b2 = static_cast<B*>(a); // error: no valid conversion from A* to B*
// C-style also strips const silently
const int ci = 42;
int* p = (int*)&ci; // compiles; writing through p is UB
int* q = static_cast<int*>(&ci); // error: cannot cast away const — correct behaviorFunctional-style casts T(expr) behave identically to C-style casts for built-in types and are equally problematic.
Best Practices
- Default to
static_cast. It fails at compile time for invalid conversions and has no runtime cost. - Use
dynamic_castfor all downcasts where the dynamic type is not statically known. The RTTI cost is worth the safety guarantee; an uncheckedstatic_castdowncast to the wrong type produces UB that is nearly impossible to diagnose. - Keep
const_castat API boundaries only. If you own the API, fix its const-correctness. If you don't, document in a comment exactly why the cast is safe (i.e., that the underlying object is not const). - Treat every
reinterpret_castas a systems-level primitive. Each use should carry a comment explaining which aliasing exemption or platform guarantee makes it safe. - Use
std::bit_cast(C++20) for type punning, notreinterpret_cast+ dereference. For C++17 and earlier, usestd::memcpyinto an aligned buffer. - Never use C-style or functional casts in C++ code. The compiler cannot warn you when they silently pick
reinterpret_cast. - Frequent
dynamic_castin business logic is a refactoring signal. Express the behavior through virtual dispatch on the base interface instead.
Common Pitfalls
static_cast downcast to wrong dynamic type — silent UB:
struct Base { virtual ~Base() = default; };
struct A : Base { int a = 1; };
struct B : Base { int b = 2; };
Base* p = new A;
B* bp = static_cast<B*>(p); // compiles; UB at point of access
int x = bp->b; // reads garbage; may corrupt memory or crash
// Fix: dynamic_cast<B*>(p) returns nullptr safelyconst_cast on a genuinely const object:
const std::string msg = "hello";
char* p = const_cast<char*>(msg.data());
p[0] = 'H'; // UB; the optimizer assumes msg is immutablereinterpret_cast dereference — strict aliasing violation:
double d = 1.0;
long long* llp = reinterpret_cast<long long*>(&d);
*llp = 0; // UB; compiler assumes long long* and double* do not alias
// Fix: std::bit_cast<long long>(d) or memcpydynamic_cast on a non-polymorphic type:
struct Plain { int x; }; // no virtual functions
Plain* p = new Plain;
Plain* q = dynamic_cast<Plain*>(p); // error: 'Plain' is not polymorphicMixing RTTI across translation units: Compiling some TUs with -fno-rtti makes dynamic_cast ill-formed or always return nullptr in those units. Ensure consistent RTTI settings across the entire link.
Quick Reference
| Cast | Purpose | Compile check | Runtime check | UB risk |
|---|---|---|---|---|
static_cast | Well-defined type conversions | Yes | No | Wrong downcast |
dynamic_cast | Safe downcast / cross-cast | Partial | Yes (RTTI) | None if result checked |
const_cast | Add / remove cv-qualifiers | Yes | No | Writing through removed-const |
reinterpret_cast | Pointer / integer reinterpretation | No | No | Strict aliasing |
std::bit_cast (C++20) | Type-pun same-size trivial types | Yes | No | None |
(T)expr | Opaque coercion | No | Varies | Unpredictable |