Casting
Four casts, four different guarantees — and why a C-style cast hides which one you're getting.
Intermediate
(type)expr silently picks whichever of static_cast,
const_cast, and reinterpret_cast would compile — you can't
tell which one you're getting just by reading it, and neither can a code reviewer. Each
C++-style cast below does exactly one specific, greppable thing, which is the whole point of
preferring them.
| Cast | Does | Checked? |
|---|---|---|
static_cast | conversions the compiler can verify make sense at compile time | compile-time only, no runtime check |
dynamic_cast | safe downcasting through a polymorphic hierarchy | runtime check — fails safely (null or exception) |
const_cast | adds or removes const/volatile — nothing else | compile-time only |
reinterpret_cast | reinterprets the same bits as an unrelated type | none — you're on your own |
void* back to a concrete type. It has no runtime check, so it's
fast, but it also means it will happily compile a cast that's wrong at runtime if you get the
relationship backward (see downcasting below).
double d = 3.9;
int i = static_cast<int>(d); // 3 — same as a C-style cast here, but explicit about intent
void *vp = &i;
int *ip = static_cast<int*>(vp); // fine — recovering a concrete type from void*
Base *b = new Derived();
Derived *d2 = static_cast<Derived*>(b); // compiles — but only actually safe if b really points at a Derived
class Base { public: virtual ~Base() = default; }; // needs at least one virtual function
class D1 : public Base {};
class D2 : public Base {};
Base *pbd = new D1();
D1 *good = dynamic_cast<D1*>(pbd); // OK — pbd really points at a D1
Base *pbb = new Base();
D1 *bad = dynamic_cast<D1*>(pbb); // pbb doesn't point at a D1 — dynamic_cast returns nullptr
if (D1 *checked = dynamic_cast<D1*>(pbd)) {
// safe to use checked as a D1* here
}
dynamic_cast only works on polymorphic types — the class needs at
least one virtual function, because the cast relies on the same RTTI machinery the vtable
carries (see Inheritance & Polymorphism). On
a non-polymorphic hierarchy it won't compile at all — use static_cast and
take responsibility for the correctness yourself. On references rather than pointers, a
failed dynamic_cast throws std::bad_cast instead of returning null,
since there's no such thing as a null reference to return.
dynamic_cast does its check at runtime, which makes it measurably slower
than static_cast — reach for it specifically when you don't statically
know which derived type a base pointer actually holds, not as a default habit.
const/volatile — nothing else. The
canonical legitimate use is calling a legacy or third-party function that takes a non-const
pointer but is known not to actually modify what it points to.
void legacyPrint(char *s) { std::cout << s; } // doesn't modify s, but its signature doesn't say so
void print(const std::string &s) {
legacyPrint(const_cast<char*>(s.c_str())); // risky if legacyPrint lied about not modifying it
}
const_cast to actually write through a pointer that was originally
declared const is undefined behavior, not just bad style — the compiler is
allowed to have placed that object in read-only memory. Every use of const_cast
is worth treating as a design smell worth a second look, not a routine tool.
int i = 0x41424344;
char *bytes = reinterpret_cast<char*>(&i); // view i's 4 bytes as a char array — implementation-defined, endianness-dependent
static_cast. Moving down a hierarchy where you're not
sure of the actual type — dynamic_cast. Only touching const/volatile
— const_cast. Reinterpreting raw bytes as an unrelated type —
reinterpret_cast, and treat reaching for it as a signal to double-check the
design first.