Skip to content
C++
Language
since C++98
Intermediate

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++98

Named 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:

  1. static_cast — any well-defined, compile-time-checked conversion
  2. dynamic_cast — verified downcast or cross-cast through a polymorphic hierarchy at runtime
  3. const_cast — add or remove const/volatile qualifiers only
  4. reinterpret_cast — raw pointer or integer reinterpretation
  5. std::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.

cpp
// 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.

cpp
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.

cpp
// 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 snapshot

const_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.

cpp
// 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 instead

The 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.

cpp
#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)==8

For C++17 and earlier, std::memcpy is the defined alternative — compilers optimize it to a register move:

cpp
// 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 behavior

C-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:

cpp
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 behavior

Functional-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_cast for all downcasts where the dynamic type is not statically known. The RTTI cost is worth the safety guarantee; an unchecked static_cast downcast to the wrong type produces UB that is nearly impossible to diagnose.
  • Keep const_cast at 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_cast as 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, not reinterpret_cast + dereference. For C++17 and earlier, use std::memcpy into 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_cast in 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:

cpp
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 safely

const_cast on a genuinely const object:

cpp
const std::string msg = "hello";
char* p = const_cast<char*>(msg.data());
p[0] = 'H';   // UB; the optimizer assumes msg is immutable

reinterpret_cast dereference — strict aliasing violation:

cpp
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 memcpy

dynamic_cast on a non-polymorphic type:

cpp
struct Plain { int x; };   // no virtual functions
Plain* p = new Plain;
Plain* q = dynamic_cast<Plain*>(p);  // error: 'Plain' is not polymorphic

Mixing 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

CastPurposeCompile checkRuntime checkUB risk
static_castWell-defined type conversionsYesNoWrong downcast
dynamic_castSafe downcast / cross-castPartialYes (RTTI)None if result checked
const_castAdd / remove cv-qualifiersYesNoWriting through removed-const
reinterpret_castPointer / integer reinterpretationNoNoStrict aliasing
std::bit_cast (C++20)Type-pun same-size trivial typesYesNoNone
(T)exprOpaque coercionNoVariesUnpredictable

See Also