1. What is the behavior of std::array compared to standard C-style arrays?
Answer: std::array wraps a fixed-size C-style array into a stack-allocated container that knows its length and supports standard iterators and value copy-assignment.
std::array<T, N> is a thin, zero-overhead wrapper around T[N]. Unlike a raw C array, it:
- Knows its size —
.size()returnsN; no moresizeof(arr)/sizeof(arr[0])hacks. - Passes by value properly — raw arrays decay to pointers when passed to functions;
std::arrayis a real type that can be copied, assigned, and returned by value. - Works with the STL —
.begin()/.end(), range-for, and standard algorithms all work directly. - Offers safety when asked —
.at(i)does bounds-checked access (throwsstd::out_of_range), whileoperator[]stays unchecked like a raw array.
And critically, it has zero abstraction overhead: it’s a stack array with no dynamic allocation, laid out identically to T[N]. The interview answer: a fixed-size stack container wrapping T[N] that knows its length, supports value semantics and STL algorithms, with no heap allocation.
2. 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.
3. 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.
4. What causes a std::system_error exception during std::thread construction?
Answer: The operating system fails to create the underlying thread — resource exhaustion or hitting OS thread limits.
std::thread isn’t a user-space abstraction; constructing one asks the OS to spawn a native thread. When that OS-level creation fails — out of memory, the process has hit its thread limit, or the OS is otherwise out of resources — the constructor throws std::system_error (which wraps the OS error code).
The constructor does throw on failure (it doesn’t silently leave you with an invalid thread). Other causes of thread-related system errors include permission issues or passing a std::thread in a move-invalid state — but the canonical case is OS thread creation failing due to exhaustion.
The interview answer: OS-level thread creation fails (resource exhaustion / thread limits), and the constructor throws std::system_error.
5. What is the role of std::launder introduced in C++17?
Answer: It tells the compiler that a new object now lives at an address that previously held another object, defeating invalid optimizer assumptions based on pointer identity.
std::launder is for a subtle lifetime problem. Consider a member function that returns a pointer to its own object, where the object was reconstructed at the same address via placement new. The compiler may legally assume the pointer refers to the original object (pointer identity/aliasing invariants). After placement-new replaces the object, that assumption is invalid — yet the compiler doesn’t know.
T* p = new (mem) T(); // construct new T at old address
return std::launder(p); // "trust me, this is the new object"
std::launder produces a pointer to the new object at that address, suppressing the stale assumptions. It’s mainly needed in advanced scenarios: reusing storage for a new object, type punning in optional-like containers, or returning this from a method where the object was recreated. For ordinary code you rarely need it. The interview answer: std::launder yields a valid pointer to an object newly constructed at an address previously occupied by another object, defeating stale optimization assumptions.
6. What is the evaluation result of alignof(std::max_align_t)?
Answer: The maximum alignment requirement supported by the platform for scalar types — typically 8 or 16 bytes.
std::max_align_t is a special scalar type whose alignment requirement is at least as strict as every other scalar type on the platform. Taking alignof(std::max_align_t) therefore reports the largest scalar alignment the architecture supports — commonly 8 bytes (32-bit) or 16 bytes (64-bit with long double). It’s essentially the answer to “what alignment do I need for any scalar?”
It matters because malloc/operator new are required to return memory aligned suitably for std::max_align_t, which is why heap allocations work for any scalar type. The interview answer: the platform’s maximum scalar alignment (typically 8 or 16 bytes), used as the baseline guarantee for heap allocations.
7. What happens if std::vector::reserve() is called with a capacity less than the vector’s current capacity()?
Answer: The call is safely ignored — capacity stays unchanged, elements and iterators untouched.
reserve(n) only grows: it reallocates only if n is greater than the current capacity. If n <= capacity(), nothing happens — no reallocation, no invalidation, no shrink. The request is a no-op.
To actually shrink, you use shrink_to_fit() (a non-binding request — the implementation may or may not reduce capacity). The contrast is important:
reserve(n)— “make capacity at least n” (never shrinks).shrink_to_fit()— “make capacity fit size” (best-effort, non-binding).
The interview answer: reserve(n) with n <= capacity() is ignored, leaving capacity, elements, and iterators unchanged.
8. What is the functionality of std::call_once combined with std::once_flag?
Answer: It guarantees a callable executes exactly once, even when multiple threads invoke std::call_once concurrently with the same flag.
std::call_once(flag, func) is the thread-safe “run this initialization exactly once” primitive:
- Whichever thread reaches
std::call_oncefirst runsfunc. - All other threads block until that run completes.
- If
functhrows, the flag resets and another thread will retry it; if it completes normally, the flag is set and later calls do nothing.
std::once_flag initFlag;
void ensureInit() {
std::call_once(initFlag, [] { /* expensive, once-only init */ });
}
This is the classic pattern for lazy thread-safe initialization without mutexes — the standard library’s answer to the double-checked locking problem. The interview answer: run a callable exactly once across all threads, blocking contenders until it finishes.
9. What happens when calling std::future::get() a second time on the same std::future instance?
Answer: It throws std::future_error with std::future_errc::no_state — the shared state was invalidated by the first get().
A std::future is single-use: it owns a handle to shared asynchronous state. The first call to .get() retrieves the result (or rethrows the exception) and invalidates the future — the shared state is moved out. Any subsequent call to .get() finds no state and throws std::future_error.
The same applies to calling .get() on a default-constructed (never-created) future. If you need to fetch the result more than once, use std::shared_future, which supports repeated .get() calls. The interview answer: a second .get() throws std::future_error (no_state) because the first call invalidated the future.
10. 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.
11. 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.
12. What is the lifetime of a temporary object bound to a const lvalue reference?
const std::string& ref = std::string("Hello");
Answer: The temporary’s lifetime is extended to match the reference’s lifetime.
Normally a temporary is destroyed at the end of the full expression. But binding a temporary to a const lvalue reference (or an rvalue reference) triggers lifetime extension: the temporary lives as long as the reference variable does. So ref safely refers to the string for its whole lifetime — no dangling.
const std::string& ref = std::string("Hello");
// ref is valid here, the string is still alive
Note the rule: the reference must be a local reference variable (not a reference member of a class — those don’t extend). The interview answer: binding a temporary to a local const lvalue reference or rvalue reference extends the temporary’s lifetime to match the reference variable’s scope.
13. What is the fundamental requirement for using std::string_view safely?
Answer: The underlying character array it references must remain valid and outlive the string_view.
std::string_view is a non-owning view — just a pointer plus a length over somebody else’s character buffer. It never copies or owns the data. That’s its performance appeal (zero-copy string handling) and its danger:
std::string_view v = getString().c_str(); // getString() destroyed → v dangles
If the backing string is destroyed or modified (e.g., a std::string reallocates), the view becomes a dangling pointer, and reading through it is undefined behavior. So the rule is: the view is only valid as long as the underlying buffer lives. No null-termination is required (it carries its own length), and it’s meant to be cheap to pass by value. The interview answer: the referenced character array must outlive the string_view — it owns nothing.
14. What is the key performance advantage of std::make_shared over constructing std::shared_ptr with a raw pointer?
Answer: std::make_shared performs a single, contiguous allocation for both the managed object and the control block — fewer allocations and better cache locality.
std::shared_ptr<T>(new T()) does two heap allocations: one for T, one for the control block (refcounts, etc.) — which sit in separate memory. std::make_shared<T>() does one allocation big enough for both the object and the control block together, contiguous in memory.
Benefits:
- Fewer allocations = less overhead and less fragmentation.
- Object and control block are adjacent → better cache locality when bumping refcounts.
- Exception-safety:
make_sharedavoids the classic leak if the argument evaluation throws.
The trade-off is the weak_ptr problem (next question): the combined block can’t be freed until weak references are also gone. The interview answer: one contiguous allocation for object + control block, reducing allocation count and improving locality.
15. What occurs if std::shared_ptr objects are created using std::make_shared when an object holds weak references long after its lifetime ends?
Answer: The object’s memory can’t be deallocated until all std::weak_ptr instances are destroyed, because the object and control block share one allocation.
With std::make_shared, the object and control block live in a single allocation. The control block must survive as long as there are any references — strong or weak — because it holds the weak count and is what weak pointers look up to check if the object is alive.
So the timeline is:
- Strong refcount hits 0 → the object’s destructor runs (resources released,
shared_ptrusers can’t access it). - But the raw memory stays allocated because the control block still needs to exist for the surviving
weak_ptrs to query.
Only when the last weak_ptr is destroyed does the whole combined block get freed. The cost: with make_shared, memory is held longer if weak pointers outlive the object. The interview answer: the shared allocation persists until all weak_ptrs die — object destructed at strong-count 0, but memory freed only at weak-count 0.
Premium Content
Unlock Top 50 - Part 1 and all premium lessons with a subscription.
From ₹199.99/year — See plans