Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Templates & Concepts
C++

Templates & Concepts

Practice 18 questions covering function and class templates, template specialization, concepts, constraints, and generic programming.

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 requires clauses 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 requires clauses 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 (intdouble), 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 (intdouble), 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 → prints 1.
  • 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 → prints 0.

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 → prints 1.
  • 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 → prints 0.

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 typesconst 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: falseint 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 typesconst 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: falseint 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::hash specialization.

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::hash specialization.

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; has unordered state).

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; has unordered state).

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)x as an entity (an id-expression naming a variable) → yields the declared type: int.
  • decltype((x))x in parentheses → treated as a general expression → the expression (x) is an lvalue (a named variable) → yields int&.

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)x as an entity (an id-expression naming a variable) → yields the declared type: int.
  • decltype((x))x in parentheses → treated as a general expression → the expression (x) is an lvalue (a named variable) → yields int&.

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.
  • memcpy into 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.
  • memcpy into 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 accessstd::get<T>(v) returns the value or throws std::bad_variant_access; std::visit dispatches 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 accessstd::get<T>(v) returns the value or throws std::bad_variant_access; std::visit dispatches 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.

My Private Notes

Notes are auto-saved locally to this device.