1. What is the primary difference between std::lock_guard and std::unique_lock?
Answer: std::unique_lock provides flexibility (deferred locking, manual unlock/lock, condition-variable support, moveability) at extra cost; std::lock_guard is a minimal RAII scope lock.
std::lock_guard is the simple case: it locks a mutex on construction and unlocks on destruction. Nothing more — lightweight, zero flexibility, perfect for a scoped critical section.
std::unique_lock adds capabilities on top:
- Manual locking/unlocking — lock and unlock at arbitrary points (
lock(),unlock()). - Deferred locking — construct without locking, lock later (
std::defer_lock). - Condition-variable support —
std::condition_variable::wait()requires aunique_lock. - Moveable — you can return or store the lock.
- Timeouts / try-lock —
try_lock_for,try_lock_until.
Trade-off: unique_lock carries a bit more overhead (it tracks whether it owns the lock). Rule of thumb: use lock_guard unless you need one of unique_lock’s features. The interview answer: lock_guard is a lightweight RAII scope lock; unique_lock adds manual locking, deferred locking, condition-variable support, and moveability.
2. What is the function of std::condition_variable::wait() when passed a unique lock reference?
Answer: It atomically releases the mutex and suspends the thread, then re-acquires the mutex before returning after being notified.
wait(lock) does a carefully coordinated dance to avoid missed wakeups:
- The calling thread must already hold the mutex (via the
unique_lock). wait()atomically releases the mutex and blocks the thread — “atomic” is critical: it’s a single step, so a notification arriving between release and block can’t be missed.- When
notify_one()/notify_all()(or a spurious wakeup) wakes the thread,wait()re-acquires the mutex before returning — because the caller is expected to inspect the shared condition again while holding the lock.
Because spurious wakeups are possible, the standard pattern wraps wait in a predicate loop:
cv.wait(lock, [] { return ready; }); // loops until ready
The interview answer: atomically unlock + sleep, and on notification re-acquire the mutex before returning — always loop on a predicate to handle spurious wakeups.
3. What design bug is prevented by checking this != &rhs inside custom copy assignment operators?
Answer: Self-assignment — where freeing this’s resources destroys the source (rhs) data before it’s copied, causing memory corruption.
Copy assignment typically: delete existing resources, then copy rhs’s resources. If x = x; (self-assignment) and there’s no identity check:
thisand&rhsare the same object.- Deleting
this’s internal buffers also destroys the very data you’re about to copy fromrhs. - Copying from freed memory → garbage state or crash → undefined behavior.
The guard if (this != &rhs) { ... } skips the whole operation on self-assignment. (Modern alternative: copy-and-swap idiom — copy into a temporary, then swap — which is naturally self-assignment-safe.) The interview answer: it prevents self-assignment, where freeing this’s resources would destroy rhs (the same object) before the copy happens.
4. 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.
5. 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).
6. What happens when calling std::shared_ptr::reset() on a non-empty smart pointer?
Answer: It decrements the shared reference count, destroys the managed object if the count hits zero, and leaves the shared_ptr empty (null).
reset() releases ownership of the current resource:
- Decrement the strong reference count.
- If the count reaches zero, the managed object is destroyed (and, with the default deleter, its memory freed).
- The
shared_ptris set to a null/empty state (it now owns nothing).
If other shared_ptrs still reference the object, the object survives (count > 0); only this pointer’s ownership is dropped. reset() is the manual “let go” that destructors perform automatically. The interview answer: reset() drops ownership, decrementing the refcount and destroying the object if it hits zero, leaving the pointer null.
7. What is the alignment requirement of a class struct containing char a; double b; int c; on standard 64-bit x86 systems (assuming default packing)?
Answer: 8-byte alignment, with the total struct size padded to a multiple of 8 — 24 bytes total.
Struct alignment is driven by the member with the strictest alignment. double requires 8-byte alignment (on standard 64-bit x86), so the struct aligns to 8.
Layout with padding:
char a— offset 0 (1 byte), then 7 bytes padding.double b— offset 8 (8 bytes, ends at 16).int c— offset 16 (4 bytes, ends at 20).- Padding to struct alignment: total must be a multiple of 8 → pad 4 bytes → 24 bytes.
That’s why the members don’t pack tightly (the naive “13 bytes”) — the compiler inserts padding so every member is at its naturally aligned address. The interview answer: 8-byte alignment (from double), 24 bytes total after padding.
8. 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).
9. What problem is solved by using std::move_iterator?
Answer: It converts dereference results from lvalues into rvalue references, so algorithms and containers move elements instead of copying them.
std::move_iterator wraps an ordinary iterator. Dereferencing it yields T&& (an xvalue) instead of T&. That changes how consuming code behaves — std::copy or a range constructor will move the elements into the destination rather than copy:
std::vector<std::string> src = {"a", "b", "c"};
std::vector<std::string> dst(std::make_move_iterator(src.begin()),
std::make_move_iterator(src.end()));
// elements moved (stolen) from src, not copied
Great for efficiently transferring ownership of heap-owning elements from one container to another. The interview answer: dereferencing yields rvalue references, letting algorithms/constructors move elements instead of copying.
10. 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.
11. 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.
12. What is the purpose of std::span introduced in C++20?
Answer: A non-owning, contiguous view over a sequence of objects — raw arrays, std::vector, std::array — with no copying or allocation.
std::span<T> holds a pointer and a count into a contiguous range. It’s the “buffer view” for arrays the way string_view is the view for strings:
- Non-owning — it never allocates, never copies the underlying data, never frees anything.
- Any contiguous source — construct it from C arrays,
std::vector,std::array, or a pointer+size. - Bounds-aware API —
.size(),.front(),.back(), iteration, and.subspan()for slicing.
It’s ideal for function parameters that just want “give me a contiguous bunch of T’s” without caring about the container type. The interview answer: a non-owning pointer+length view over any contiguous sequence, enabling zero-copy, container-agnostic access.
13. 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&.
14. What design restriction applies to static class member functions in C++?
Answer: Static member functions cannot be const, volatile, or virtual, and have no this pointer — so they can’t access non-static members.
A static member function belongs to the class type, not to any instance:
- No
thispointer — there’s no instance to be “current.” Consequently it can only access static data members and call other static functions. - Can’t be
const/volatile— those qualifiers describe the implicitthisobject, which doesn’t exist. - Can’t be
virtual— virtual dispatch is an instance mechanism; a static function isn’t invoked through an object.
Use static methods for behavior that doesn’t depend on instance state: factories, utilities, and operations on static data. The interview answer: no this, no non-static member access, and they can’t be const, volatile, or virtual.
15. What causes dangling pointers when using std::vector?
Answer: Capacity growth reallocates the internal buffer, moving elements to new memory and freeing the old buffer — invalidating all existing pointers, references, and iterators.
When push_back/insert exceed the vector’s current capacity, the vector allocates a new, larger heap block, moves (or copies) the elements over, and frees the old block. Any pointer, reference, or iterator that pointed into the old block now points at freed memory — dangling.
This is the classic std::vector gotcha:
std::vector<int> v = {1, 2, 3};
int* p = &v[0];
v.push_back(4); // may reallocate → p dangles
Rules to remember: don’t hold raw pointers/references/iterators across operations that may grow the vector; use indices (which survive reallocation) or re-fetch iterators after mutation, and use reserve() up front to avoid reallocation. The interview answer: reallocation on growth moves elements to a new buffer and frees the old one, invalidating outstanding pointers/references/iterators.
16. What is the outcome of compiling and running a program containing a Data Race in C++?
Answer: Undefined Behavior — the program may corrupt memory, compute wrong results, or crash; there is no defined outcome.
A data race is: two threads access the same memory location concurrently, at least one access is a write, and there’s no synchronization between them (no mutex, atomic, or happens-before edge). In the C++ memory model this is explicitly undefined behavior:
- The compiler may reorder or eliminate the racing accesses based on the assumption of no race.
- The observed result can differ run-to-run, including corrupted values and crashes.
- Nothing “fixes” it automatically — the runtime won’t upgrade conflicting accesses to atomics.
The fixes are to introduce synchronization: mutexes, std::atomic, or std::thread join edges. The interview answer: a data race is UB — possible memory corruption, wrong results, or crashes, with no automatic fix.
17. What is the purpose of std::unreachable() introduced in C++23?
Answer: It tells the compiler that execution cannot reach this point, enabling aggressive optimizations.
std::unreachable() is an explicit UB hook: you assert “this code is genuinely unreachable.” It’s used after exhaustive dispatch where the compiler can’t prove totality — e.g., after a fully-covered switch, an infinite loop, or an exhaustive if/else chain:
switch (x) {
case 0: return a;
case 1: return b;
default: std::unreachable(); // logically can't happen
}
By asserting unreachability, the compiler can eliminate the default branch, dead-code the fall-through, and assume stronger invariants downstream — better codegen. (If you violate the assertion, that’s UB.) It’s a replacement for the old __builtin_unreachable() idiom. The interview answer: an explicit unreachable-assertion that lets the compiler remove branches and optimize assuming the point is never hit.
18. 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.
19. What is the effect of invoking std::abort() in a C++ program?
Answer: The program terminates immediately with SIGABRT, skipping destructors and cleanup for automatic, thread-local, and static objects.
std::abort() raises SIGABRT and terminates the process abnormally:
- No stack unwinding — local (automatic) objects’ destructors do not run.
- No static/thread-local cleanup — those destructors are skipped too.
- No
return 0— the process ends with an abnormal-termination signal.
Unlike std::exit() (which runs atexit handlers and static destructors), abort() is the hard kill — useful when the program is in a state too corrupt to trust any cleanup. The interview answer: immediate abnormal termination via SIGABRT, bypassing destructors and unwinding entirely.
20. 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.
Premium Content
Unlock Top 50 - Part 3 and all premium lessons with a subscription.
From ₹199.99/year — See plans