Casts
C++ casts, RTTI, const-correctness, and undefined behavior traps.
17 questions
JuniorTheoryVery commonWhat is array-to-pointer decay and how does it interact with overload resolution?
What is array-to-pointer decay and how does it interact with overload resolution?
An lvalue of type T[N] implicitly converts to T* pointing at the first element — that's array decay. In overload resolution a decayed array matches T* but not T (&)[N], so a reference-to-array overload preserves the size.
Common mistakes
- ✗Computing array length as
sizeof(arr)/sizeof(arr[0])after the array has decayed inside a function — gives pointer-size / element-size - ✗Passing a 2D array
int a[3][4]tovoid f(int** p)— incompatible, decay producesint(*)[4]notint** - ✗Forgetting that string literals are
const char[N]and decay toconst char*
Follow-up questions
- →How do you write a function that takes an array and deduces its length?
- →Why does
auto x = "hi";giveconst char*and notconst char[3]?
JuniorTheoryCommonWhat does dynamic_cast return on failure and how should the caller handle each variant?
What does dynamic_cast return on failure and how should the caller handle each variant?
Pointer form dynamic_cast<T*> returns nullptr on failure — check before dereferencing. Reference form dynamic_cast<T&> cannot return null and throws std::bad_cast. Use pointer form for branching, reference form for invariants.
Common mistakes
- ✗Forgetting to check the pointer result and dereferencing
nullptron a failed downcast - ✗Catching
std::exceptionto handlebad_castwithout knowing the source — better catchstd::bad_castdirectly - ✗Using exceptions for normal control flow — prefer the pointer form when failure is expected
Follow-up questions
- →Why is the reference form needed at all if pointer form covers all cases?
- →How does
dynamic_cast<void*>behave?
JuniorTheoryCommonWhat is the difference between an upcast and a downcast and which casts perform each?
What is the difference between an upcast and a downcast and which casts perform each?
Upcast Derived* → Base* is implicit and always safe. Downcast Base* → Derived* needs static_cast (no check, UB if wrong) or dynamic_cast (RTTI check, nullptr/bad_cast on failure, requires polymorphic base).
Common mistakes
- ✗Using
static_castfor downcast and getting silent wrong-type behaviour at runtime - ✗Calling
dynamic_caston a non-polymorphic class (no virtual functions) — compile error - ✗Forgetting that pointer adjustment in multiple inheritance means the numeric address may change after upcast
Follow-up questions
- →How does
dynamic_castwork internally (vtable type info)? - →What is the cost of
dynamic_castand when should you avoid it on hot paths?
JuniorTheoryCommonWhat is a narrowing conversion and when does the compiler diagnose it?
What is a narrowing conversion and when does the compiler diagnose it?
A narrowing conversion is an implicit conversion that may lose information (double→int, int→short, signed→unsigned). In brace-initialisation T x{e}; it is a compile error; in copy-initialisation T x = e; or function arguments it is silently allowed.
Common mistakes
- ✗Writing
int x = 3.9;and expecting a warning — only brace-init enforces no-narrowing - ✗Forgetting that
int → unsignedis a narrowing conversion under the standard - ✗Suppressing narrowing with
static_castinstead of using a wider type or checking the range
Follow-up questions
- →Why is
char c{300};an error butchar c = 300;not? - →How does
gsl::narrowdiffer fromstatic_cast?
MiddleTheoryCommonWhat is wrong with C-style casts in C++ and which C++ casts replace them?
What is wrong with C-style casts in C++ and which C++ casts replace them?
A C-style cast (T)x silently picks among const_cast, static_cast, reinterpret_cast, hiding intent and accepting dangerous combinations without warning. C++ named casts are explicit, grep-able, and tied to one operation each.
Common mistakes
- ✗Using
(T)xthinking it is the same asstatic_cast<T>(x)— it can silently insert areinterpret_cast - ✗Treating the functional cast
T(x)as safer — it has the same semantics as(T)xfor non-class types - ✗Hiding const-removal behind a C-style cast and then writing through the result, which is undefined if the original object was const
Follow-up questions
- →When does
T(x)become a constructor call rather than a cast? - →How does clang-tidy's
cppcoreguidelines-pro-type-cstyle-casthelp enforce this?
MiddleTheoryCommonWhen are const_cast and reinterpret_cast dangerous?
When are const_cast and reinterpret_cast dangerous?
const_cast is only safe if the original object was not actually const; modifying a truly const object is undefined behavior. reinterpret_cast changes the expression type without creating a new object and can violate alignment, aliasing, or ABI assumptions.
Common mistakes
- ✗Using const_cast to mutate string literals or const globals
- ✗Treating reinterpret_cast as a safe serialization mechanism
- ✗Ignoring strict aliasing and alignment requirements
Follow-up questions
- →How would you inspect object bytes safely?
- →What is std::bit_cast and how is it different?
MiddleTheoryCommonWhat is dynamic_cast and how does RTTI enable it?
What is dynamic_cast and how does RTTI enable it?
dynamic_cast<T*>(ptr) checks at runtime whether ptr points to a T or a class derived from T, returning null (pointer) or throwing std::bad_cast (reference) on failure. It requires RTTI metadata embedded in the vtable of polymorphic classes.
Common mistakes
- ✗Using
dynamic_caston a non-polymorphic class — compile error; the base must have at least one virtual function - ✗Not checking the result for null after a pointer cast — if the cast fails, dereferencing is UB
- ✗Disabling RTTI with
-fno-rttibut still trying to usedynamic_castortypeid— undefined behaviour
Follow-up questions
- →When would you use
dynamic_castfor cross-casting between sibling classes? - →How does
typeidrelate todynamic_castand what does it return for a polymorphic type?
MiddleTheoryCommonWhat is the difference between static_cast and dynamic_cast in a class hierarchy?
What is the difference between static_cast and dynamic_cast in a class hierarchy?
static_cast performs a compile-time conversion and does not verify the dynamic type. dynamic_cast checks the real dynamic type at runtime for polymorphic classes: pointer casts return nullptr on failure, reference casts throw std::bad_cast.
Common mistakes
- ✗Using static_cast for unchecked downcasts and assuming it is safe
- ✗Forgetting that dynamic_cast needs a polymorphic base type
- ✗Expecting dynamic_cast on references to return a null reference
Follow-up questions
- →What happens if RTTI is disabled?
- →When would you redesign instead of using dynamic_cast?
MiddleTheoryCommonHow do you write a user-defined conversion and when should it be explicit?
How do you write a user-defined conversion and when should it be explicit?
Two forms: a non-explicit constructor T(U) enables U → T, and a member operator U() const enables T → U. Mark both explicit unless the conversion is lossless and unsurprising, to avoid silent misuse in overload resolution.
Common mistakes
- ✗Forgetting that single-argument constructors are converting unless marked
explicit(since C++11 also multi-arg with{}) - ✗Adding
operator bool()withoutexplicit— leads to surprising arithmetic and comparison overloads - ✗Defining both a converting constructor and a conversion operator between the same types — creates ambiguity
Follow-up questions
- →How does C++11 contextual conversion to bool interact with
explicit operator bool()? - →Why is
std::stringconstructible fromconst char*implicitly butstd::filesystem::pathfromstd::stringexplicitly?
JuniorTheoryOccasionalWhat does the functional cast T(x) do and how does it differ from (T)x?
What does the functional cast T(x) do and how does it differ from (T)x?
With one argument, T(x) is equivalent to (T)x — same dangerous escalation through const/static/reinterpret_cast. With multiple arguments T(a, b) cannot be a cast and is always a constructor call.
Common mistakes
- ✗Believing functional-cast is safer than C-style cast — for fundamental types they're identical
- ✗Writing
int(p)to convert pointer to integer — silently insertsreinterpret_cast - ✗Mixing
T(x)(might be a cast) withT{x}(always direct-list-init)
Follow-up questions
- →Why does
T()(no args) value-initialize? - →When is
T(x)parsed as a function declaration (most-vexing-parse)?
MiddleTheoryOccasionalWhat is std::bit_cast and why is it preferred over reinterpret_cast for type punning?
What is std::bit_cast and why is it preferred over reinterpret_cast for type punning?
std::bit_cast<To>(from) (C++20) returns a To copied bit-for-bit from from. Requires both types trivially copyable and same-sized; does not violate strict aliasing and is constexpr when allowed.
Common mistakes
- ✗Using
*reinterpret_cast<T*>(&x)for type punning — violates strict aliasing for non-character types - ✗Using
unionfor type punning in C++ — technically only the last-written member is alive (legal in C, formally UB in C++ before C++20 with bit_cast) - ✗Forgetting that
bit_castrequires equal sizes — different-size casts must use memcpy on a buffer
Follow-up questions
- →How would you implement
bit_castin pre-C++20 code? - →What is the strict aliasing rule and which types are exempt?
MiddleTheoryOccasionalWhen is writing through a const_cast-removed pointer undefined behaviour?
When is writing through a const_cast-removed pointer undefined behaviour?
If the original object was declared const, modifying it through a const_cast-stripped pointer is undefined behaviour. Safe only when the underlying object is non-const but viewed through a const interface (e.g. legacy C APIs).
Common mistakes
- ✗Casting away const on a
const int x = 5;and writing — UB - ✗Using
const_castto silence compiler errors instead of fixing const-correctness in the API - ✗Forgetting that
const_castcannot removevolatileon a truly volatile object either
Follow-up questions
- →What is
mutableand how does it interact with const member functions? - →Why does the standard library provide
std::as_constbut notstd::as_mutable?
MiddleTheoryOccasionalWhat is an implicit conversion sequence and what stages does it have?
What is an implicit conversion sequence and what stages does it have?
An implicit conversion sequence has up to three stages: (1) standard conversion (lvalue→rvalue, array/function→pointer); (2) one user-defined conversion (constructor or operator T()); (3) a final standard conversion. Only one user-defined conversion is allowed per sequence.
Common mistakes
- ✗Expecting two user-defined conversions to chain — the standard explicitly forbids this
- ✗Forgetting that overload resolution ranks sequences: exact match > promotion > standard conversion > UDC > ellipsis
- ✗Marking conversion constructors non-
explicitand being surprised by unintended implicit conversions
Follow-up questions
- →What is the difference between a converting constructor and a converting operator?
- →How does
expliciton a conversion operator change overload resolution?
MiddleTheoryOccasionalHow do you safely convert a pointer to an integer and back?
How do you safely convert a pointer to an integer and back?
Use std::uintptr_t from <cstdint> with reinterpret_cast<std::uintptr_t>(ptr) and reinterpret_cast<T*>(integer). The round trip pointer→integer→same pointer is well-defined; arithmetic on the integer is implementation-defined.
Common mistakes
- ✗Casting a pointer to
inton a 64-bit system — silently truncates and the round trip fails - ✗Assuming
(uintptr_t)ptr & MASKis portable — bit-tagging pointers is implementation-defined - ✗Mixing
std::ptrdiff_tandstd::uintptr_t— different purposes (subtraction vs. address representation)
Follow-up questions
- →What does
std::intptr_tadd overstd::uintptr_t? - →Can you store a function pointer in a
void*?
MiddleTheoryOccasionalHow do you safely round-trip a typed pointer through void*?
How do you safely round-trip a typed pointer through void*?
Conversion T* → void* is implicit and always safe. Conversion void* → T* requires static_cast<T*>(vp). The round trip T* → void* → T* yields the original pointer; casting to a different U* is only valid if U is similar enough (e.g. cv-qualified T) — otherwise you have a strict-aliasing problem if you dereference.
Common mistakes
- ✗Using
reinterpret_castforvoid* → T*—static_castis the correct, well-defined choice - ✗Storing function pointers in
void*— not portable; use a separatevoid(*)()typed handle - ✗Forgetting to apply correct cv-qualification when round-tripping
const T* → void* → T*(loses const)
Follow-up questions
- →Why is
void**not implicitly convertible fromT**? - →What replaces
void*in modern C++ for type-erased storage?
SeniorTheoryRareHow does RTTI work internally?
How does RTTI work internally?
Under the Itanium ABI a vtable slot points to a std::type_info for the class. dynamic_cast walks the base-class graph using offsets and type_info pointers in __base_class_type_info. typeid(expr) returns the most-derived object's type_info via the same slot; -fno-rtti removes these structures.
Common mistakes
- ✗Assuming RTTI has negligible cost — it increases binary size and
dynamic_castdoes real work proportional to hierarchy depth - ✗Comparing
type_infoobjects with==across shared library boundaries — name-based comparison may be needed (name()string comparison) - ✗Thinking
-fno-rttionly removesdynamic_cast— it also removestypeidand exception type matching for polymorphic types
Follow-up questions
- →How would you implement your own lightweight RTTI without the compiler-generated version?
- →What is the difference between
typeidon a pointer andtypeidon the dereferenced object?
SeniorTheoryRareWhy must a downcast from a virtual base use dynamic_cast rather than static_cast?
Why must a downcast from a virtual base use dynamic_cast rather than static_cast?
With virtual inheritance, the offset from the virtual base subobject to the most-derived object isn't fixed at compile time — it depends on the actual type and lives in the vtable. static_cast is a compile error; dynamic_cast reads RTTI to compute the offset at runtime.
Common mistakes
- ✗Trying
static_castfrom a virtual base and being confused by the compile error - ✗Believing
dynamic_castis required only across hierarchies — it is also required for virtual-base downcast - ✗Designing wide diamond hierarchies and then suffering
dynamic_castcost on hot paths
Follow-up questions
- →What is the layout of a class with a virtual base?
- →Why does
dynamic_cast<void*>(p)return a pointer to the most-derived object?