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 |
C-style casts and their dangers
A C-style cast is a trap wrapped in brevity. It silently cycles through up to five distinct conversion semantics — in order: const_cast, static_cast, static_cast+const_cast, reinterpret_cast, reinterpret_cast+const_cast — picking whichever succeeds first, with no warning.
// C++98 / C++11 / C++17 / C++20 — same dangerous behavior throughout
struct A { int a = 1; };
struct B { int b = 2; };
const A* ca = new A;
// (1) strips const AND reinterprets — two wrongs in a single, silent cast
B* bp = (B*)ca;
// Named equivalents expose each issue separately at compile time:
// const_cast<A*>(ca) — remove const only
// static_cast<B*>(ca) — error: unrelated types
// reinterpret_cast<B*>(const_cast<A*>(ca)) — explicit and searchable
// Functional-style cast T(x) is identical for built-in types
int x = 3;
double* dp = (double*)&x; // UB: strict aliasing violation; grep won't catch it
// double* dp = reinterpret_cast<double*>(&x); // same UB, but at least visibleEnable -Wold-style-cast (GCC/Clang) to turn every C-style cast into a compile error; -Wall does not include this warning by default.
dynamic_cast and RTTI
dynamic_cast is the only cast that interrogates the actual runtime type of an object. It walks the RTTI graph embedded by the compiler into every polymorphic object, so it can navigate across multiple-inheritance lattices that static_cast cannot express.
// C++11
#include <stdexcept>
struct Animal {
virtual ~Animal() = default;
virtual void speak() const = 0;
};
struct Pet { virtual std::string name() const = 0; virtual ~Pet() = default; };
struct Dog : Animal, Pet {
void speak() const override {}
std::string name() const override { return "Rex"; }
};
Animal* a = new Dog;
// Pointer form — check result before use
if (auto* pet = dynamic_cast<Pet*>(a)) { // cross-cast: Animal* → Pet*
std::cout << pet->name() << '\n'; // "Rex"
}
// Reference form — throws std::bad_cast; use when failure is a programming error
try {
Pet& p = dynamic_cast<Pet&>(*a);
} catch (const std::bad_cast&) { /* impossible here, but catch for safety */ }
// Casting to void* yields address of the most-derived complete object
void* most_derived = dynamic_cast<void*>(a);
// dynamic_cast to the same type is a no-op (zero cost)
Animal* same = dynamic_cast<Animal*>(a); // == aRTTI storage overhead is typically 1–2 machine words of read-only data per polymorphic class. The traversal cost at runtime is proportional to the depth of the inheritance chain, not the size of the object. Disabling RTTI with -fno-rtti makes dynamic_cast ill-formed and removes all RTTI data from the binary.
reinterpret_cast: what it actually does
reinterpret_cast does not move or transform bits — it changes the type the compiler uses to reason about an address. The strict aliasing rule ([basic.lval]) then governs whether dereferencing that pointer is defined behavior.
// C++11 — pointer ↔ uintptr_t round-trip (only safe restored to original type)
int val = 0xDEADBEEF;
uintptr_t raw = reinterpret_cast<uintptr_t>(&val);
int* back = reinterpret_cast<int*>(raw); // valid: same type restored
// Memory-mapped I/O — the canonical embedded use case
// C++11 / C++14 / C++17 / C++20
volatile uint32_t* const GPIO_OUT =
reinterpret_cast<volatile uint32_t*>(0x4002'0014u);
*GPIO_OUT |= (1u << 5); // toggle pin; volatile prevents elision
// Reading the object representation — char/unsigned char/std::byte are aliasing-exempt
// C++17 (std::byte)
int n = 0x04030201;
const std::byte* b = reinterpret_cast<const std::byte*>(&n);
// b[0] == 0x01 on little-endian; reading is defined via the aliasing exemption
// Strict aliasing violation — reinterpret_cast does NOT make this safe
// C++11 / C++14 / C++17 / C++20 — always UB
alignas(float) unsigned char buf[sizeof(float)];
std::memcpy(buf, "\x00\x00\x80\x3F", 4); // bits of 1.0f
float* fp = reinterpret_cast<float*>(buf);
float f = *fp; // UB: buf was not created as a float; use std::bit_cast or memcpyThe only pointer types that may alias any other object type are char*, unsigned char*, signed char*, and std::byte*. All other pointer reinterpretations that result in a dereference require either std::memcpy or std::bit_cast to stay within defined behavior.
std::bit_cast as a safe type pun (C++20)
std::bit_cast is the only well-defined, constexpr-capable way to reinterpret the bit pattern of one type as another. It requires that both types are the same size and trivially copyable — violations are caught at compile time.
// C++20
#include <bit>
#include <limits>
// Inspect IEEE 754 single-precision layout
float f = 1.0f;
uint32_t bits = std::bit_cast<uint32_t>(f); // 0x3F800000
// Reconstruct from bits — round-trip is always exact
assert(std::bit_cast<float>(bits) == f);
// Usable in constant expressions
constexpr float kPi = 3.14159265f;
constexpr uint32_t kPiBits = std::bit_cast<uint32_t>(kPi); // evaluated at compile time
// Detect negative zero vs positive zero
constexpr bool is_negative_zero(float x) {
return std::bit_cast<uint32_t>(x) == 0x8000'0000u;
}
static_assert(!is_negative_zero(0.0f));
static_assert( is_negative_zero(-0.0f));
// Size mismatch → compile error (not a silent truncation)
// std::bit_cast<uint64_t>(3.14f); // error: sizeof(float)=4 != sizeof(uint64_t)=8For C++17 and earlier, std::memcpy between two identically-sized stack objects is the conforming alternative; modern compilers emit a single register move:
// C++17 — defined type pun via memcpy
float src = 3.14f;
uint32_t dst;
static_assert(sizeof(src) == sizeof(dst));
std::memcpy(&dst, &src, sizeof(dst)); // optimizer: one MOV, no memory trafficconst_cast: when it's valid
const_cast is safe if and only if the object being pointed to was never declared const. The distinguishing question is not whether you have a const reference — it is whether the object at the other end of that reference was originally declared with const.
// C++11
// VALID: underlying storage is non-const; removing const is safe
int mutable_value = 10;
const int& cref = mutable_value;
int& writable = const_cast<int&>(cref);
writable = 20; // ok — mutable_value is now 20
// UB: object is const; optimizer may place it in .rodata or fold uses
const int immutable = 10;
int& bad = const_cast<int&>(immutable);
bad = 20; // undefined behavior — do not do this
// Legitimate use: calling a const-incorrect C API
// The API contract guarantees buf is read-only; we just can't express that in its signature
extern "C" int legacy_hash(char* data, int len); // doesn't modify data
int safe_hash(const std::string& s) {
return legacy_hash(const_cast<char*>(s.data()),
static_cast<int>(s.size()));
}
// Overload via const_cast to avoid code duplication (canonical Meyers pattern)
// C++11
class Buffer {
char data_[64]{};
public:
const char& at(size_t i) const { return data_[i]; }
char& at(size_t i) {
// Cast *this to const to reuse the const overload, then cast result back
return const_cast<char&>(std::as_const(*this).at(i)); // C++17 as_const
}
};Adding const is always implicit and never requires const_cast. The cast is only needed for removal, and every removal should be accompanied by a comment that justifies the safety.
Narrowing conversions and gsl::narrow
Implicit narrowing conversions are a silent source of data corruption that the C++ type system does not prevent for most built-in types. Braced initialization ({}) catches narrowing at compile time, but only within an initializer — not during assignment or function calls.
// C++11 — braced-init catches narrowing in initializers
int big = 1000;
uint8_t a{big}; // error: narrowing conversion from int to uint8_t
// But assignment and function arguments silently truncate
uint8_t b = big; // compiles; b == 232 (1000 % 256) — silent data loss
void take(uint8_t);
take(big); // compiles; passes 232
// static_cast makes truncation explicit but still silently wraps
uint8_t c = static_cast<uint8_t>(big); // 232; intent is clear, value loss is not caught
// gsl::narrow — throws std::narrowing_error if the round-trip value differs
// Requires: Microsoft GSL or similar implementation
#include <gsl/narrow> // github.com/microsoft/GSL
uint8_t d = gsl::narrow<uint8_t>(255); // ok
uint8_t e = gsl::narrow<uint8_t>(1000); // throws gsl::narrowing_error at runtime
// gsl::narrow_cast — same semantics as static_cast but communicates intent
// No runtime check; use when you have already verified the value is in range
uint8_t f = gsl::narrow_cast<uint8_t>(42); // C++14
// C++20: std::in_range for compile-time or runtime range checks
#include <utility>
int val = 300;
if (std::in_range<uint8_t>(val)) { // C++20
uint8_t safe = static_cast<uint8_t>(val);
}Use gsl::narrow at trust boundaries (user input, network data, external API results) where an out-of-range value indicates a programming error. Use static_cast with a preceding std::in_range check when the narrowing is expected but must be handled gracefully.