본문으로 건너뛰기
Previous
Next
C++ Casting

C++ Casting

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 operatorsstatic_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_cast cannot 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 const removal: the same ( token might strip const when 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

Takeawaystatic_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_cast may 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 a const view). 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-const but does not write, if you can prove it.
  • In generic code that adds const to 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_cast as 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.

  1. Design away the need when I can: virtual function, Liskov-safe interface, or std::variant for a closed set. No cast in the hot path is still the best cast.

  2. static_cast for 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
  1. dynamic_cast when 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
}
  1. const_cast to adjust cv-qualification at a boundary to an old API, never as a back door to “make the compiler quiet.”

  2. reinterpret_cast (or C++20 std::bit_cast for values) in the narrow, documented places: hardware, handles, and interop. Everything else in application code gets a why in the review, not a shrug.

  3. C-style ( — in new code, I treat it the same as an unexplained goto: someone else can merge it, I will not.


8. RTTI: typeid, dynamic_cast, and the cost model

RTTI provides:

  • dynamic_cast for polymorphic types.
  • typeid for obtaining a std::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 typestypeid 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 nametype_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) where a is const std::any& or std::any& returns a copy (or const ref overloads where applicable) and throws std::bad_any_cast if the held type is not exactly T (decay rules apply; see the Standard for cv/ref nuances).
  • std::any_cast<T>(&a) returns a pointernullptr if the type does not match — which mirrors the dynamic_cast pointer / 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

ConstructCost model
static_cast (numeric)Same as the underlying conversion (e.g. doubleint 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_castAlgorithm + data-structure dependent: typically O(depth) of inheritance or O(1) with ABI-specific fixed limits; much more than a pointer load
const_castNo-op at assembly level; danger is semantic
reinterpret_castNo-op; danger is wrong load/store and aliasing
C-styleWhatever the composed cast does
typeid on polymorphic referenceMay have a cost (implementation-defined) similar to a vptr indirection; sometimes optimized
std::any type recoveryHeap 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 of unique_ptr<ConcreteBaseInterface>.
  • Prefer free functions on Base& with virtual hooks over repeated dynamic_cast chains in callers.
  • Tag dispatch and vtables — store an enum in a known layout only when profiling proves dynamic_cast is a bottleneck; document ABI.
  • Templates and concepts (C++20) avoid object-level casts in generic code.
  • Minimize const_cast; fix APIs or use mutable for caches.
  • Encapsulate the one reinterpret_cast that 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_cast downcast a Base* that actually points to UnrelatedUB on use.
  • dynamic_cast from 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 for dynamic_cast where required.
  • const_cast write to read-only storage or a const object → UB.
  • reinterpret_cast between unrelated pointers and dereferencing as a different object type can violate strict aliasingUB unless a Standard exception applies (use char / std::byte to copy bytes, or std::bit_cast for values).
  • Misaligned access on some architectures (ARM, older) → bus fault or UB.
  • Casting this in 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:

  1. Read raw bytes from the wire (or std::span<std::byte const>).
  2. Assemble a scalar in local variables with defined byte order (shift-or).
  3. Optionally std::bit_cast to float when size and trivially_copyable match.
#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-cast in 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 typeid in debug builds at plugin boundaries to verify the dynamic type.
  • Break on std::bad_cast when using dynamic_cast to 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 overloaded lambda 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.


  • C++ Type Conversion Guide
  • C++ explicit keyword
  • 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.