C++ Casting in Depth
C++'s four named casts each make a different, narrow promise about what kind of conversion is happening -- learning exactly what each one checks (and doesn't) is what separates a cast that's merely legal from one that's actually safe.
C inherited a single, all-purpose cast syntax -- (Type)expression -- that silently does whatever conversion the compiler can find a way to perform, whether that's a safe, well-defined numeric conversion or a dangerous reinterpretation of unrelated bit patterns, with no way to tell which one just happened by looking at the code. C++ replaces that one blunt instrument with four named casts -- static_cast, dynamic_cast, const_cast, and reinterpret_cast -- each restricted to a specific category of conversion and each searchable in source code by name. This page works through what each cast actually guarantees, where dynamic_cast's runtime type checking and RTTI come from, why downcasting needs dynamic_cast rather than static_cast in polymorphic code, what removing const with const_cast does and doesn't make safe, and why a C-style cast's willingness to silently become any of these four (including the most dangerous one) is reason enough to avoid it in modern C++.
Why C-Style Casts Are Dangerous
A C-style cast (Type)expression tries, in order, a static_cast, then a const_cast, then a combination of both, and finally a reinterpret_cast, silently picking whichever one succeeds -- without telling you which. That means the exact same syntax can mean 'convert this int to a double' (safe, well-defined) or 'reinterpret these bytes as an unrelated type' (dangerous, often undefined behavior), and nothing in the code distinguishes the two.
All three lines use identical (Type) syntax, but each one quietly performs a completely different, increasingly dangerous kind of conversion -- from a safe numeric narrowing, to stripping const off a variable that was declared const for a reason, to reinterpreting an Unrelated object's bytes as if it were a Base, which has no defined meaning at all since Unrelated doesn't inherit from Base. A C-style cast will never refuse this last one at compile time the way a named cast would.
The four named casts fix this by making each category of conversion explicit and individually searchable -- grep a codebase for reinterpret_cast and you find every genuinely dangerous low-level reinterpretation; grep for (Type) casts and you find an undifferentiated mix of everything, safe and unsafe alike. This is why style guides for modern C++ (including the C++ Core Guidelines) recommend avoiding C-style casts entirely.
#include <iostream>
class Base { public: virtual ~Base() = default; };
class Derived : public Base { public: int extra = 7; };
class Unrelated { public: int value = 99; };
int main()
{
double d = 3.9;
int i = (int)d; // C-style cast picks static_cast here -- safe truncation to 3
const int limit = 100;
int &mutable_ref = (int&)limit; // C-style cast silently picks const_cast here -- removes const!
Unrelated u;
Base *b = (Base*)&u; // C-style cast silently picks reinterpret_cast here -- WRONG, unrelated types
std::cout << i << " " << mutable_ref << "\n";
}static_cast: Well-Defined, Compile-Time-Checked Conversions
static_cast<Type>(expression) performs conversions that the compiler can verify are meaningful at compile time based purely on the types involved: numeric conversions (int to double, double to int), conversions between related pointer types up an inheritance hierarchy (derived-to-base, always safe), explicit calls to converting constructors or conversion operators, and void* back to a concrete pointer type.
Each static_cast here is checked at compile time purely from the static types involved: double to int is a standard numeric conversion; Dog* to Animal* is safe because Dog publicly inherits from Animal, so every Dog genuinely is an Animal; and void* to Dog* is accepted because the programmer is asserting (correctly, here) that raw really does point to a Dog. static_cast performs no runtime check at all -- if that last assertion were wrong, static_cast would not catch the mistake.
#include <iostream>
class Animal { public: virtual ~Animal() = default; virtual void speak() const { std::cout << "...\n"; } };
class Dog : public Animal { public: void speak() const override { std::cout << "Woof\n"; } };
int main()
{
double pi = 3.14159;
int truncated = static_cast<int>(pi); // numeric conversion: truncated = 3
Dog d;
Animal *a = static_cast<Animal*>(&d); // upcast (derived-to-base): always safe
void *raw = &d;
Dog *back = static_cast<Dog*>(raw); // void* back to known concrete type
std::cout << truncated << "\n";
a->speak(); // Woof -- virtual dispatch still applies
back->speak(); // Woof
}dynamic_cast, RTTI, and Runtime-Checked Downcasting
dynamic_cast<Type>(expression) is the one named cast that performs an actual runtime check. It's used almost exclusively for downcasting (base-to-derived) or cross-casting across a polymorphic hierarchy, and it requires the source type to be polymorphic -- meaning it has at least one virtual function -- because the check relies on RTTI (Run-Time Type Information), extra metadata the compiler embeds per polymorphic class specifically to support dynamic_cast and typeid.
dynamic_cast<Dog*>(a) checks, at runtime, whether the object a actually points to is (or derives from) a Dog. When a genuinely points at a Dog, the cast succeeds and returns a valid Dog*; when it doesn't (as when identify(&c) is called), the cast returns nullptr instead of crashing or silently producing garbage, which is exactly why the result is checked in an if before being used.
identify(&d) is called with a pointing at a real Dog object.
dynamic_cast<Dog*>(a) checks a's actual runtime type via RTTI, finds it IS a Dog, and returns a valid Dog*.
d->bark() runs, printing Woof.
identify(&c) is called with a pointing at a real Cat object.
dynamic_cast<Dog*>(a) checks again, finds a is NOT a Dog, and returns nullptr -- the if is false.
The else if dynamic_cast<Cat*>(a) succeeds instead, and meow() runs.
dynamic_cast on a reference type behaves differently on failure: since there's no 'null reference' in C++, a failed dynamic_cast<Derived&>(baseRef) throws std::bad_cast instead of returning anything -- so reference-form dynamic_cast is typically wrapped in a try/catch when the type genuinely might not match.
#include <iostream>
class Animal { public: virtual ~Animal() = default; };
class Dog : public Animal { public: void bark() const { std::cout << "Woof\n"; } };
class Cat : public Animal { public: void meow() const { std::cout << "Meow\n"; } };
void identify(Animal *a)
{
if (Dog *d = dynamic_cast<Dog*>(a))
{
d->bark();
}
else if (Cat *c = dynamic_cast<Cat*>(a))
{
c->meow();
}
else
{
std::cout << "Unknown animal\n";
}
}
int main()
{
Dog d;
Cat c;
identify(&d); // Woof
identify(&c); // Meow
}const_cast: Adding or Removing const (and Nothing Else)
const_cast<Type>(expression) is the only named cast that can add or remove const (or volatile) qualification -- it cannot change the underlying type in any other way. It exists almost entirely for interoperating with poorly-const-correct APIs (typically older C libraries) that take a non-const pointer even though they don't actually modify what it points to.
const_cast<char*>(message) strips const from message's type so it satisfies legacy_print's (badly designed) char* parameter. This is only safe because message actually points to a string literal that legacy_print, by contract, never writes to -- const_cast is a promise from the programmer, not a runtime-checked guarantee, and using it to then actually modify a genuinely const object is undefined behavior.
#include <cstring>
#include <iostream>
// Imagine this is a legacy C function that never modifies its argument
// but was written (incorrectly) without const:
void legacy_print(char *s)
{
std::cout << s << "\n";
}
int main()
{
const char *message = "hello from const_cast";
// legacy_print(message); // would not compile: const char* -> char* is not implicit
legacy_print(const_cast<char*>(message)); // explicitly asserts: I know this won't be modified
}reinterpret_cast: Low-Level Bit Reinterpretation
reinterpret_cast<Type>(expression) is the most dangerous named cast: it reinterprets an object's bit pattern as if it were a different, generally unrelated type, with essentially no compiler checking and no runtime checking at all. It's used for genuinely low-level work -- reinterpreting a pointer's bytes for hashing, converting between pointer types when talking to hardware or a binary protocol, or serialization code that walks raw bytes.
reinterpret_cast<unsigned char*>(&value) doesn't convert the int 65 into a byte array in the numeric sense -- it takes the exact same 4 bytes that already represent 65 in memory and simply relabels them as an array of unsigned char, so bytes[i] reads value's raw representation one byte at a time (endianness-dependent, per the memory topics page). Converting a pointer to std::uintptr_t and back is one of the few reinterpret_cast uses the standard specifically guarantees round-trips correctly.
#include <cstdint>
#include <iostream>
int main()
{
int value = 65;
// Reinterpret the SAME 4 bytes as an array of individual bytes, for inspection:
unsigned char *bytes = reinterpret_cast<unsigned char*>(&value);
for (std::size_t i = 0; i < sizeof(value); ++i)
std::printf("%02x ", bytes[i]);
std::printf("\n");
// Reinterpret a pointer as an integer, for logging/hashing its address:
std::uintptr_t address_as_int = reinterpret_cast<std::uintptr_t>(&value);
std::cout << "address as integer: " << address_as_int << "\n";
}Choosing the Right Cast
In practice, the choice among the four narrows down quickly once you know what you're actually trying to do. Converting between related numeric types, or moving up (or safely down, when you're certain of the type) an inheritance hierarchy: static_cast. Downcasting when the actual runtime type isn't certain, and you need a safe, checked answer: dynamic_cast. Stripping const or volatile to satisfy a badly-typed API, when you're certain the underlying object won't actually be modified through that path: const_cast. Reinterpreting raw bytes at the lowest level, with full understanding of the platform-specific consequences: reinterpret_cast. And a plain C-style cast: essentially never, in code written for readability and safety, since it silently offers all four options above interchangeably, hiding which one actually fired.
This ordering is also roughly an ordering of how much the compiler is doing to protect you: static_cast leans on the type system at compile time, dynamic_cast adds an actual runtime check, and const_cast and reinterpret_cast both compile with essentially no safety net, trusting the programmer's judgment entirely.
Why is a C-style cast considered more dangerous than the four named casts, even though it can express the same conversions?
A C-style cast silently tries static_cast, then const_cast, then a combination, then reinterpret_cast, and uses whichever one succeeds, without indicating in the source which conversion actually happened. That makes it impossible to tell, just by reading (Type)expression, whether a safe numeric conversion or a dangerous reinterpretation just occurred, and it also can't be searched for separately in a codebase the way reinterpret_cast or const_cast can.
Why does dynamic_cast require its source type to be polymorphic (have at least one virtual function), while static_cast has no such requirement?
dynamic_cast performs an actual runtime check of the object's dynamic type using RTTI, and that metadata is only generated by the compiler for polymorphic classes (via their vtable). static_cast performs no runtime check at all -- its validity is determined purely by the static types involved at compile time, so it has no need for RTTI and works on non-polymorphic types too.
What does a failed dynamic_cast return when casting to a pointer type, versus what happens when casting to a reference type?
A failed dynamic_cast to a pointer type (dynamic_cast<Derived*>(basePtr)) returns nullptr, which the caller is expected to check. A failed dynamic_cast to a reference type (dynamic_cast<Derived&>(baseRef)) instead throws std::bad_cast, because there is no such thing as a null reference in C++ for it to return.
Why is const_cast safe to use on some objects but undefined behavior on others, even though the cast itself always compiles?
const_cast only strips (or adds) the const qualifier in the type system -- it does not change whether the underlying object is actually const. If the object was accessed through a const reference or pointer to something that is genuinely non-const (e.g., a non-const object passed to a function taking a const parameter), removing const and modifying it is safe. But if the object itself was originally declared const, the compiler may have placed it in read-only memory or optimized based on the assumption it never changes, so modifying it through a const_cast'd pointer is undefined behavior regardless of the cast compiling cleanly.
Give one legitimate, low-level use case for reinterpret_cast, and explain why it should not be used as a substitute for dynamic_cast when downcasting.
A legitimate use is converting a pointer to std::uintptr_t (or back) for logging, hashing, or low-level bookkeeping -- the standard specifically guarantees this particular round-trip works correctly. reinterpret_cast should not replace dynamic_cast for downcasting because it performs no runtime check of the object's actual type at all -- reinterpret_cast<Derived*>(basePtr) compiles and 'succeeds' syntactically even when basePtr doesn't point to a Derived object, and dereferencing the result is then undefined behavior, whereas dynamic_cast would have safely detected the mismatch and returned nullptr.
Write a C++ program with a polymorphic base class Shape and two derived classes Circle and Square, and a function that takes a Shape* and uses dynamic_cast to print type-specific information for whichever concrete type it actually receives, safely handling both cases.
describe() tries dynamic_cast<Circle*> first, which succeeds only when shape's actual runtime type is Circle (checked via RTTI, since Shape is polymorphic thanks to its virtual destructor); if that returns nullptr, it falls through to try Square instead. Calling describe(&circle) prints the circle's radius and describe(&square) prints the square's side, each safely routed to the correct branch without ever risking an invalid access, since a failed dynamic_cast simply returns nullptr rather than corrupting anything.
What does this program print, and explain exactly why the reinterpret_cast-based downcast produces undefined behavior even though the code compiles and 'appears' to run.
The program's printed output cannot be reliably predicted -- it might print 999 (if Unrelated::something happens to occupy the same offset as Derived::derived_value on this compiler and layout), garbage, or crash, because the behavior is undefined, not merely 'wrong but consistent'. reinterpret_cast<Derived*>(&u) compiles because reinterpret_cast performs no relationship check between Unrelated and Derived at all -- it simply relabels u's address as if it pointed to a Derived object. But u was never constructed as a Derived (it has no Base subobject, no vtable matching Derived's layout, and no derived_value member at the expected offset), so dereferencing bad->derived_value reads memory as if a Derived object's layout were present when it isn't -- a strict-aliasing violation the compiler is free to optimize around unpredictably. This is exactly the situation dynamic_cast exists to catch safely (it would have required Unrelated to derive from Base to even attempt the cast, and would fail cleanly here).
Write a C++ program that uses const_cast to call a legacy function taking a non-const char* on a string literal that the legacy function is known (by contract) not to modify, and add a comment explaining why this pattern is only safe under that assumption.
const_cast<char*>(greeting) strips the const qualifier so greeting's type matches legacy_log's char* parameter, letting the call compile. This is only safe because of an external guarantee -- legacy_log's documented contract that it never writes through the pointer -- not because of anything the compiler verifies; string literals are typically stored in read-only memory, so if legacy_log actually attempted to modify the characters, the program would have undefined behavior (potentially crashing on some platforms). const_cast never makes a write-through-a-genuinely-const-object safe; it only removes a type-system restriction that was in the way of a call the programmer has separately verified is safe.
// Cost / safety, roughly ordered from safest to most dangerous:
// static_cast<T>(x) -- compile-time checked, no runtime cost
// dynamic_cast<T*>(x) -- runtime checked (RTTI lookup), has a real runtime cost
// const_cast<T>(x) -- compiles freely; SAFETY depends entirely on programmer discipline
// reinterpret_cast<T>(x) -- compiles freely; essentially no safety net at all
// (T)x (C-style cast) -- silently picks one of the above; you can't tell which from the syntax#include <iostream>
class Base { public: virtual ~Base() = default; int base_value = 1; };
class Derived : public Base { public: int derived_value = 2; };
class Unrelated { public: int something = 999; };
int main()
{
Unrelated u;
Derived *bad = reinterpret_cast<Derived*>(&u);
std::cout << bad->derived_value << "\n"; // undefined behavior
}