1. What is the difference between constexpr and C++20 consteval?
Answer: A constexpr function may evaluate at compile time or runtime; a consteval function (an “immediate function”) must evaluate at compile time.
constexpr is flexible: if you call it with constant expressions, the compiler can compute it at compile time (and typically does); if you call it with runtime values, it falls back to normal runtime execution. It’s a “may run at compile time” function.
consteval (C++20) removes the fallback: the function must be evaluated at compile time. Calling it with anything other than a constant expression is a compile error. It’s a “must run at compile time” function.
The use cases follow:
constexpr— a function that should be usable in constant contexts (array sizes, template args) but also fine at runtime.consteval— a function you intend only for compile-time computation, typically because its result is required in a constant context, or to guarantee no runtime cost ever.
The interview answer: constexpr = compile-time or runtime; consteval = compile-time only, non-constant calls are rejected at compile time.
Answer:
A constexpr function may evaluate at compile time or runtime; a consteval function (an “immediate function”) must evaluate at compile time.
constexpr is flexible: if you call it with constant expressions, the compiler can compute it at compile time (and typically does); if you call it with runtime values, it falls back to normal runtime execution. It’s a “may run at compile time” function.
consteval (C++20) removes the fallback: the function must be evaluated at compile time. Calling it with anything other than a constant expression is a compile error. It’s a “must run at compile time” function.
The use cases follow:
constexpr— a function that should be usable in constant contexts (array sizes, template args) but also fine at runtime.consteval— a function you intend only for compile-time computation, typically because its result is required in a constant context, or to guarantee no runtime cost ever.
The interview answer: constexpr = compile-time or runtime; consteval = compile-time only, non-constant calls are rejected at compile time.
2. What design pattern or problem do C++20 Concepts simplify compared to C++11/14 SFINAE?
Answer: Template parameter constraints — replacing the verbose, cryptic SFINAE (std::enable_if) idiom with clear, readable, and better-diagnosing syntax.
Before C++20, constraining templates meant SFINAE (Substitution Failure Is Not An Error): buried typename std::enable_if<...>::type in template parameter lists, return types, or helper structs. It works, but it’s hard to read, produces horrific error messages when a constraint fails, and is a pain to compose.
C++20 concepts make constraints first-class:
template<typename T>
requires std::integral<T> // a constraint
T half(T x) { return x / 2; }
or the compact form: template<std::integral T>. The benefits:
- Readability — the intent (“T must be an integral type”) is stated directly.
- Better diagnostics — a failed constraint check produces a message naming the concept, instead of a wall of substitution noise.
- Overload resolution — concepts participate cleanly, and
requiresclauses can express relationships between parameters.
The interview answer: Concepts replace SFINAE/enable_if for constraining templates, with cleaner syntax, faster compile times, and far better error messages.
Answer:
Template parameter constraints — replacing the verbose, cryptic SFINAE (std::enable_if) idiom with clear, readable, and better-diagnosing syntax.
Before C++20, constraining templates meant SFINAE (Substitution Failure Is Not An Error): buried typename std::enable_if<...>::type in template parameter lists, return types, or helper structs. It works, but it’s hard to read, produces horrific error messages when a constraint fails, and is a pain to compose.
C++20 concepts make constraints first-class:
template<typename T>
requires std::integral<T> // a constraint
T half(T x) { return x / 2; }
or the compact form: template<std::integral T>. The benefits:
- Readability — the intent (“T must be an integral type”) is stated directly.
- Better diagnostics — a failed constraint check produces a message naming the concept, instead of a wall of substitution noise.
- Overload resolution — concepts participate cleanly, and
requiresclauses can express relationships between parameters.
The interview answer: Concepts replace SFINAE/enable_if for constraining templates, with cleaner syntax, faster compile times, and far better error messages.
3. What is the key functional difference between reinterpret_cast and static_cast?
Answer: static_cast performs conversions the compiler can check at compile time based on known type relationships; reinterpret_cast reinterprets the raw bit pattern between unrelated types without any checks.
static_cast operates within the rules of the type system: numeric conversions (int ↔ double), up/down casts within a known inheritance hierarchy, void* to typed pointer, and so on. The compiler validates the relationship — a downcast through static_cast assumes the object really is that derived type (no runtime check), but the types must be related.
reinterpret_cast ignores the type system entirely: it tells the compiler “just treat this bit pattern as that other type.” reinterpret_cast<int*>(&someFloat) reinterprets the bytes of a float as an int pointer — no relationship required, no check performed. This is exactly the territory where strict-aliasing UB lives.
The contrast:
static_cast— safe-ish, compile-time-checked, type-system-aware.reinterpret_cast— raw bit reinterpretation, unchecked, easily UB.
The interview answer: static_cast uses compile-time-known relationships; reinterpret_cast blindly reinterprets bit patterns with no verification.
Answer:
static_cast performs conversions the compiler can check at compile time based on known type relationships; reinterpret_cast reinterprets the raw bit pattern between unrelated types without any checks.
static_cast operates within the rules of the type system: numeric conversions (int ↔ double), up/down casts within a known inheritance hierarchy, void* to typed pointer, and so on. The compiler validates the relationship — a downcast through static_cast assumes the object really is that derived type (no runtime check), but the types must be related.
reinterpret_cast ignores the type system entirely: it tells the compiler “just treat this bit pattern as that other type.” reinterpret_cast<int*>(&someFloat) reinterprets the bytes of a float as an int pointer — no relationship required, no check performed. This is exactly the territory where strict-aliasing UB lives.
The contrast:
static_cast— safe-ish, compile-time-checked, type-system-aware.reinterpret_cast— raw bit reinterpretation, unchecked, easily UB.
The interview answer: static_cast uses compile-time-known relationships; reinterpret_cast blindly reinterprets bit patterns with no verification.
4. What is the primary operational mechanism of Coroutines introduced in C++20?
Answer: Functions that can suspend and resume execution (co_await, co_yield), preserving state in a heap-allocated coroutine frame.
C++20 coroutines are stackless, non-preemptive functions. When a coroutine hits co_await or co_yield, it suspends: control returns to the caller, but the function’s local state (parameters, locals, where it was) is saved in a coroutine frame, typically heap-allocated. Later, the coroutine is resumed from exactly where it suspended, with its state intact.
Key properties:
- Stackless — the suspension isn’t tied to an OS thread or a CPU stack; many suspended coroutines can coexist on one thread.
- Non-preemptive — the coroutine decides when to yield; it isn’t forced off the CPU.
- State in the frame — everything needed to resume lives in the coroutine frame, not on the stack, which is why the frame must persist across suspension.
This powers efficient async/await patterns and generator-style lazy sequences. The interview answer: suspend/resume functions that save state in a heap-allocated coroutine frame, letting control return to the caller and resume later.
Answer:
Functions that can suspend and resume execution (co_await, co_yield), preserving state in a heap-allocated coroutine frame.
C++20 coroutines are stackless, non-preemptive functions. When a coroutine hits co_await or co_yield, it suspends: control returns to the caller, but the function’s local state (parameters, locals, where it was) is saved in a coroutine frame, typically heap-allocated. Later, the coroutine is resumed from exactly where it suspended, with its state intact.
Key properties:
- Stackless — the suspension isn’t tied to an OS thread or a CPU stack; many suspended coroutines can coexist on one thread.
- Non-preemptive — the coroutine decides when to yield; it isn’t forced off the CPU.
- State in the frame — everything needed to resume lives in the coroutine frame, not on the stack, which is why the frame must persist across suspension.
This powers efficient async/await patterns and generator-style lazy sequences. The interview answer: suspend/resume functions that save state in a heap-allocated coroutine frame, letting control return to the caller and resume later.
5. What does decltype(auto) evaluate to when deducting function return types?
Answer: It uses decltype rules, preserving the exact value category and reference type of the expression — returning T&, T&&, or T as appropriate.
auto return type deduction uses template deduction rules, which strip top-level const and references — return x; where x is a int& would deduce int (a value copy). decltype(auto) instead applies decltype semantics to the returned expression: it yields exactly what decltype(expression) would — int&, int&&, or int.
auto get_val() { return x; } // deduces int (copy)
decltype(auto) get_ref() { return x; } // deduces int& (reference)
This is what lets you write perfect-forwarding wrapper functions that transparently preserve whether their return is a value, lvalue reference, or rvalue reference. The interview answer: decltype(auto) preserves exact types and reference categories per decltype rules, unlike auto which strips them.
Answer:
It uses decltype rules, preserving the exact value category and reference type of the expression — returning T&, T&&, or T as appropriate.
auto return type deduction uses template deduction rules, which strip top-level const and references — return x; where x is a int& would deduce int (a value copy). decltype(auto) instead applies decltype semantics to the returned expression: it yields exactly what decltype(expression) would — int&, int&&, or int.
auto get_val() { return x; } // deduces int (copy)
decltype(auto) get_ref() { return x; } // deduces int& (reference)
This is what lets you write perfect-forwarding wrapper functions that transparently preserve whether their return is a value, lvalue reference, or rvalue reference. The interview answer: decltype(auto) preserves exact types and reference categories per decltype rules, unlike auto which strips them.
6. What is the purpose of tag dispatching in template metaprogramming?
Answer: Selecting between function overloads at compile time based on type traits — typically iterator categories — using empty tag classes as dummy arguments.
Tag dispatching works on overload resolution. Empty tag types (like std::random_access_iterator_tag, std::forward_iterator_tag) are passed as parameters to differently overloaded functions. The compiler picks the best match at compile time based on the tag the caller supplies:
void advance(It& it, Diff n, std::random_access_iterator_tag) {
it += n; // O(1) for random access
}
void advance(It& it, Diff n, std::input_iterator_tag) {
while (n--) ++it; // step-by-step otherwise
}
template<typename It>
void advance(It& it, Diff n) {
advance(it, n, typename std::iterator_traits<It>::iterator_category{});
}
The tag parameter has no runtime cost (empty struct, probably optimized away); it exists purely to steer overload resolution. This is how algorithms like std::advance choose the optimal implementation for each iterator category. The interview answer: compile-time overload selection driven by empty tag classes representing type categories like iterator kinds.
Answer:
Selecting between function overloads at compile time based on type traits — typically iterator categories — using empty tag classes as dummy arguments.
Tag dispatching works on overload resolution. Empty tag types (like std::random_access_iterator_tag, std::forward_iterator_tag) are passed as parameters to differently overloaded functions. The compiler picks the best match at compile time based on the tag the caller supplies:
void advance(It& it, Diff n, std::random_access_iterator_tag) {
it += n; // O(1) for random access
}
void advance(It& it, Diff n, std::input_iterator_tag) {
while (n--) ++it; // step-by-step otherwise
}
template<typename It>
void advance(It& it, Diff n) {
advance(it, n, typename std::iterator_traits<It>::iterator_category{});
}
The tag parameter has no runtime cost (empty struct, probably optimized away); it exists purely to steer overload resolution. This is how algorithms like std::advance choose the optimal implementation for each iterator category. The interview answer: compile-time overload selection driven by empty tag classes representing type categories like iterator kinds.
7. What will be the output of the following SFINAE trait test code?
template <typename T, typename = void>
struct has_serialize : std::false_type {};
template <typename T>
struct has_serialize<T, std::void_t<decltype(std::declval<T>().serialize())>> : std::true_type {};
struct Foo { void serialize() {} };
struct Bar {};
std::cout << has_serialize<Foo>::value << has_serialize<Bar>::value;
Output: 10.
This is the classic SFINAE “detection idiom” using std::void_t. The primary template defaults to std::false_type (0). The partial specialization is selected only if the expression std::declval<T>().serialize() is well-formed for T:
- For
Foo: it has a.serialize()member, so the expression is valid → the partial specialization matches →std::true_type→ prints1. - For
Bar: no.serialize(), so the expression is ill-formed. Substitution fails, but SFINAE (Substitution Failure Is Not An Error) silently discards the specialization → falls back to the primary template →std::false_type→ prints0.
Output: 10. The interview answer: 1 for Foo (has .serialize()), 0 for Bar (doesn’t), so the output is 10.
Answer:
10.
This is the classic SFINAE “detection idiom” using std::void_t. The primary template defaults to std::false_type (0). The partial specialization is selected only if the expression std::declval<T>().serialize() is well-formed for T:
- For
Foo: it has a.serialize()member, so the expression is valid → the partial specialization matches →std::true_type→ prints1. - For
Bar: no.serialize(), so the expression is ill-formed. Substitution fails, but SFINAE (Substitution Failure Is Not An Error) silently discards the specialization → falls back to the primary template →std::false_type→ prints0.
Output: 10. The interview answer: 1 for Foo (has .serialize()), 0 for Bar (doesn’t), so the output is 10.
8. What is the output of the following integer bitwise manipulation?
int x = 5; // 0101 in binary
int y = x << 2;
std::cout << y;
Output: 20.
Shifting left by N bits multiplies the value by 2^N. 5 << 2 = 5 × 4 = 20. In binary, 5 is 0101; shifting left two positions gives 10100, which is 16 + 4 = 20. Output: 20. (Watch for signed-overflow UB at extremes, but this example is well within range.)
Answer:
20.
Shifting left by N bits multiplies the value by 2^N. 5 << 2 = 5 × 4 = 20. In binary, 5 is 0101; shifting left two positions gives 10100, which is 16 + 4 = 20. Output: 20. (Watch for signed-overflow UB at extremes, but this example is well within range.)
9. What is the evaluation of std::is_same_v<int, const int>?
Answer: false.
std::is_same<T, U> asks whether two types are exactly identical. int and const int are distinct types — const is a top-level qualifier that makes them different. const on the type isn’t stripped when comparing with is_same; you’d have to remove it explicitly (std::remove_const_t<int> == int) to get true.
So std::is_same_v<int, const int> evaluates to false, while std::is_same_v<int, int> is true. The interview answer: false — int and const int are different types.
Answer:
false.
std::is_same<T, U> asks whether two types are exactly identical. int and const int are distinct types — const is a top-level qualifier that makes them different. const on the type isn’t stripped when comparing with is_same; you’d have to remove it explicitly (std::remove_const_t<int> == int) to get true.
So std::is_same_v<int, const int> evaluates to false, while std::is_same_v<int, int> is true. The interview answer: false — int and const int are different types.
10. What does the compile-time expression sizeof…(args) do when expanding variadic templates?
Answer: It evaluates the number of elements in the parameter pack at compile time.
sizeof...(args) is the pack-size operator, usable only with variadic template parameter packs. It returns the count of arguments in the pack as a compile-time constant — not the total byte size of the arguments (that would be sizeof per-element, summed or sizeof on a fold).
template<typename... Args>
size_t count() { return sizeof...(Args); } // e.g., 3 for <int, char, double>
It’s a constant expression, so it can be used in static assertions, array sizes, and template logic — often to stop recursion or to select overloads during pack expansion. The interview answer: the exact count of elements in the pack, computed at compile time.
Answer:
It evaluates the number of elements in the parameter pack at compile time.
sizeof...(args) is the pack-size operator, usable only with variadic template parameter packs. It returns the count of arguments in the pack as a compile-time constant — not the total byte size of the arguments (that would be sizeof per-element, summed or sizeof on a fold).
template<typename... Args>
size_t count() { return sizeof...(Args); } // e.g., 3 for <int, char, double>
It’s a constant expression, so it can be used in static assertions, array sizes, and template logic — often to stop recursion or to select overloads during pack expansion. The interview answer: the exact count of elements in the pack, computed at compile time.
11. What is the purpose of std::type_index in C++?
Answer: It wraps a std::type_info reference into a hashable, comparable, copyable object usable as a map key (e.g., in std::unordered_map).
typeid(T) returns a std::type_info, but type_info can’t be copied and has no ordering/hash support directly. std::type_index wraps it:
- Copyable (like a value).
- Supports
==,<(ordering). - Provides a
std::hashspecialization.
That makes it a natural key for maps from a type to associated data:
std::unordered_map<std::type_index, std::string> names;
names[typeid(int)] = "int";
This is the tool for runtime type-keyed registries and dynamic dispatch tables. The interview answer: a hashable, comparable, copyable wrapper around typeid results, designed for use as map keys.
Answer:
It wraps a std::type_info reference into a hashable, comparable, copyable object usable as a map key (e.g., in std::unordered_map).
typeid(T) returns a std::type_info, but type_info can’t be copied and has no ordering/hash support directly. std::type_index wraps it:
- Copyable (like a value).
- Supports
==,<(ordering). - Provides a
std::hashspecialization.
That makes it a natural key for maps from a type to associated data:
std::unordered_map<std::type_index, std::string> names;
names[typeid(int)] = "int";
This is the tool for runtime type-keyed registries and dynamic dispatch tables. The interview answer: a hashable, comparable, copyable wrapper around typeid results, designed for use as map keys.
12. What is the evaluation result of the following fold expression introduced in C++17?
template<typename... Args>
auto sum(Args... args) {
return (... + args);
}
// Called as: sum(1, 2, 3, 4);
Output: 10.
(... + args) is a unary left fold. It expands the pack into left-associative nested additions: (((1 + 2) + 3) + 4). 1 + 2 + 3 + 4 = 10. Output: 10.
Fold expressions (C++17) let you operate over parameter packs without recursion — with (... + args) (left fold) or (args + ...) (right fold). The interview answer: 10 — left fold expands to (((1+2)+3)+4).
Answer:
10.
(... + args) is a unary left fold. It expands the pack into left-associative nested additions: (((1 + 2) + 3) + 4). 1 + 2 + 3 + 4 = 10. Output: 10.
Fold expressions (C++17) let you operate over parameter packs without recursion — with (... + args) (left fold) or (args + ...) (right fold). The interview answer: 10 — left fold expands to (((1+2)+3)+4).
13. What is the behavior of the C++20 spaceship operator (<=>)?
Answer: It performs a three-way comparison, returning an object indicating less-than, equal, or greater-than in one operation.
a <=> b compares two values and returns a comparison-category object that encodes the full ordering: it’s 0 if equal, < 0 if a is less, > 0 if a is greater — but as a type with rich semantics, not just an int.
The return category is one of:
std::strong_ordering— total order, indistinguishable equal values.std::weak_ordering— like strong but equivalent values can differ.std::partial_ordering— some values incomparable (e.g., NaN in floats; hasunorderedstate).
A single auto operator<=>(const T&) = default; lets the compiler generate all comparison operators (<, <=, >, >=) for a class. The interview answer: one operation computes less/equal/greater, returning a comparison-category type (strong/weak/partial ordering).
Answer:
It performs a three-way comparison, returning an object indicating less-than, equal, or greater-than in one operation.
a <=> b compares two values and returns a comparison-category object that encodes the full ordering: it’s 0 if equal, < 0 if a is less, > 0 if a is greater — but as a type with rich semantics, not just an int.
The return category is one of:
std::strong_ordering— total order, indistinguishable equal values.std::weak_ordering— like strong but equivalent values can differ.std::partial_ordering— some values incomparable (e.g., NaN in floats; hasunorderedstate).
A single auto operator<=>(const T&) = default; lets the compiler generate all comparison operators (<, <=, >, >=) for a class. The interview answer: one operation computes less/equal/greater, returning a comparison-category type (strong/weak/partial ordering).
14. What is the evaluation result of calling sizeof on a raw reference type (sizeof(int&) execution)?
Answer: It returns the size of the referenced type (sizeof(int), typically 4 bytes) — not the size of a pointer.
A reference is an alias to an object, not a separate variable with its own storage. sizeof applied to a reference yields the size of the thing it refers to. sizeof(int&) == sizeof(int) == 4 on typical platforms.
(If you want the size of a pointer, you’d ask for sizeof(int*) — a reference isn’t a pointer, though implementations often compile it to one internally. The language semantics treat it as the referenced object.) The interview answer: sizeof(int&) equals sizeof(int) (4 bytes) because references alias their targets.
Answer:
It returns the size of the referenced type (sizeof(int), typically 4 bytes) — not the size of a pointer.
A reference is an alias to an object, not a separate variable with its own storage. sizeof applied to a reference yields the size of the thing it refers to. sizeof(int&) == sizeof(int) == 4 on typical platforms.
(If you want the size of a pointer, you’d ask for sizeof(int*) — a reference isn’t a pointer, though implementations often compile it to one internally. The language semantics treat it as the referenced object.) The interview answer: sizeof(int&) equals sizeof(int) (4 bytes) because references alias their targets.
15. What is the outcome of passing std::ref(x) to a function template that accepts parameters by value?
Answer: x is wrapped in a std::reference_wrapper<T> — a copyable value-like object that refers back to x, letting by-value APIs modify the original.
Some APIs require by-value parameters: std::bind, std::thread constructors, std::thread/async argument passing. If you pass x directly, they receive a copy, and modifications inside don’t affect the caller’s x. std::ref(x) creates a std::reference_wrapper<T> — a small copyable object storing a reference. Passing that by value still refers to the original:
std::thread t([](int& v){ v = 42; }, std::ref(x));
// x (the caller's variable) gets 42
reference_wrapper has an implicit conversion to T&, so it behaves like a reference where one is expected. The interview answer: std::ref wraps x in a std::reference_wrapper<T>, emulating reference semantics through by-value parameter passing.
Answer:
x is wrapped in a std::reference_wrapper<T> — a copyable value-like object that refers back to x, letting by-value APIs modify the original.
Some APIs require by-value parameters: std::bind, std::thread constructors, std::thread/async argument passing. If you pass x directly, they receive a copy, and modifications inside don’t affect the caller’s x. std::ref(x) creates a std::reference_wrapper<T> — a small copyable object storing a reference. Passing that by value still refers to the original:
std::thread t([](int& v){ v = 42; }, std::ref(x));
// x (the caller's variable) gets 42
reference_wrapper has an implicit conversion to T&, so it behaves like a reference where one is expected. The interview answer: std::ref wraps x in a std::reference_wrapper<T>, emulating reference semantics through by-value parameter passing.
16. What is the evaluation result of applying decltype to an unparenthesized variable name (decltype(x)) vs a parenthesized expression (decltype((x))) where int x = 10;?
Answer: decltype(x) is int; decltype((x)) is int& (an lvalue reference).
decltype distinguishes between naming an entity and naming an expression:
decltype(x)—xas an entity (an id-expression naming a variable) → yields the declared type:int.decltype((x))—xin parentheses → treated as a general expression → the expression(x)is an lvalue (a named variable) → yieldsint&.
This distinction is exactly why decltype(auto) behaves differently from auto, and why decltype((x)) can be used to form an lvalue reference. The interview answer: decltype(x) → int; decltype((x)) → int&.
Answer:
decltype(x) is int; decltype((x)) is int& (an lvalue reference).
decltype distinguishes between naming an entity and naming an expression:
decltype(x)—xas an entity (an id-expression naming a variable) → yields the declared type:int.decltype((x))—xin parentheses → treated as a general expression → the expression(x)is an lvalue (a named variable) → yieldsint&.
This distinction is exactly why decltype(auto) behaves differently from auto, and why decltype((x)) can be used to form an lvalue reference. The interview answer: decltype(x) → int; decltype((x)) → int&.
17. What is the role of std::bit_cast introduced in C++20?
Answer: It safely reinterprets the raw bit pattern of an object as another type of identical size, without strict-aliasing UB, and works in constexpr contexts.
std::bit_cast<To>(from) copies from’s bits into a To object of the same size. Compared to the old tricks:
reinterpret_cast— often violates strict aliasing when you then read the result.memcpyinto a target — works but is verbose and not constexpr-friendly.
bit_cast is the clean, defined path: it does the equivalent of a memcpy under the hood, which is the only well-defined way to reinterpret representations in C++. It requires both types to be trivially copyable and the same size.
float f = 1.5f;
uint32_t bits = std::bit_cast<uint32_t>(f); // raw IEEE-754 bits
It’s also constexpr, so usable in compile-time computations. The interview answer: a defined, constexpr-safe bit-reinterpretation (memcpy-like) for same-size trivially copyable types, avoiding strict-aliasing UB.
Answer:
It safely reinterprets the raw bit pattern of an object as another type of identical size, without strict-aliasing UB, and works in constexpr contexts.
std::bit_cast<To>(from) copies from’s bits into a To object of the same size. Compared to the old tricks:
reinterpret_cast— often violates strict aliasing when you then read the result.memcpyinto a target — works but is verbose and not constexpr-friendly.
bit_cast is the clean, defined path: it does the equivalent of a memcpy under the hood, which is the only well-defined way to reinterpret representations in C++. It requires both types to be trivially copyable and the same size.
float f = 1.5f;
uint32_t bits = std::bit_cast<uint32_t>(f); // raw IEEE-754 bits
It’s also constexpr, so usable in compile-time computations. The interview answer: a defined, constexpr-safe bit-reinterpretation (memcpy-like) for same-size trivially copyable types, avoiding strict-aliasing UB.
18. What design pattern does std::variant implement in modern C++?
Answer: A type-safe, non-allocating tagged union — it holds exactly one value from a set of alternative types at any time.
std::variant<A, B, C> is a discriminated union:
- Holds a value of one of its template alternatives at a time (no more, no less).
- Tracks the active alternative internally — no manual discriminant management, and safe access.
- No dynamic allocation — it’s a fixed-size value type (the alternatives share the storage).
- Type-safe access —
std::get<T>(v)returns the value or throwsstd::bad_variant_access;std::visitdispatches to a visitor over the active alternative.
It replaces error-prone C unions and manual enum+payload tagging with a safe, modern abstraction. The interview answer: a type-safe tagged union holding one of several alternatives, allocation-free, with std::get/std::visit for safe access.
Answer:
A type-safe, non-allocating tagged union — it holds exactly one value from a set of alternative types at any time.
std::variant<A, B, C> is a discriminated union:
- Holds a value of one of its template alternatives at a time (no more, no less).
- Tracks the active alternative internally — no manual discriminant management, and safe access.
- No dynamic allocation — it’s a fixed-size value type (the alternatives share the storage).
- Type-safe access —
std::get<T>(v)returns the value or throwsstd::bad_variant_access;std::visitdispatches to a visitor over the active alternative.
It replaces error-prone C unions and manual enum+payload tagging with a safe, modern abstraction. The interview answer: a type-safe tagged union holding one of several alternatives, allocation-free, with std::get/std::visit for safe access.
Premium Content
Unlock Templates & Concepts and all premium lessons with a subscription.
From ₹199.99/year — See plans