Menu

Earn Premium with Referrals

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

See how it works and start inviting friends.

Top 50 - Part 3
C++

Top 50 - Part 3

Practice the final 20 questions from a comprehensive set of 50 important C++ programming interview questions.

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 supportstd::condition_variable::wait() requires a unique_lock.
  • Moveable — you can return or store the lock.
  • Timeouts / try-locktry_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:

  1. The calling thread must already hold the mutex (via the unique_lock).
  2. 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.
  3. 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:

  • this and &rhs are the same object.
  • Deleting this’s internal buffers also destroys the very data you’re about to copy from rhs.
  • 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::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.

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:

  1. Decrement the strong reference count.
  2. If the count reaches zero, the managed object is destroyed (and, with the default deleter, its memory freed).
  3. The shared_ptr is 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; 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).

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)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&.

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 this pointer — 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 implicit this object, 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.
  • 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.

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 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.