C++ Casting
이 글의 핵심
C++ casting complete guide: static_cast, dynamic_cast, const_cast, reinterpret_cast with principles and practical examples.
1. Introduction: why C++ has multiple cast operators
C and early C++ relied on a single C-style cast syntax—(T)expr or T(expr)—to force almost any conversion the compiler would accept. That convenience hides intent: the same syntax can perform arithmetic conversions, pointer adjustments, const removal, and bitwise reinterpretation. When a program misbehaves, searching for ( is useless, and code review cannot tell which language rule the author meant to use.
C++ therefore provides four named cast operators—static_cast, const_cast, dynamic_cast, and reinterpret_cast—plus the nearly obsolete C-style and function-style casts, which the standard still allows for compatibility. Each named form:
- Documents intent in the source text and in
grep/ static analysis. - Enforces a narrower contract (for example,
const_castcannot change type category except cv-qualification). - Aligns with the C++ type system (value categories, object lifetime, and undefined behavior rules).
This article walks through when each cast is appropriate, how it interacts with the abstract machine and RTTI (Runtime Type Information), and how to replace casts with virtual dispatch, std::variant, and std::any-style design where possible.
2. C-style cast and function-style cast: old style, new problems
C-style and function-style notation still exist:
double y = (double)count; // C-style
double z = double(count); // function-style (also "C++ functional cast" for single types)
A C-style cast can perform, in a defined order, a static_cast plus optional const_cast and reinterpret_cast; in effect it is a “do whatever is necessary” request. The standard describes this composition in [expr.cast] (as of C++17 and later, phrasing in terms of the named casts).
Problems in practice
- Unintended
reinterpret_cast: casting unrelated pointer types to “make it compile” may violate strict aliasing or alignment requirements. - Hidden
constremoval: the same(token might stripconstwhen the author only wanted a numeric conversion. - Unsearchable in large codebases compared to
static_cast<. - Review and tooling: linters and humans both benefit from explicit
static_cast/dynamic_cast.
I do not use C-style casts in new C++—static_cast, const_cast, dynamic_cast, and reinterpret_cast spell out which rules I am buying into. The only time I will touch (T)expr is when I am surgically editing a file that is already a museum piece, and even then I treat it as a bug-shaped compatibility shim, not a style choice. If you are writing or reviewing new code, default to: no C-style cast; the compiler and grep should see the intent.
3. static_cast: compile-time, mostly safe conversions
static_cast<T>(expr) performs conversions that the compiler can reason about at compile time in terms of the language rules: numeric promotions and conversions, pointer conversions along an inheritance hierarchy (with no runtime check for downcasts), void*-to-object-pointer, explicit user-defined conversions, and certain inverse conversions.
3.1 Numeric conversions and promotions
Use static_cast to make narrowing or cross-domain conversions explicit:
std::uint32_t u = 0xFFFFFFFF;
std::int32_t s = static_cast<std::int32_t>(u); // well-defined, implementation-defined or implementation-defined+wrap depending on / rep
double pi = 3.14;
int n = static_cast<int>(pi); // truncates toward zero
Why it matters — implicit narrowing in braces is diagnosed in list-initialization (int x{3.1}; is ill-formed), but assignment and many APIs still smuggle double to int silently. static_cast is the self-documenting place to say: “I accept truncation, sign change, or representation change.”
3.2 Upcasting (pointers and references to base)
In class hierarchies, converting Derived* to Base* (or reference variants) is a standard conversion; static_cast is optional for upcasts but can clarify intent in templates:
struct Base { virtual ~Base() = default; };
struct Derived : Base {};
Derived d;
Base& br = static_cast<Base&>(d);
When the compiler can prove the relationship (well-formed hierarchy, accessible base), the cast is a no-op at the machine level: same address, different static type.
3.3 Downcasting: static_cast vs dynamic_cast
Base* b = new Derived;
Derived* d1 = static_cast<Derived*>(b); // compiles: undefined if b does not actually point to Derived
Derived* d2 = dynamic_cast<Derived*>(b); // returns non-null if truly Derived (needs polymorphic Base)
static_cast downcast is unchecked. If the object’s dynamic type is not Derived (or a type derived from Derived), you have undefined behavior when you use the result as a Derived*.
dynamic_cast downcast is the safe option when the hierarchy is polymorphic (at least one virtual function, typically a virtual destructor). The runtime consults the object’s dynamic type.
Use static_cast for downcast only when you have a separate, stronger invariant (for example, a private factory that always stores Derived in a Base* with a known tag enum checked earlier).
3.4 Explicit and implicit user-defined conversion chains
static_cast can invoke explicit conversion functions and converting constructors in contexts where implicit conversion is not allowed:
struct A { explicit A(int) {} };
A a = static_cast<A>(42);
In templates, static_cast is the usual way to disambiguate dependent names and force a particular overload set.
3.5 void* conversions
static_cast can convert from void* to T*, but not the reverse (use const_cast if you must adjust cv-qualification on a non-void pointer, or the appropriate cast). Converting an arbitrary void* to T* is only valid if the void* actually pointed to a T object (or a suitably related storage).
int x;
void* p = &x;
int* q = static_cast<int*>(p); // OK: x is an int
Pitfall: treating unrelated storage as T* and dereferencing is UB.
3.6 Enumerations and static_cast (scoped enums, underlying types)
C++11 strongly typed enums often require static_cast to convert to the underlying type or to integrate with C APIs.
enum class Mode : std::uint8_t { A = 1, B = 2 };
std::uint8_t wire = static_cast<std::uint8_t>(Mode::A);
Unscoped enums (legacy) integrate more loosely with integers; static_cast still helps when you want to stop implicit promotion surprises in modern code.
3.7 Pointers to void* and cv-qualified void*
A non-cv T* converts implicitly to cv T*; converting T* to void* (and back with static_cast for the reverse) is the standard C pattern for opaque handles. A const T* should become const void*, not a writable void*, to preserve const correctness through APIs like memcpy (which takes void* for historical reasons, but your wrapper can take std::byte* in new code).
int n = 3;
const int* cp = &n;
const void* cv = cp; // OK
// void* w = cp; // not allowed: discards const
Takeaway — static_cast is still the go-to for void* → object pointer, after you have verified the buffer truly holds a T (or you are constructing a T in raw storage with placement new, which is a different, careful story).
3.8 static_cast in generic code (templates)
In dependent contexts, a plain cast may be parsed as a comparison. static_cast avoids typename / ::template confusions in some edge cases and makes intent obvious:
template<class T, class U>
T narrow(U u) { return static_cast<T>(u); }
CRTP and static dispatch use static_cast<Derived*>(this) inside a base class template to reach derived members. That is a static (compile-time) relationship: if the CRTP contract is broken, the error is still UB at runtime, so the idiom is “checked by the template,” not by RTTI.
4. dynamic_cast: runtime type checking and RTTI
dynamic_cast<T>(expr) requires RTTI for polymorphic class types: it needs type information for the dynamic type of the operand.
4.1 Downcasting with pointers and references
For pointers, a failed dynamic_cast to Derived* yields nullptr; for references, it throws std::bad_cast.
struct B { virtual ~B() = default; };
struct D1 : B {};
struct D2 : B {};
void use(B* b) {
if (auto* p = dynamic_cast<D1*>(b)) {
// use D1-specific interface
} else if (auto* p = dynamic_cast<D2*>(b)) {
// use D2-specific interface
}
}
A crash I actually had to own: a colleague had left a dynamic_cast<D1*> at a call boundary; in one path the dynamic type was only B (or D2), not D1, so the cast returned nullptr—and the next line dereferenced it anyway. The core dump was honest (SIGSEGV on a null), but the blame did not go to dynamic_cast for “failing”; it went to the code that treated success as guaranteed. I treat every pointer dynamic_cast as a std::optional in disguise: I either branch on nullptr, I return an error, or I stop routing that pointer through a code path that needs D1 without checking. Reference dynamic_cast is different (it throws), but the pointer form will happily hand you a null and walk away. That is not a quirk; it is the contract.
4.2 Cross-casts in multiple inheritance
When Derived inherits two bases, you can cross-cast between sibling subobjects if the complete object is compatible:
struct A { virtual ~A() = default; int a; };
struct B { virtual ~B() = default; int b; };
struct C : A, B {};
B* b = new C;
A* a = dynamic_cast<A*>(b); // can recover sibling A* from B* within a C
Failure modes: If the pointer does not point into a complete object of an appropriate C, the cast can fail (nullptr) or, in the case of bad lifetime use, the program may already be in UB from earlier mistakes.
4.3 Performance cost
- Type information: RTTI is typically stored in vtable-like structures;
dynamic_castmay walk base tables or use hash tables, depending on ABI. - Relative cost — far cheaper than a disk read, but hot paths in games or HFT sometimes disable RTTI (
-fno-rtti) and use manual tags or the visitor pattern instead.
Cross-cutting concern: with -fno-rtti, dynamic_cast on pointers to polymorphic class types is not available; design must change (visitor, std::variant of std::unique_ptr<Concrete>, or custom type ID).
4.4 dynamic_cast to reference: std::bad_cast
For references there is no nullptr; failure throws std::bad_cast (from <typeinfo> / <exception> in typical implementations via standard headers).
#include <typeinfo>
struct B { virtual ~B() = default; };
struct D : B {};
void f(B& b) {
try {
D& d = dynamic_cast<D&>(b);
(void)d;
} catch (std::bad_cast const&) {
// b was not a D
}
}
Design tip — in application code, the pointer form with a null test is often clearer than try/catch for control flow, unless the non-null reference is a truly exceptional situation.
4.5 Slicing and dynamic_cast
Object slicing happens when a Derived is copied or assigned by value into a Base. The dynamic type is lost; a subsequent Base& to Derived& dynamic_cast can fail. Always pass polymorphic objects by pointer or reference if downstream code needs the dynamic type. Value semantics and RTTI are a poor mix.
4.6 Polymorphic requirement in plain language
dynamic_cast to a reference or pointer to a polymorphic class type requires that the source expression’s type in the dynamic_cast be related as specified in the Standard (typically, pointer/reference to a polymorphic complete class). A practical rule: give the hierarchy a virtual destructor in the base, so destruction is always correct, and the type is set up the way dynamic_cast expects.
5. const_cast: removing or adding const and volatile
const_cast adjusts cv-qualifiers (const / volatile) on types; it is the only cast that can add or remove const in this way.
void legacy_modifies_in_place(char* s);
void good_api(const char* s) {
legacy_modifies_in_place(const_cast<char*>(s)); // only if you KNOW legacy won't mutate, or the buffer isn't truly const
}
5.1 When it is (sometimes) “safe”
- The actual object in memory is not
const(you only have aconstview). Adjusting a pointer to match an old API, then not mutating when the string was originally string-literal backed, is still a contract with the callee. - “De-consting” to call an API that is incorrectly non-
constbut does not write, if you can prove it. - In generic code that adds
constto match interfaces.
5.2 When it is undefined behavior
If the original object is truly const (e.g. a const object with static storage, or a variable declared const with static initialization), writing through a const_cast-derived pointer to modify that object is UB.
const int x = 42;
int* p = const_cast<int*>(&x);
// *p = 0; // undefined behavior
String literals are typically stored in read-only memory; const_cast + write often crashes, always violates the literal’s const contract.
5.3 Practical, dangerous pattern: const methods that cache
A mutable data member is the idiomatic C++ way to allow internal caching in a const member function. A const_cast on this to “cheat” mutability in the same class is a code smell; mutable or restructuring is usually clearer.
6. reinterpret_cast: bit-level reinterpretation
reinterpret_cast reinterprets the bit pattern of a pointer (or an integer) as another type, subject to a long list of restrictions in the Standard.
6.1 Type punning, strict aliasing, and char / std::byte
C++ strict aliasing rules (roughly) allow accessing an object of type T through a glvalue of type T, char / std::byte, or a few other related cases. Reinterpreting a int* as double* and reading as double is not a generally valid “view” of the same memory; it is often undefined behavior.
Exceptions / patterns — copying bytes into T (e.g. std::memcpy to a T object) is the portable way to create a new T from bytes.
6.2 Hardware, MMIO, and kernel boundaries
reinterpret_cast appears frequently in device drivers and memory-mapped I/O where the platform ABI guarantees the layout a physical address maps to a volatile T*.
std::uintptr_t reg = 0x4000'0000;
auto* mmio = reinterpret_cast<volatile std::uint32_t*>(reg);
6.3 Pointers to integers and back
Converting a pointer to std::uintptr_t / std::intptr_t and back to the same pointer type, for the same address, is the intended escape hatch for “opaque handles.” Round-tripping through an integer is implementation-defined if alignment or size does not match.
6.4 Warnings and review
- Treat every
reinterpret_castas a red flag in application-level code; require a one-line standard reference or platform doc why it is valid. - Prefer
std::bit_cast(C++20) for value reinterpretation of trivially copyable types of the same size (see Section 10).
7. Which cast, without a grid
Tables read like a cheat sheet; runs feel like a story. I pick operators in this order, and I use small examples to remind my future self what each one is for.
-
Design away the need when I can: virtual function, Liskov-safe interface, or
std::variantfor a closed set. No cast in the hot path is still the best cast. -
static_castfor things the compiler can justify at build time: numbers, upcasts, explicit conversions, and downcasts only when I have a stronger proof than “it compiled.”
double x = 3.9;
int n = static_cast<int>(x); // I meant truncation, not a surprise implicit rule
struct Base { virtual ~Base() = default; };
struct Derived : Base { int k; };
Base* b = new Derived;
Derived* d = static_cast<Derived*>(b); // I am asserting the dynamic type; no runtime check
dynamic_castwhen I do not know the dynamic type and I will handle failure (pointer → nullptr, reference →std::bad_cast).
if (auto* p = dynamic_cast<Derived*>(b)) {
(void)p->k; // only if the check passed
} else {
// not Derived — I don’t deref p here
}
-
const_castto adjust cv-qualification at a boundary to an old API, never as a back door to “make the compiler quiet.” -
reinterpret_cast(or C++20std::bit_castfor values) in the narrow, documented places: hardware, handles, and interop. Everything else in application code gets a why in the review, not a shrug. -
C-style
(— in new code, I treat it the same as an unexplainedgoto: someone else can merge it, I will not.
8. RTTI: typeid, dynamic_cast, and the cost model
RTTI provides:
dynamic_castfor polymorphic types.typeidfor obtaining astd::type_info&(for polymorphic lvalues, the dynamic type; for other cases, the static type, with caveats in the Standard for incomplete types / polymorphic use).
#include <typeinfo>
#include <iostream>
struct Poly { virtual ~Poly() = default; };
struct One : Poly {};
void print_name(Poly& p) {
std::cout << typeid(p).name() << '\n'; // implementation-mangled name
}
Use of typeid for branching in application logic duplicates what virtual functions and visitors do, often less safely (string comparisons or addresses are not a substitute for a closed set of known operations).
Disabling RTTI — Some projects use -fno-rtti to save binary size and a bit of vtable cost; you lose dynamic_cast and must replace type discovery.
Comparing types — typeid on std::type_info can be compared: typeid(b) == typeid(Derived) is a common (but brittle if inheritance is deep and you meant “is a” vs “is exactly”) test. dynamic_cast already answers “is a” for pointers; for exact type, typeid with polymorphic lvalues can match exact dynamic type, which is not the same as “convertible to.”
ABI name — type_info::name() returns an implementation-defined string (often mangled). Demangle with abi::__cxa_demangle on Itanium ABI systems if you need human-readable text in debug logs.
9. std::any and std::any_cast: type erasure, not a hierarchy cast
std::any erases a value’s static type. std::any_cast<T>(&a) returns T* or throws for values; std::any_cast<T*>( std::any* ) is also available in pointer form.
#include <any>
#include <string>
std::any a = std::string("hi");
if (auto* ps = std::any_cast<std::string>(&a)) {
*ps = "bye";
}
Contrast with dynamic_cast: this is not OO polymorphism. It is “recover the stored type in a std::any.” Prefer std::variant, a sum type, when the set of alternatives is closed and known at compile time: variant gives compile-time exhaustiveness (with std::visit).
9.1 Value vs pointer overloads and bad_any_cast
std::any_cast<T>(a)whereaisconst std::any&orstd::any&returns a copy (or const ref overloads where applicable) and throwsstd::bad_any_castif the held type is not exactlyT(decay rules apply; see the Standard for cv/ref nuances).std::any_cast<T>(&a)returns a pointer — nullptr if the type does not match — which mirrors thedynamic_castpointer / nullptr pattern without inheritance.
#include <any>
#include <string>
void demo(std::any& a) {
if (auto* s = std::any_cast<std::string>(&a)) {
s->append("!");
} else {
// wrong type: nullptr, no throw
}
try {
int x = std::any_cast<int>(a); // throws std::bad_any_cast if not int
} catch (std::bad_any_cast const&) {}
}
Performance — Storing a large object in std::any may trigger heap allocation and a type-erased vtable; it is a poor substitute for a homogeneous buffer or a std::variant of a few large structs.
10. std::bit_cast (C++20): safe “reinterpret for values” of trivially copyable types
std::bit_cast<To>(from) reinterprets the object representation of a trivially copyable From as a To of the same size and satisfies various constraints. It is the standard blessed replacement for many reinterpret_cast + union hacks for in-memory, non-pointer type punning.
#include <bit>
#include <cstdint>
static_assert(sizeof(float) == sizeof(std::uint32_t));
std::uint32_t bits = 0x4048f5c3;
float f = std::bit_cast<float>(bits);
Limitations — Not for reinterpreting pointers; not for volatile; target type must be trivially copyable and of equal size. Use for IEEE float bits, network-endian integer packing with known layout, etc.
11. Performance: costs of each cast in rough terms
| Construct | Cost model |
|---|---|
static_cast (numeric) | Same as the underlying conversion (e.g. double → int is one instruction) |
static_cast (pointer up/down in hierarchy) | Often zero at runtime; downcast to wrong type = UB, not a performance issue but a correctness one |
dynamic_cast | Algorithm + data-structure dependent: typically O(depth) of inheritance or O(1) with ABI-specific fixed limits; much more than a pointer load |
const_cast | No-op at assembly level; danger is semantic |
reinterpret_cast | No-op; danger is wrong load/store and aliasing |
| C-style | Whatever the composed cast does |
typeid on polymorphic reference | May have a cost (implementation-defined) similar to a vptr indirection; sometimes optimized |
std::any type recovery | Heap allocation in many implementations; slower than variant for hot code |
Rule of thumb: in inner loops, avoid dynamic_cast in favor of a vfunc, CRTP static dispatch, or std::variant. Avoid std::any on hot paths.
12. My rules: reducing and isolating casts
- Make illegal states unrepresentable — e.g.
std::optional, sum types, factory return ofunique_ptr<ConcreteBaseInterface>. - Prefer free functions on
Base&with virtual hooks over repeateddynamic_castchains in callers. - Tag dispatch and vtables — store an enum in a known layout only when profiling proves
dynamic_castis a bottleneck; document ABI. - Templates and concepts (C++20) avoid object-level casts in generic code.
- Minimize
const_cast; fix APIs or usemutablefor caches. - Encapsulate the one
reinterpret_castthat talks to your DMA buffer in a single function and unit-test it. - Enable warnings —
-Wold-style-cast(GCC/Clang) and treat new C-style casts in product code as review rejections, not nits. - Never add C-style casts in new code — if a legacy file forces my hand, I wrap the one ugly line and leave a pointer to a ticket; I do not spread
(T)across new files “for consistency with the 1990s.”
13. Common mistakes and undefined behavior (quick catalog)
static_castdowncast aBase*that actually points toUnrelated→ UB on use.dynamic_castfrom non-polymorphic type in contexts where the Standard requires a polymorphic lvalue; ill-formed or wrong depending on the exact expression—ensure at least one virtual in the declared class used fordynamic_castwhere required.const_castwrite to read-only storage or a const object → UB.reinterpret_castbetween unrelated pointers and dereferencing as a different object type can violate strict aliasing → UB unless a Standard exception applies (usechar/std::byteto copy bytes, orstd::bit_castfor values).- Misaligned access on some architectures (ARM, older) → bus fault or UB.
- Casting
thisin constructors / destructors (when virtual dispatch is not yet / no longer the derived type) — subtle lifetime bugs, not always “cast” bugs but related to static vs dynamic type.
14. Real examples: factories, plugins, and serialization
14.1 Factory returning std::unique_ptr<Base>
#include <memory>
#include <string>
#include <stdexcept>
struct File {};
struct FileReader : File {};
struct FileWriter : File {};
enum class FileKind { Read, Write };
std::unique_ptr<File> make_file(FileKind k) {
switch (k) {
case FileKind::Read: return std::make_unique<FileReader>();
case FileKind::Write: return std::make_unique<FileWriter>();
}
throw std::invalid_argument("kind");
}
Avoid a void* and static_cast when ownership and deletion are known at creation time. If you must store heterogeneous objects in a container of Base*, virtual destructor and dynamic_cast in plugin boundaries are a common, defensible use.
14.2 Plugin boundary with dynamic_cast
The plugin is loaded in another translation unit: you receive Plugin* and need AudioPlugin*:
struct Plugin { virtual ~Plugin() = default; };
struct AudioPlugin : Plugin { void set_volume(int) {} };
void maybe_tune(Plugin* p) {
if (auto* a = dynamic_cast<AudioPlugin*>(p)) {
a->set_volume(11);
}
}
Alternative: pass AudioPlugin& to AudioDriver only, so no dynamic_cast in the inner call graph—dynamic_cast at the composition root (factory / registry) is often enough.
14.3 Serialization: bytes vs reinterpret
Do not reinterpret_cast a POD struct pointer and stream it if you care about portability (padding, alignment, endianness, float representation). A portable pattern: explicit wire format + std::bit_cast or memcpy into local correctly aligned variables after bounds checks.
// Sketch: read 4 big-endian bytes into uint32, then use value
// Always validate buffer length before any cast to T*
reinterpret_cast to char* is common for sending buffers to the OS, not for reinterpreting the same memory as a different C++ object type.
14.4 Portable binary layout: explicit read_uint32_be (no object slicing through cast)
A pattern that avoids casting a char* to T* and reading a struct in one go:
- Read raw bytes from the wire (or
std::span<std::byte const>). - Assemble a scalar in local variables with defined byte order (shift-or).
- Optionally
std::bit_casttofloatwhen size andtrivially_copyablematch.
#include <cstdint>
#include <span>
// Example: 4-byte big-endian unsigned (network order)
std::uint32_t read_u32_be(std::span<std::uint8_t const, 4> b) {
return (static_cast<std::uint32_t>(b[0]) << 24u) |
(static_cast<std::uint32_t>(b[1]) << 16u) |
(static_cast<std::uint32_t>(b[2]) << 8u) |
(static_cast<std::uint32_t>(b[3]));
}
static_cast here is the mathematical conversion from the promoted byte to the wider uint32_t — always prefer this over a “clever” reinterpret_cast of the whole struct when ABI and portability matter.
14.5 COM, Win32, and “opaque” void*
Windows APIs and COM often use void* and IUnknown*-style type discovery (QueryInterface) instead of C++ dynamic_cast across DLL boundaries, because the C++ mangling, RTTI, and destructor of objects must match across modules. A C++ dynamic_cast between objects created in different DLLs with mismatched RTTI or vtables is a classic hard-to-debug crash. The lesson generalizes: at module boundaries, prefer a small C ABI or virtual interface designed for that boundary, not ad hoc casts.
15. Debugging: finding problematic casts
- Grep and CI: fail builds on C-style cast for new C++ with
-Werror=old-style-castin modules you control. - UndefinedBehaviorSanitizer (UBSan) and AddressSanitizer (ASan) on test binaries catch many bad loads after bad casts, especially in combination with strict aliasing violations.
- Valgrind / MSVC debug heap for use-after-free; casts often hide the original type mistake.
- Log
typeidin debug builds at plugin boundaries to verify the dynamic type. - Break on
std::bad_castwhen usingdynamic_castto reference types. - Static analysis (clang-tidy checks like
google-readability-casting,cppcoreguidelines-pro-type-reinterpret-cast).
16. Alternatives: visitor pattern, std::variant, and virtual methods
Visitor (double dispatch)
struct Circle;
struct Square;
struct ShapeVisitor {
virtual void on(Circle const&) = 0;
virtual void on(Square const&) = 0;
};
struct Shape {
virtual ~Shape() = default;
virtual void accept(ShapeVisitor& v) const = 0;
};
When it wins: closed set of types in one module, and you can add operations more often than shapes.
std::variant<Circle, Square> + std::visit
- Closed set at compile time; exhaustive handling with
overloadedlambda pattern. - No RTTI; often faster and clearer than a chain of
dynamic_cast.
Virtual interface
- best default for extensible plugin ecosystems where new concrete types are added without recompiling the core.
Summary: replace “RTTI + cast soup” in the middle of your domain model with a single decision point: factory, vtable, or variant—not all three for the same abstraction.
FAQ (quick)
Q: Which single cast is “default”?
A: static_cast for well-defined static conversions. Never “default” to reinterpret_cast or C-style.
Q: Is dynamic_cast a design smell?
A: Sometimes. At composition boundaries (plugins) it is normal; in tight inner loops, prefer a virtual or std::visit.
Q: C++20 std::span and casts?
A: span retypes a contiguous buffer’s element type without allocation; you still must respect lifetime and const correctness. It does not replace static_cast for numeric or hierarchy rules.
Q: Can I dynamic_cast from void*?
A: dynamic_cast requires typed pointers to polymorphic types in the pointer form; void* in general is not a substitute—convert back to a known static type with great care, usually through your API’s opaque handle with tag.
Related posts (internal)
- C++ Type Conversion Guide
- C++
explicitkeyword - C++ Type traits
The named C++ cast operators make your intent visible to the human reader, the compiler, and your static analysis tools. Use the narrowest cast, the smallest surface where a cast is unavoidable, and the design that makes the need for a cast the exception, not the rule.