C++ Interview Questions and Answers

Last updated:

Check out 60 of the most common C++ interview questions, then take an AI-powered practice interview

CData StructuresAlgorithmsSystem ProgrammingGame Development
60+
Questions
24
Basic
24
Intermediate
12
Advanced
Q1

Explain RAII. Why do interviewers treat it as the most important idiom in C++?

BasicFundamentals

Answer

RAII (Resource Acquisition Is Initialization) means a resource's lifetime is bound to an object's lifetime: the constructor acquires the resource (memory, file handle, socket, mutex lock) and the destructor releases it. Because C++ guarantees destructors run deterministically when an object goes out of scope, including during stack unwinding when an exception is thrown, RAII gives you leak-free, exception-safe cleanup without a garbage collector and without finally blocks. Every serious standard library type is an RAII wrapper: std::vector owns its heap buffer, std::unique_ptr owns a pointer, std::lock_guard owns a mutex lock, std::fstream owns a file descriptor.

Interviewers care because RAII is the answer to half of all C++ questions in disguise: why smart pointers exist, why you never write naked new/delete pairs, why early returns are safe in modern code, why lock_guard beats manual lock()/unlock(), and why the Rule of Zero works. A classic probe: what happens if a function throws between fopen and fclose? With raw calls, you leak the handle; with an RAII wrapper, the destructor closes it during unwinding.

Another probe: why must a destructor not throw? Because if unwinding is already in progress and a destructor throws a second exception, std::terminate is called. In production codebases at places like Adobe or NVIDIA, code review rejects any resource that is not RAII-managed, and the presence of a manual delete in new code is treated as a bug until proven otherwise.

#include <cstdio>
#include <stdexcept>

class File {
  std::FILE* f_;
public:
  explicit File(const char* path) : f_(std::fopen(path, "r")) {
    if (!f_) throw std::runtime_error("open failed");
  }
  ~File() { if (f_) std::fclose(f_); }  // runs even during unwinding
  File(const File&) = delete;            // no accidental double-close
  File& operator=(const File&) = delete;
  std::FILE* get() const { return f_; }
};

void parse(const char* path) {
  File f(path);          // acquired
  throw std::runtime_error("parse error");
}                        // f closed here automatically

Key Points

  • Constructor acquires, destructor releases, deterministically
  • Destructors run during stack unwinding, giving exception safety
  • std::vector, unique_ptr, lock_guard, fstream are all RAII types
  • Destructors must never throw: two active exceptions call std::terminate
💡 Pro Tip: When asked any resource-management question, frame your answer around RAII first and mention smart pointers second. Interviewers listen for the idiom, not the tool.
Q2

Where do objects live in a C++ program: stack, heap, and static storage? What decides it?

BasicMemory

Answer

C++ has storage durations, and where a variable is declared decides which one it gets. Automatic storage (the stack): local variables inside functions. Allocation is a stack-pointer bump, effectively free, and deallocation is automatic at scope exit, but the object dies when the frame dies and stack size is limited (commonly 1-8 MB per thread, so a local std::array<double, 10'000'000> will crash with a stack overflow).

Dynamic storage (the heap): anything created with new or, in practice, whatever std::vector, std::string, std::make_unique allocate internally. Allocation goes through the allocator (ptmalloc, jemalloc, tcmalloc), costs real time, can fragment, and the object lives until explicitly freed. Static storage: globals, static locals, and static members; they live for the whole program, are zero-initialized before dynamic initialization, and function-local statics are initialized on first use in a thread-safe way since C++11 (the compiler emits a guard variable, which is why the Meyers singleton works).

Interviewers probe two things. First, dangling: returning a pointer or reference to a stack local is undefined behavior because the frame is reused. Second, cost intuition: a hot loop that constructs a std::string or vector per iteration is allocating on the heap every pass, and hoisting it out or reserving capacity is often the first optimization in a code review. In low-latency shops like Graviton or Tower, heap allocation on the hot path is essentially banned, everything is preallocated or arena-allocated.

#include <memory>
#include <vector>

int g_counter = 0;                 // static storage, whole program

int* dangling() {
  int x = 42;                      // automatic (stack)
  return &x;                       // UB: frame is gone after return
}

std::unique_ptr<int> fine() {
  return std::make_unique<int>(42); // heap, ownership transferred out
}

void hotLoop(const std::vector<int>& in) {
  std::vector<int> scratch;
  scratch.reserve(in.size());      // one allocation, not O(log n) regrows
  for (int v : in) scratch.push_back(v * 2);
}

Key Points

  • Automatic = stack, scope-bound, near-zero cost, limited size
  • Dynamic = heap via allocator, explicit lifetime, real cost
  • Static locals are thread-safe initialized since C++11
  • Returning address of a stack local is classic UB
Q3

How does std::unique_ptr work, and why is std::make_unique preferred over raw new?

BasicSmart Pointers

Answer

std::unique_ptr<T> is a zero-overhead RAII wrapper expressing exclusive ownership: exactly one unique_ptr owns the object, the destructor calls delete (or a custom deleter), and copying is deleted at compile time, only moves are allowed. With the default deleter, sizeof(unique_ptr<T>) equals sizeof(T*), and the indirection compiles away entirely, so there is no excuse for raw owning pointers. Transfer of ownership is explicit: you must write std::move(ptr), which makes ownership handoffs visible in code review. std::make_unique<T>(args...) is preferred over unique_ptr<T>(new T(args...)) for three reasons.

First, exception safety: before C++17's stricter evaluation order, an expression like f(unique_ptr<T>(new T), g()) could leak if g() threw between the new and the unique_ptr constructor; make_unique closes that window and remains the habit. Second, it removes the raw new from the code entirely, so a grep for 'new' finds only suspicious code. Third, it avoids repeating the type name.

Things interviewers probe: unique_ptr with a custom deleter (the deleter is part of the type, unique_ptr<FILE, decltype(&fclose)> is fatter than a plain pointer); unique_ptr<T[]> for arrays calls delete[]; release() gives up ownership without deleting (a common leak source when misunderstood); get() hands out a non-owning view that must not outlive the owner. In codebases at Samsung R&D or Qualcomm, unique_ptr is the default for any heap object, and shared_ptr requires justification.

#include <memory>
#include <cstdio>

struct Engine { void start() {} };

void demo() {
  auto e = std::make_unique<Engine>();   // preferred creation
  e->start();

  std::unique_ptr<Engine> owner2 = std::move(e); // explicit transfer
  // e is now null; using *e would be UB

  // custom deleter: fclose the FILE* automatically
  std::unique_ptr<std::FILE, int(*)(std::FILE*)>
      f(std::fopen("data.bin", "rb"), &std::fclose);

  auto buf = std::make_unique<char[]>(4096); // array form -> delete[]
}

Key Points

  • Exclusive ownership, move-only, zero overhead with default deleter
  • make_unique: exception-safe, no visible new, no type repetition
  • release() abandons ownership without deleting; get() is non-owning
  • unique_ptr<T[]> uses delete[]; custom deleters are part of the type
Q4

How does std::shared_ptr reference counting actually work, and what lives in the control block?

BasicSmart Pointers

Answer

std::shared_ptr<T> implements shared ownership through a separately allocated control block containing two atomic counters: the strong count (number of shared_ptrs keeping the object alive) and the weak count (number of weak_ptrs keeping the control block alive), plus the deleter and allocator. Copying a shared_ptr atomically increments the strong count; destruction decrements it, and when it hits zero the object is destroyed. The control block itself is freed only when the weak count also reaches zero.

This design is why std::make_shared matters: shared_ptr<T>(new T) performs two allocations (object, then control block), while make_shared performs one combined allocation, which is faster and more cache-friendly. The trade-off interviewers love: with make_shared, the memory for the object cannot be released until the last weak_ptr dies, because object and control block share one allocation. Costs you must be able to state: every copy is an atomic RMW operation, which contends across cores in hot loops, so pass shared_ptr by const reference or pass T* / T& when not sharing ownership. sizeof(shared_ptr) is two pointers (object pointer plus control block pointer).

Cycles are the classic leak: two objects holding shared_ptrs to each other never reach zero; break the cycle with weak_ptr, and convert back with lock(), which returns an empty shared_ptr if the object already died. Also know enable_shared_from_this: calling shared_from_this() on an object not owned by any shared_ptr throws std::bad_weak_ptr.

#include <memory>

struct Node {
  std::shared_ptr<Node> next;
  std::weak_ptr<Node> prev;   // weak breaks the cycle
};

void demo() {
  auto a = std::make_shared<Node>(); // ONE allocation (obj + control block)
  auto b = std::make_shared<Node>();
  a->next = b;                        // strong: a keeps b alive
  b->prev = a;                        // weak: no cycle, no leak

  if (std::shared_ptr<Node> p = b->prev.lock()) {
    // safe: object still alive, we hold a temporary strong ref
  }
}
// counts: use_count() is advisory only in multithreaded code

Key Points

  • Control block holds atomic strong/weak counts + deleter
  • make_shared: one allocation instead of two
  • Atomic count updates cost real time; do not copy in hot loops
  • Cycles leak; weak_ptr::lock() is the safe observation pattern
💡 Pro Tip: If you reach for shared_ptr in an interview design question, expect the follow-up 'who are the co-owners?'. If you cannot name two, the answer was unique_ptr.
Q5

When should you use a reference instead of a pointer in C++, and what can references not do?

BasicFundamentals

Answer

A reference is an alias to an existing object: it must be bound at initialization, can never be null, and can never be reseated to refer to something else. A pointer is an independent object holding an address: it can be null, reassigned, stored in containers, and subjected to arithmetic. The modern guideline: use references for parameters and returns when the referent is mandatory (a function taking const std::string& promises the caller must pass a real string), and use pointers (or std::optional, or smart pointers) when absence is a legal state or ownership is being expressed.

What references cannot do: no arrays of references, no reference members without crippling assignability (a class with a reference member gets its copy assignment implicitly deleted), no rebinding inside loops. Gotchas interviewers fish for: a const reference extends the lifetime of a temporary it binds to (const std::string& s = makeString(); is safe), but that extension does not propagate through function returns, so returning a reference to a local is UB. Range-based for with auto& mutates in place, auto makes copies, and const auto& reads without copying; writing for (auto x : vec) on a vector of std::string in a hot loop copies every element, a favorite code-review catch.

Dangling references are the other big one: holding a reference to a vector element and then push_back-ing can invalidate it silently. In interviews, saying 'reference when it cannot be null, pointer when it can' followed by the lifetime caveats is the complete answer.

Key Points

  • References: non-null, bound once, never reseated
  • Pointers: nullable, reassignable, storable, arithmetic-capable
  • const& extends temporary lifetime at local scope only
  • Reference members delete copy assignment; prefer pointers or reference_wrapper
Q6

What does const correctness mean in practice: const member functions, const references, and mutable?

BasicFundamentals

Answer

Const correctness is the discipline of marking everything that does not mutate as const, so the compiler enforces your design intent. A const member function (int size() const) promises not to modify the object's logical state; inside it, this is const T*, so only other const members can be called and members cannot be assigned. Overloading on constness is standard practice: std::vector::operator[] has a const overload returning const T& and a non-const one returning T&.

Const references (const T&) are the default way to pass anything bigger than a couple of machine words: no copy, no mutation, and they bind to temporaries. The mutable keyword marks members that may change even inside const member functions, legitimate for caches, memoization, and mutexes (a const getter still needs to lock its std::mutex, so the mutex is declared mutable). What interviewers probe: the difference between const T* p (pointee is const), T* const p (pointer is const), and const T* const p (both); that const_cast-ing away const and then writing through it is UB if the object was originally declared const; and that const on a member function is part of the signature, so a const object simply cannot call non-const members.

A practical production note: const-correct interfaces are also thread-relevant. The standard library assumes const member functions are safe to call concurrently, which is exactly why a mutable mutex inside a const-qualified read path is idiomatic rather than a hack. Breaking that assumption (a const function that mutates without synchronization) creates data races that TSan will flag.

#include <mutex>
#include <string>

class Config {
  mutable std::mutex m_;          // lockable inside const functions
  mutable std::string cached_;    // memoization cache
  std::string raw_;
public:
  const std::string& value() const {   // const: safe concurrent reads
    std::lock_guard<std::mutex> lk(m_);
    if (cached_.empty()) cached_ = expand(raw_);
    return cached_;
  }
  void set(std::string v) { raw_ = std::move(v); }  // non-const
private:
  static std::string expand(const std::string& s) { return s; }
};

Key Points

  • const member functions make this a const pointer; part of the signature
  • Read the declaration right-to-left: const T* vs T* const
  • mutable is for caches and mutexes inside const paths
  • Standard library treats const as concurrently-safe; honor that
Q7

Explain the Rule of Three, Rule of Five, and Rule of Zero. Which one should modern code follow?

BasicObject Model

Answer

These rules govern the special member functions: destructor, copy constructor, copy assignment, move constructor, move assignment. Rule of Three (pre-C++11): if a class needs a user-defined destructor, copy constructor, or copy assignment, it almost certainly needs all three, because needing any one implies the class manages a resource that default memberwise copying would mishandle (double-free being the classic outcome). Rule of Five (C++11): the same logic extended to the move constructor and move assignment; if you declare any of the five, the compiler suppresses or deprecates generation of others.

The precise trap interviewers test: declaring a destructor (even = default) prevents implicit generation of move operations, so your type silently degrades to copying everywhere, a real performance bug that never fails a test. Rule of Zero: the modern answer. Classes should own resources only through members that already manage themselves (std::string, std::vector, std::unique_ptr), and declare none of the five; the compiler-generated operations are then automatically correct.

Production guidance: write Rule-of-Zero classes by default; when you genuinely manage a raw resource (a file descriptor, a mapped region, a GPU buffer), write one small RAII wrapper that follows the Rule of Five with correct move semantics and noexcept annotations, then compose it. Also know =default and =delete: =default asks for the compiler version explicitly (and a defaulted destructor in the .cpp file is the standard trick for unique_ptr to an incomplete type, the PIMPL idiom), while =delete removes an operation, e.g. making a class non-copyable.

#include <unistd.h>
#include <utility>

// Rule of Five: one tiny wrapper owns the raw resource
class Fd {
  int fd_ = -1;
public:
  explicit Fd(int fd) : fd_(fd) {}
  ~Fd() { if (fd_ >= 0) ::close(fd_); }
  Fd(const Fd&) = delete;
  Fd& operator=(const Fd&) = delete;
  Fd(Fd&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
  Fd& operator=(Fd&& o) noexcept {
    if (this != &o) { this->~Fd(); fd_ = std::exchange(o.fd_, -1); }
    return *this;
  }
};

// Rule of Zero: composes managed members, declares nothing
struct Connection {
  Fd socket;
  std::string peer;   // all five operations generated correctly
};

Key Points

  • Declaring a destructor suppresses implicit move generation
  • Rule of Zero: own resources only via self-managing members
  • Write one Rule-of-Five RAII wrapper per raw resource, then compose
  • std::exchange is the idiomatic move-constructor pattern
💡 Pro Tip: If an interviewer shows you a class with a destructor and asks 'what is wrong', check whether it still moves. Silent copy-instead-of-move is the intended answer.
Q8

How do virtual functions work under the hood, and why must a polymorphic base class have a virtual destructor?

BasicObject Model

Answer

Every class with at least one virtual function gets a vtable: a per-class static array of function pointers, one entry per virtual function, emitted by the compiler. Every object of such a class carries a hidden vptr (one pointer, typically the first 8 bytes of the object) pointing at its class's vtable. A virtual call compiles to: load vptr from the object, index into the vtable, call through the function pointer.

That is two dependent loads plus an indirect call, which matters not because of the instructions but because the indirect branch can miss prediction and, more importantly, cannot be inlined, blocking downstream optimization. The constructor sets the vptr, which explains a classic gotcha: calling a virtual function inside a constructor dispatches to the current class's version, not the derived override, because the derived part does not exist yet. The virtual destructor rule: if you delete a Derived object through a Base*, and Base's destructor is not virtual, the behavior is undefined; typically only ~Base runs, leaking everything Derived owns, and with different base/derived addresses (multiple inheritance) the wrong address hits the allocator.

Declaring virtual ~Base() makes the delete dispatch through the vtable so ~Derived runs first, then ~Base. The corollary: if a class is not meant to be a polymorphic base, do not give it virtual functions at all (std::string has no virtual destructor by design; inheriting from it publicly is a bug). Interviewers extend this into: what does final enable (devirtualization), what is a pure virtual function (= 0, makes the class abstract), and why sizeof grows by 8 when you add the first virtual function.

#include <iostream>
#include <memory>

struct Base {
  Base() { hook(); }        // calls Base::hook, NOT Derived::hook
  virtual ~Base() { std::cout << "~Base\n"; }
  virtual void hook() { std::cout << "Base::hook\n"; }
};

struct Derived : Base {
  ~Derived() override { std::cout << "~Derived\n"; }
  void hook() override { std::cout << "Derived::hook\n"; }
};

int main() {
  std::unique_ptr<Base> p = std::make_unique<Derived>();
  p->hook();   // vptr -> Derived vtable -> Derived::hook
}                // prints ~Derived then ~Base: correct because virtual

Key Points

  • vtable per class, vptr per object, set by the constructor
  • Virtual calls block inlining; that is the real cost
  • Deleting Derived via Base* without virtual dtor is UB
  • Virtual calls in constructors dispatch to the class under construction
Q9

What is the difference between overloading and overriding, and what do the override and final keywords change?

BasicObject Model

Answer

Overloading is compile-time: multiple functions share a name in the same scope but differ in parameter types, and the compiler picks one via overload resolution based on the static types of the arguments. Overriding is runtime: a derived class provides its own implementation of a base class virtual function with the same signature, and calls through a base pointer or reference dispatch to it via the vtable. The signature-matching requirement is where bugs hide: if the derived function's signature differs even slightly (a missing const qualifier, int vs long, a different reference qualifier), it silently declares a brand-new function that hides the base one instead of overriding it, and polymorphic calls keep hitting the base version.

The override keyword (C++11) turns that silent bug into a compile error: it tells the compiler 'this must override something', and mismatches fail the build. Every override in a modern codebase should carry the keyword; most style guides (Google's included) mandate it. final serves two purposes: on a function (void f() final) it forbids further overriding down the hierarchy, and on a class (class Widget final) it forbids inheriting at all. Beyond design intent, final feeds the optimizer: a call through a pointer whose static type is a final class (or to a final function) can be devirtualized into a direct, inlinable call, because the compiler proves no other override can exist.

Related trap interviewers add: name hiding, where any derived function named f hides all base overloads of f, fixed with 'using Base::f;'. Default arguments are also resolved statically, so a base call with the derived override active still uses the base's default argument values, a genuinely nasty quiz favorite.

struct Base {
  virtual void log(int level) const {}
  virtual void draw() {}
  void util(int) {}
  void util(double) {}
};

struct Derived : Base {
  // void log(int level) {}        // missing const: would HIDE, not override
  void log(int level) const override {}  // override catches mismatches
  void draw() final {}                   // nobody may override further

  using Base::util;               // un-hide the base overload set
  void util(const char*) {}
};

struct Widget final : Derived {}; // no class may inherit Widget

Key Points

  • Overloading resolves at compile time; overriding via vtable at runtime
  • Signature mismatch silently hides instead of overrides; override prevents it
  • final enables devirtualization, a real optimizer win
  • Default arguments bind statically even on virtual calls
Q10

How does std::vector grow, and which operations invalidate its iterators, pointers, and references?

BasicContainers

Answer

std::vector stores elements in one contiguous heap buffer and tracks size (elements in use) and capacity (allocated slots). push_back when size == capacity triggers reallocation: a new larger buffer is allocated, elements are moved (or copied if the move constructor is not noexcept, see the move-if-noexcept rule) into it, and the old buffer is freed. Growth factor is implementation-defined: libstdc++ and libc++ double, MSVC grows by 1.5x; either way push_back is amortized O(1). Reallocation invalidates every iterator, pointer, and reference into the vector, the single most common cause of subtle vector bugs.

Even without reallocation, insert and erase invalidate iterators at and after the modification point. The canonical crash: holding a reference to v[0], calling v.push_back(...), then using the reference; if a reallocation happened, that reference points into freed memory, and AddressSanitizer reports heap-use-after-free. A sneakier variant: v.push_back(v[0]) can be UB if the implementation reallocates before reading the argument (the standard requires implementations to handle this, but the pattern with insert-from-self on other containers is a real trap worth mentioning).

Practical control: reserve(n) preallocates capacity so subsequent push_backs neither allocate nor invalidate, mandatory when the final size is known; shrink_to_fit is a non-binding request to release excess capacity; clear() destroys elements but keeps capacity. Know the difference between emplace_back (constructs in place from forwarded args) and push_back (copies or moves a finished object); for a vector<std::string>, emplace_back("abc") constructs directly in the buffer. Also state data() for C-API interop and that erasing from the middle is O(n) because everything after shifts left.

#include <vector>
#include <string>
#include <cassert>

void demo() {
  std::vector<std::string> v;
  v.reserve(3);                 // capacity 3, no reallocation below
  v.emplace_back("a");
  const std::string* p = &v[0];
  v.emplace_back("b");        // within capacity: p still valid
  assert(p == &v[0]);

  v.emplace_back("c");
  v.emplace_back("d");        // capacity exceeded: REALLOCATION
  // *p is now a dangling read: heap-use-after-free under ASan

  // capacity survives clear():
  v.clear();
  assert(v.capacity() >= 4);
}

Key Points

  • Reallocation moves elements and invalidates ALL iterators/pointers/refs
  • Growth: 2x on GCC/Clang stdlibs, 1.5x on MSVC; amortized O(1) push_back
  • reserve() when size is known; it also pins references
  • Moves used on regrow only if the move ctor is noexcept
💡 Pro Tip: Any time you say 'vector' in an interview, be ready for 'what invalidates iterators?'. It is the most reliable follow-up in all of C++ interviewing.
Q11

How is std::string implemented, and what is the small string optimization (SSO)?

BasicContainers

Answer

std::string is a dynamic character buffer with the same size/capacity mechanics as vector, plus the small string optimization: short strings are stored inline inside the string object itself, with no heap allocation at all. The string object is typically 24-32 bytes; libstdc++ stores up to 15 characters inline, libc++ up to 22. Only when the content exceeds the inline buffer does the string allocate on the heap.

Consequences you should articulate: constructing or copying short strings is cheap and allocation-free, so a map keyed by short strings performs far better than naive intuition suggests; moving a string is not always 'free', because an SSO string's characters must actually be copied (there is no heap pointer to steal), though it is still O(1) with a small constant. SSO also explains why data() pointers differ after a move and why interoperating code must not cache c_str() across mutations: any operation that reallocates (append past capacity, resize) invalidates pointers into the buffer. Historical context that still comes up: before C++11, GCC's std::string was copy-on-write (COW); C++11 outlawed COW because operator[] must not invalidate other strings' iterators under the new rules, which is what forced the GCC 5 dual-ABI migration (_GLIBCXX_USE_CXX11_ABI).

Modern additions worth naming: std::string_view (C++17) for non-owning views, starts_with/ends_with (C++20), and contains (C++23). A production habit interviewers respect: reserve() before building strings in loops, use += or append rather than s = s + t (which creates a temporary), and pass const std::string& or std::string_view, never std::string by value, unless you intend to store it (then take by value and std::move).

#include <string>
#include <iostream>

int main() {
  std::string s = "short";          // SSO: no heap allocation
  const char* p1 = s.data();

  s = std::string(40, 'x');            // exceeds SSO buffer: heap
  const char* p2 = s.data();           // different storage now

  std::string t = std::move(s);        // steals the heap buffer, O(1)

  std::string out;
  out.reserve(256);                    // build-in-loop pattern
  for (int i = 0; i < 10; ++i) out += "chunk,";

  std::cout << (t.contains("xxx"))   // C++23 contains()
            << out.starts_with("chunk"); // C++20
}

Key Points

  • SSO: ~15 chars inline (libstdc++), ~22 (libc++), zero allocation
  • Moving an SSO string copies chars; still O(1), not pointer theft
  • COW strings are illegal since C++11; caused GCC's dual ABI
  • c_str()/data() invalidate on reallocation, same as vector
Q12

std::map vs std::unordered_map: how do they differ in implementation, complexity, and iterator guarantees?

BasicContainers

Answer

std::map is a node-based, sorted associative container, in practice always a red-black tree: O(log n) find/insert/erase, elements ordered by the comparator, and stable iteration order. std::unordered_map is a hash table with separate chaining (buckets of nodes): average O(1) find/insert/erase, worst case O(n) when many keys collide, no ordering whatsoever. Choose map when you need ordered traversal, range queries (lower_bound/upper_bound), or a deterministic iteration order; choose unordered_map for pure point lookups, which is most real code. The details that separate candidates: both are node-based, so both give pointer/reference stability, an element's address never changes while it is in the container.

Iterator invalidation differs though: unordered_map rehashes when load_factor exceeds max_load_factor (default 1.0), invalidating all iterators but not pointers/references to elements; map iterators are only invalidated for the erased element. operator[] default-constructs a value if the key is missing, a classic read-that-mutates bug in code that meant to query; use find(), at() (throws std::out_of_range), or C++20 contains() for pure lookups. Performance realities: hash tables beat trees by a large factor on lookups, but std::unordered_map is famously cache-unfriendly (a heap node per element, pointer chasing per probe); high-performance code at HFT firms replaces it with open-addressing tables like absl::flat_hash_map. For custom key types, map needs operator< (or a comparator with is_transparent for heterogeneous lookup, letting you find with string_view against string keys without constructing a string); unordered_map needs a std::hash specialization plus operator==. Also mention try_emplace and insert_or_assign (C++17) for cleaner insert semantics.

#include <map>
#include <unordered_map>
#include <string>
#include <string_view>

void demo() {
  std::map<std::string, int, std::less<>> m;   // less<>: transparent
  m["alpha"] = 1;
  auto it = m.find(std::string_view("alpha")); // no temp string built

  std::unordered_map<std::string, int> h;
  h.reserve(1000);              // pre-size buckets, avoid rehashes
  h.try_emplace("key", 42);    // no overwrite if present (C++17)

  if (h.contains("key")) {}    // C++20; no accidental insert
  // h["typo"] would INSERT {"typo", 0}: the classic operator[] trap
}

Key Points

  • map: red-black tree, O(log n), ordered, range queries
  • unordered_map: hash buckets, avg O(1), rehash invalidates iterators
  • operator[] inserts on miss; use find/at/contains to query
  • Node-based means pointer stability but poor cache behavior
Q13

Pass by value, pass by reference, pass by const reference: how do you decide for function parameters?

BasicFundamentals

Answer

The default decision tree: for cheap-to-copy types (int, double, pointers, string_view, span, small structs up to roughly two machine words), pass by value; copying is as cheap as the indirection and enables register passing. For anything larger that you only read, pass by const reference (const std::vector<T>&): no copy, cannot mutate, binds to temporaries. Pass by non-const reference only when the function's purpose is to mutate the caller's object, and prefer making that visible in the API name.

The modern refinement is the sink-parameter pattern: when the function intends to keep a copy (a setter, a constructor storing into a member), take the parameter by value and std::move it into place. This is optimal for both call styles: callers passing an lvalue pay one copy plus one cheap move; callers passing an rvalue (temporary or moved) pay just two moves and zero copies, because the compiler constructs the argument directly. The old alternative, overloading on const T& and T&&, doubles the code for marginal gain and is now reserved for hot paths.

Special cases interviewers raise: never take std::unique_ptr by const reference (it expresses nothing; take T* or T& to observe, unique_ptr by value to transfer ownership); take std::string_view instead of const std::string& in APIs that only read characters, so string literals and substrings avoid constructing a temporary string; and avoid returning const references to locals. Output parameters via reference are increasingly replaced by return values, since guaranteed copy elision (C++17) makes returning even large objects free, and std::optional/std::expected express failure better than bool-plus-out-param.

#include <string>
#include <string_view>
#include <utility>

class User {
  std::string name_;
public:
  // sink parameter: by value + move, optimal for lvalues AND rvalues
  void setName(std::string name) { name_ = std::move(name); }

  // read-only view: accepts string, literal, substring; no allocation
  bool matches(std::string_view needle) const {
    return name_.find(needle) != std::string::npos;
  }
};

void caller() {
  User u;
  std::string n = "Saksham";
  u.setName(n);              // one copy, one move
  u.setName(std::move(n));   // two moves, zero copies
  u.setName("literal");    // constructs in place, moves
}

Key Points

  • Small trivially-copyable types: by value (registers)
  • Read-only large types: const&; read-only strings: string_view
  • Storing a copy: take by value, std::move into the member
  • C++17 guaranteed elision makes return-by-value free
Q14

What do auto, range-based for, and structured bindings change about everyday C++, and where does auto backfire?

BasicModern C++

Answer

auto deduces a variable's type from its initializer using template deduction rules, which means it drops top-level const and references unless you ask for them: auto x = f() copies, auto& x binds a mutable reference, const auto& x binds a read-only reference (and extends temporary lifetime), auto&& x is a forwarding reference that binds to anything. The classic backfire: for (auto item : container) silently copies every element; on a vector<std::string> that is an allocation per iteration. The second backfire is proxy types: auto flag = bitset[3]; or auto x = vec_of_bool[i]; deduces the proxy reference type, not bool, and the proxy may dangle after the container mutates; Eigen and other expression-template libraries have the same hazard, where auto captures an unevaluated expression referencing temporaries.

Range-based for desugars to begin()/end() iteration; the crucial rule is that the range expression is evaluated once, so for (auto x : getTemporaryVector()) is safe (lifetime extended), but for (auto x : getTemp().field) dangled until C++23 fixed lifetime extension for the full range expression. Structured bindings (C++17) unpack tuples, pairs, arrays, and aggregate structs: auto [key, value] : myMap gives named access instead of it->first/it->second, and auto [it, inserted] = map.insert(...) names the pair members. They bind by value or reference following the same auto rules (auto& [k, v] to mutate values in a map loop; note k is const in a map regardless). Since C++20 you can add init-statements in range-for, and C++23's std::views::enumerate gives index-plus-element iteration without a manual counter.

#include <map>
#include <string>
#include <vector>

void demo(std::map<std::string, int>& scores,
          const std::vector<std::string>& names) {
  for (const auto& n : names) {}        // no copies
  // for (auto n : names) {}            // copies every string: avoid

  for (auto& [name, score] : scores) {  // structured bindings
    score += 10;                        // name is const key, score mutable
  }

  auto [it, inserted] = scores.insert({"ravi", 50});
  if (!inserted) it->second = 50;

  std::vector<bool> flags{true, false};
  auto f = flags[0];        // proxy object, NOT bool: use bool f = flags[0]
}

Key Points

  • auto drops const/& like template deduction; auto& / const auto& restore
  • for (auto x : c) copies elements: the top code-review catch
  • Proxy types (vector<bool>, Eigen) make auto dangerous
  • Structured bindings unpack pairs/tuples/aggregates since C++17
Q15

Why does enum class exist when plain enum already worked, and when is a plain enum still correct?

BasicType System

Answer

Plain (unscoped) enums have three problems that enum class (C++11, scoped enums) fixes. First, scope pollution: unscoped enumerators inject into the enclosing scope, so enum Color { Red } and enum Fruit { Red } in the same namespace collide at compile time; scoped enumerators live inside the enum (Color::Red) and never collide. Second, implicit conversion: an unscoped enum converts silently to int, so accidents like passing a Color where a Size was expected, or comparing enums of different types, compile without complaint; enum class values do not convert implicitly to anything, forcing an explicit static_cast<int> when you genuinely need the number.

Third, unspecified underlying type: the compiler picked any integer type big enough for unscoped enums, which made forward declaration impossible and binary layout unpredictable; both enum kinds can now specify the underlying type (enum class ErrorCode : std::uint8_t) which enables forward declarations and stable serialization. Interview follow-ups worth knowing: casting an out-of-range value into an enum with a fixed underlying type is well-defined (value preserved); without a fixed type it can be UB. std::to_underlying (C++23) replaces the verbose static_cast<std::underlying_type_t<E>>(e). Scoped enums do not define operators, so flag-style bitmask enums need explicit operator| overloads, a small idiom worth writing once per codebase.

When is a plain enum still right? Mostly when you want implicit conversion on purpose: enumerating array indices, or the classic enum { BufferSize = 4096 } trick (now better served by constexpr). Modern style guides default to enum class everywhere, with a fixed underlying type on anything crossing a serialization or ABI boundary.

#include <cstdint>
#include <utility>

enum class Status : std::uint8_t { Ok = 0, Retry = 1, Fatal = 2 };
enum class Perm : unsigned { Read = 1, Write = 2, Exec = 4 };

// bitmask idiom: scoped enums need explicit operators
constexpr Perm operator|(Perm a, Perm b) {
  return static_cast<Perm>(std::to_underlying(a) | std::to_underlying(b));
}
constexpr bool has(Perm set, Perm bit) {
  return (std::to_underlying(set) & std::to_underlying(bit)) != 0;
}

void demo() {
  Status s = Status::Retry;
  // int i = s;                 // error: no implicit conversion
  auto raw = std::to_underlying(s);  // C++23, explicit and typed
  Perm p = Perm::Read | Perm::Write;
  (void)raw; (void)has(p, Perm::Exec);
}

Key Points

  • Scoped: no scope pollution, no implicit int conversion
  • Fix the underlying type for serialization and forward declarations
  • std::to_underlying (C++23) for explicit numeric access
  • Bitmask enums need hand-written operator| overloads
Q16

Why was nullptr introduced when NULL and 0 already existed?

BasicType System

Answer

Because NULL was never a pointer. In C++, NULL is a macro defined as the integer literal 0 (or 0L), so it participates in overload resolution as an integer. The canonical demonstration: given void f(int) and void f(char*), the call f(NULL) selects f(int), which is almost never what the author meant, and on some platforms it is ambiguous and fails to compile.

Templates made it worse: forwarding NULL through a template deduces the parameter as an integer type, so the pointer-ness is lost before it reaches the target function; you cannot write a wrapper that forwards a null pointer with NULL. C++11's nullptr is a keyword literal of its own distinct type, std::nullptr_t, which implicitly converts to any pointer type and to pointer-to-member types, but not to integers. That fixes overload resolution (f(nullptr) picks f(char*)), makes intent explicit, and lets you overload specifically on std::nullptr_t when a function should react differently to a literal null.

Deduction works correctly too: auto p = nullptr gives std::nullptr_t, and forwarding it through templates preserves its nature. Follow-up points that earn credit: comparing pointers to nullptr is the idiomatic null check (if (p != nullptr), or just if (p)); delete on a null pointer is well-defined and does nothing, so guarding delete with a null check is noise; and smart pointers interoperate cleanly, unique_ptr compares to and resets from nullptr. The remaining legitimate use of 0 in pointer context is none; modern compilers with -Wzero-as-null-pointer-constant will warn on it, and clang-tidy's modernize-use-nullptr rewrites legacy code automatically, a mechanical migration you can mention as part of upgrading old codebases.

#include <cstddef>
#include <iostream>

void f(int)   { std::cout << "f(int)\n"; }
void f(char*) { std::cout << "f(char*)\n"; }

template <typename T>
void forward_to_f(T v) { f(v); }

int main() {
  f(0);            // f(int)
  f(NULL);         // f(int)!  NULL is an integer literal
  f(nullptr);      // f(char*): what was actually meant

  forward_to_f(nullptr);  // deduces std::nullptr_t, still calls f(char*)
  // forward_to_f(NULL);  // deduces long: calls f(int), pointer-ness lost
}

Key Points

  • NULL is integer 0; it picks integer overloads and breaks templates
  • nullptr has type std::nullptr_t, converts only to pointers
  • Deduction preserves nullptr_t through forwarding
  • clang-tidy modernize-use-nullptr mechanizes migration
Q17

Why does C++ need header files at all, and what do include guards and #pragma once actually prevent?

BasicCompilation Model

Answer

C++ compiles each .cpp file (translation unit) in complete isolation. When a.cpp calls a function defined in b.cpp, the compiler needs the declaration (name, parameters, return type) to type-check the call and generate the right calling code; the actual definition is resolved later by the linker. Headers are the mechanism for sharing those declarations: #include is textual paste, the preprocessor literally splices the header's contents into the including file.

That textual nature creates the double-inclusion problem: if header X includes Y, and your .cpp includes both X and Y, Y's contents appear twice, and any class definition in Y becomes an illegal redefinition within the same translation unit. Include guards (#ifndef MYLIB_FOO_H / #define MYLIB_FOO_H / #endif) make the second paste a no-op; #pragma once does the same non-standardly but is supported by every compiler that matters and avoids guard-name collisions. Important precision: guards prevent redefinition within one translation unit; the One Definition Rule across translation units is a separate concern, which is why headers contain declarations, inline functions, templates, and inline variables, but not plain function definitions.

Practices worth naming in interviews: forward declarations (class Widget;) instead of includes when a header only uses pointers or references to a type, cutting compile times and dependency fan-out; include-what-you-use as the discipline (and IWYU as the tool); PIMPL to remove private members from the header entirely. C++20 modules (import foo;) are the designed replacement for this whole textual model, compiling interfaces once into a binary representation, but adoption is still partial in 2026, so header discipline remains a day-one skill.

// widget.h
#pragma once                    // or classic #ifndef guards
#include <memory>

class Engine;                   // forward declaration: no #include needed
                                 // (we only use Engine*)
class Widget {
public:
  Widget();
  ~Widget();                    // defined in .cpp: Engine complete there
  void run();
private:
  std::unique_ptr<Engine> engine_;  // pointer-to-incomplete is fine
};

// widget.cpp
// #include "widget.h"
// #include "engine.h"        // full definition only where needed
// Widget::~Widget() = default; // must live here, Engine is complete

Key Points

  • #include is textual paste; each TU compiles independently
  • Guards stop double-paste within one TU; ODR is a separate rule
  • Forward-declare when only pointers/references are used
  • C++20 modules replace the model but adoption is incomplete
Q18

Walk through what happens between source code and executable: preprocessing, compilation, linking, and the One Definition Rule.

BasicCompilation Model

Answer

Four stages. Preprocessing: #include pastes headers, #define macros expand, #if branches resolve; the output is one giant translation unit of pure C++ (view it with g++ -E). Compilation: the compiler parses, type-checks, applies optimizations, and emits assembly for one TU at a time (g++ -S to see it), producing an object file (.o) via the assembler (g++ -c).

The object file contains machine code plus a symbol table: symbols it defines and symbols it merely references (view with nm or objdump). Linking: the linker (ld, gold, lld, mold) merges object files and libraries, resolves every undefined reference to exactly one definition, and lays out the final executable. The two errors every C++ developer must be able to diagnose instantly: 'undefined reference to X' means no linked object defines X, commonly a missing .cpp in the build, a missing -l library, wrong library order with static archives (dependencies must come after their users on the command line), or a template defined in a .cpp instead of a header. 'multiple definition of X' means two TUs both define X, usually a function defined in a header without inline.

That is the One Definition Rule: every non-inline function or variable must be defined exactly once across the whole program; inline functions, templates, and inline variables (C++17) may be defined in every TU provided the definitions are token-identical, and the linker deduplicates them. The nastiest ODR failure is silent: two TUs see different definitions of the same class (different macros, different versions of a header); the linker picks one arbitrarily, and you get miscompiled behavior with no diagnostic. Also worth naming: name mangling encodes C++ signatures into symbol names (c++filt demangles), and extern "C" disables mangling for C interop.

# The pipeline, made visible:
g++ -E main.cpp -o main.ii     # 1. preprocess (headers pasted)
g++ -S main.ii -o main.s       # 2. compile to assembly
g++ -c main.s -o main.o        # 3. assemble to object file
g++ main.o util.o -lssl -o app # 4. link (order matters for static libs)

nm -C main.o                   # symbols, demangled (-C)
# 'U _ZN4Util3runEv' = undefined reference, must come from another TU

# Classic failures:
# undefined reference to Util::run()   -> util.o not linked / template in .cpp
# multiple definition of helper()      -> header definition missing 'inline'

Key Points

  • Preprocess -> compile per-TU -> assemble -> link
  • undefined reference: missing definition/library/order problem
  • multiple definition: header definition lacking inline
  • Mismatched class definitions across TUs = silent ODR UB
Q19

The static keyword means different things in different places in C++. Enumerate them.

BasicFundamentals

Answer

Four distinct meanings. (1) Static local variable inside a function: the object is constructed on first execution of its declaration and lives until program exit. Since C++11 that initialization is thread-safe, the compiler emits a guard (you can see __cxa_guard_acquire in the assembly), which makes the Meyers singleton (static Foo instance; return instance;) correct without explicit locking. (2) Static data member of a class: one instance shared by all objects of the class, stored in static storage, not in the objects. Historically it required an out-of-class definition in a .cpp; since C++17, inline static members can be defined in the class body, killing a whole category of linker errors. (3) Static member function: callable without an object, no this pointer, so it can only touch static members; commonly used for factories and callbacks that need a plain function pointer. (4) Static at namespace scope (a free function or global variable marked static): internal linkage, meaning the symbol is invisible to other translation units; two .cpp files can each have their own static helper with the same name without colliding.

The modern preference is an unnamed namespace over static for internal linkage, same effect, also works for types. Interview traps: the static initialization order fiasco, where globals in different TUs have unspecified relative construction order, so one global's constructor reading another global from a different TU may see it uninitialized; the fix is a function-local static behind an accessor function. Also be ready to state that static local destruction order is the reverse of construction and runs at exit, which interacts badly with threads still running during shutdown.

// meaning 1: thread-safe lazy singleton (Meyers)
Logger& logger() {
  static Logger instance("/var/log/app.log");  // guarded init, C++11
  return instance;
}

class Counter {
public:
  inline static int total = 0;   // meaning 2: C++17 inline static member
  static void reset() { total = 0; }  // meaning 3: no this pointer
};

// meaning 4: internal linkage, invisible outside this .cpp
static int localHelper(int x) { return x * 2; }
namespace {                       // preferred modern equivalent
  int alsoInternal(int x) { return x + 1; }
}

Key Points

  • Function-local static: lazy, thread-safe since C++11
  • inline static members (C++17) need no out-of-class definition
  • Namespace-scope static = internal linkage; prefer unnamed namespace
  • Static initialization order across TUs is unspecified: use accessors
Q20

What does inline actually mean in modern C++? It is not about inlining.

BasicCompilation Model

Answer

In modern C++, inline is a linkage instruction, not an optimization hint. Marking a function inline tells the toolchain: this definition may appear in multiple translation units (because it lives in a header included everywhere), all definitions are identical, keep one. Without inline, a function defined in a header produces a 'multiple definition' linker error the moment two .cpp files include it.

Whether the optimizer actually inlines the call is a completely separate decision the compiler makes from its own cost model at -O2, guided by function size, call frequency (with PGO), and attributes; compilers freely inline functions never marked inline and refuse to inline marked ones. When you truly need to force the issue you use compiler attributes: __attribute__((always_inline)) / [[gnu::always_inline]] on GCC/Clang or __forceinline on MSVC, and [[gnu::noinline]] to forbid it, tools for hot-path surgery after profiling, not defaults. Related facts that complete the answer: functions defined inside a class body are implicitly inline; constexpr functions are implicitly inline; templates behave like inline (vague linkage, deduplicated by the linker).

C++17 added inline variables, which solved the ancient problem of header-only libraries needing global state: inline int config_version = 3; in a header is one shared variable across all TUs, no .cpp definition needed, and inline static class members are the same mechanism. An interview one-liner that lands well: 'inline means the ODR forgives repeated definitions; the optimizer never needed my permission to inline.' Also worth knowing: excessive inlining bloats code size and can hurt instruction-cache performance, one reason -O3 can occasionally lose to -O2 on large codebases.

// mathutil.h  (header-only library, included by many .cpp files)
#pragma once

inline double lerp(double a, double b, double t) {  // ODR-safe in header
  return a + (b - a) * t;
}

inline int call_count = 0;      // C++17 inline variable: one instance

struct Vec2 {
  double x, y;
  double len2() const { return x * x + y * y; }  // implicitly inline
};

// forcing actual inlining is a separate, compiler-specific tool:
[[gnu::always_inline]] inline double sq(double v) { return v * v; }

Key Points

  • inline = 'multiple identical definitions allowed', an ODR property
  • Actual inlining is the optimizer's independent decision
  • In-class definitions and constexpr functions are implicitly inline
  • C++17 inline variables enable header-only global state
Q21

How do lambda captures work, and when does capturing by reference bite you in production?

BasicModern C++

Answer

A lambda compiles into an unnamed class (a closure type) with one data member per captured variable and an operator() containing the body. [x] captures by value (a copy made when the lambda is created, not when called); [&x] captures by reference (the member is effectively a reference to the original); [=] and [&] capture everything used, by value or reference respectively; [this] captures the enclosing object's pointer; and C++14 init-captures [p = std::move(ptr)] let you move things in or compute new members. Value captures are const by default inside the body, mutable unlocks mutation of the lambda's own copies. The production bite: a by-reference capture whose referent dies before the lambda runs.

Every asynchronous pattern is exposed: posting a lambda to a thread pool with [&] captured stack locals, capturing this in a callback that outlives the object (the timer fires after the widget is destroyed), std::async or std::thread with references to loop variables. The symptom is a use-after-free that ASan reports at callback time, far from the bug. Rules that hold up: lambdas executed synchronously in the same scope (passed to std::sort, std::for_each) may capture by reference freely; lambdas that escape the current scope (stored, queued, detached) must capture by value, moving in expensive things with init-capture, and must capture shared state via shared_ptr, or weak_ptr checked with lock() at call time for the this problem.

C++20 deprecated the implicit capture of this via [=] exactly because of this trap; write [self = shared_from_this()] for async members. Also know that a capture-less lambda converts to a plain function pointer, which is what lets it pass into C APIs like qsort or pthread_create.

#include <functional>
#include <memory>
#include <thread>
#include <vector>

std::function<int()> makeCounter() {
  int start = 100;
  return [start]() mutable { return ++start; };  // owns its copy: safe
  // return [&start] { return ++start; };        // dangles after return: UB
}

struct Session : std::enable_shared_from_this<Session> {
  void poll();
  void schedule(std::vector<std::function<void()>>& queue) {
    queue.push_back([self = shared_from_this()] {  // keeps object alive
      self->poll();
    });
  }
};

void worker() {
  auto big = std::make_unique<std::vector<int>>(1'000'000);
  std::thread t([data = std::move(big)] {          // move into the closure
    /* use *data */
  });
  t.join();
}

Key Points

  • Lambda = compiler-generated class; captures are its members
  • Escaping lambdas must capture by value / move / shared_ptr
  • this capture in async callbacks: use weak_ptr or shared_from_this
  • Capture-less lambdas convert to C function pointers
💡 Pro Tip: State the rule as 'capture by reference only when the lambda dies before the captures do'. Interviewers accept that as the complete safety story.
Q22

std::array vs C-style array vs std::vector: what actually changes between them?

BasicContainers

Answer

A C-style array (int a[8]) is a contiguous block with size fixed at compile time, but it is a second-class citizen: it decays to a pointer the moment you pass it to a function (losing its size), cannot be copied by assignment, cannot be returned from functions, and sizeof tricks for element counts break silently after decay. std::array<int, 8> (C++11) is a zero-overhead struct wrapping that same block: identical memory layout and performance, but it copies, compares (== and <=> elementwise), knows its size(), offers iterators, at() with bounds checking, and passes to functions without decaying. There is no allocation and no size member at runtime; the size is part of the type. std::vector differs fundamentally: the buffer lives on the heap, the size is dynamic, and the object itself is three pointers. Decision rules: size known at compile time and small enough for the stack, std::array; size known only at runtime or large, std::vector; interfacing with C APIs, both expose data().

The modern glue type is std::span<T> (C++20): a non-owning view of any contiguous sequence (C array, std::array, vector, or pointer-plus-length), which is what function parameters should take so one signature accepts all of them without templates or overloads; std::span<const T> for read-only. Interview probes: why does void f(int arr[10]) actually receive int*, and how do you preserve the size (answer: template on size with int (&arr)[N], or take std::span or std::array by reference); what is the cost of std::array by value in a function call (a full copy, so pass by const& or span when large); and aggregate initialization rules, std::array supports brace-init like the raw array it wraps.

#include <array>
#include <span>
#include <vector>
#include <numeric>

// one signature for C array, std::array, vector: span
int sum(std::span<const int> xs) {
  return std::accumulate(xs.begin(), xs.end(), 0);
}

void demo() {
  int c_arr[4] = {1, 2, 3, 4};
  std::array<int, 4> arr = {1, 2, 3, 4};
  std::vector<int> vec = {1, 2, 3, 4};

  sum(c_arr);   // decays, but span captures the extent
  sum(arr);
  sum(vec);

  auto copy = arr;          // real copy semantics, unlike int[4]
  static_assert(arr.size() == 4);  // size is a compile-time constant
}

Key Points

  • C arrays decay to pointers; sizes vanish at function boundaries
  • std::array: same layout, zero cost, value semantics, no decay
  • vector: heap buffer, dynamic size, three-pointer object
  • std::span is the parameter type that unifies all three
Q23

Why is 'using namespace std' in a header considered a firing offense, and what is argument-dependent lookup?

BasicCompilation Model

Answer

A using-directive in a header is transitive pollution: every translation unit that includes the header, directly or through a chain of includes, gets the entire std namespace dumped into its scope, forever, with no way to opt out. That creates two concrete failure modes. First, silent collisions: user code with its own count, distance, swap, begin, size, or byte (all std names) suddenly faces ambiguity errors, or worse, silently changed overload resolution when a std function becomes a better match than the intended local one.

Second, fragility across standard versions: each new C++ standard adds names to std (std::format, std::expected, std::print), so a codebase that compiled cleanly can break just by upgrading the toolchain. Inside a .cpp file or a function body, a using-directive is a contained style choice; in a header it imposes the choice on everyone downstream. Argument-dependent lookup (ADL, Koenig lookup) is the related mechanism worth explaining: when you call an unqualified function f(x), the compiler searches not just the enclosing scopes but also the namespaces of the argument types.

This is why std::cout << "hi" works, operator<< lives in namespace std and is found because the arguments are std types, and why the idiomatic generic swap is 'using std::swap; swap(a, b);' rather than std::swap(a, b): the using-declaration provides the fallback while ADL finds a type's own more efficient swap in its home namespace. ADL is also an attack surface for surprises: a function in your type's namespace can be picked up by an unqualified call you never intended to customize, which is why libraries qualify calls (std::move, std::forward are always written qualified) to suppress ADL when they need exactness.

// header.h: NEVER do this
// using namespace std;      // pollutes every includer, forever

// ADL in action:
namespace geo {
  struct Point { int x, y; };
  void swap(Point& a, Point& b) {          // found via ADL
    std::swap(a.x, b.x); std::swap(a.y, b.y);
  }
}

template <typename T>
void rotate3(T& a, T& b, T& c) {
  using std::swap;   // fallback for types without their own swap
  swap(a, b);        // ADL prefers geo::swap for geo::Point
  swap(b, c);
}

Key Points

  • Header using-directives leak into every downstream TU
  • New std names each standard turn it into a time bomb
  • ADL searches argument types' namespaces on unqualified calls
  • 'using std::swap; swap(a,b);' is the canonical ADL idiom
Q24

Compare static_cast, dynamic_cast, const_cast, and reinterpret_cast. When is each the right tool?

BasicType System

Answer

C++ split C's one-size-fits-all cast into four searchable, intent-revealing operators. static_cast is the workhorse for conversions the type system understands: numeric conversions (double to int, with truncation), enum to integer and back, void* back to its original type, and pointer up/down class hierarchies without runtime checking. Downcasting with static_cast is fast but on you: if the object is not actually the target type, using the result is UB. dynamic_cast is the checked downcast for polymorphic types (the source type must have at least one virtual function): it consults RTTI at runtime; for pointers it returns nullptr on failure, for references it throws std::bad_cast. It also handles cross-casts in multiple inheritance and sidecasts static_cast cannot.

Its cost is a real runtime check (string comparison of type_info in the general case), which is why hot loops avoid it and why some codebases (games, HFT) compile with -fno-rtti and forbid it entirely, replacing it with their own type tags. const_cast removes or adds const/volatile, and its only legitimate use is interfacing with const-incorrect legacy APIs; writing through a const_cast to an object that was originally declared const is UB. reinterpret_cast reinterprets the bit pattern: pointer to integer (uintptr_t), unrelated pointer types, function pointers. Nearly every use is implementation-defined at best; the classic type-punning use (float bits as int) is UB via reinterpret_cast and must be done with std::memcpy or C++20's std::bit_cast, which is the modern, constexpr-friendly answer. Interview framing that lands: 'static_cast for sanctioned conversions, dynamic_cast when the type is genuinely unknown at compile time, const_cast only at const-broken boundaries, reinterpret_cast almost never, and bit_cast for punning.'

#include <bit>
#include <cstdint>

struct Shape { virtual ~Shape() = default; };
struct Circle : Shape { double r = 1.0; };

void demo(Shape* s) {
  // checked downcast: nullptr if s is not actually a Circle
  if (auto* c = dynamic_cast<Circle*>(s)) { (void)c->r; }

  // unchecked downcast: valid ONLY if you know the dynamic type
  auto* c2 = static_cast<Circle*>(s);
  (void)c2;

  double d = 3.9;
  int i = static_cast<int>(d);            // truncates to 3, visibly

  float f = 1.5f;
  auto bits = std::bit_cast<std::uint32_t>(f);  // legal type punning
  (void)i; (void)bits;
  // *(std::uint32_t*)&f  <- reinterpret_cast punning: UB, do not
}

Key Points

  • static_cast: sanctioned conversions, unchecked downcasts
  • dynamic_cast: RTTI-checked, nullptr/bad_cast on failure, has cost
  • const_cast: legacy boundaries only; writing to true const is UB
  • Type punning: std::bit_cast or memcpy, never reinterpret_cast deref
Q25

What actually happens when you std::move an object? Walk through move semantics precisely.

IntermediateMove Semantics

Answer

std::move moves nothing. It is a cast: static_cast<T&&>(x), turning an lvalue into an xvalue so that overload resolution selects move constructors and move assignment operators instead of their copy counterparts. The actual resource theft happens inside the move constructor: a moving std::vector copies three pointers from the source and nulls them out, instead of allocating and copying N elements.

The moved-from object must be left in a 'valid but unspecified state': its destructor must run safely, and you may assign to it or call state-free operations (clear(), empty()), but reading its value is a logic bug (clang-tidy's bugprone-use-after-move flags it). Facts interviewers drill into: moving is not always cheap or even a move, std::move on a const object silently selects the copy constructor, because T&& will not bind to const T&&'s content mutation needs (a very common silent performance bug: marking members const disables moving); an SSO string's move copies characters; a std::array's move moves each element, O(n). Return statements do not need std::move: returning a local by value triggers NRVO or an implicit move, and writing return std::move(local) actively disables copy elision (compilers warn with -Wpessimizing-move).

The move-if-noexcept rule ties it together: std::vector regrowth uses moves only when the element's move constructor is noexcept; otherwise it copies, to preserve the strong exception guarantee, so forgetting noexcept on your move constructor silently degrades every vector<YourType> reallocation to deep copies. Verifying that a type moves as intended with static_assert(std::is_nothrow_move_constructible_v<T>) is a habit that reads as senior in interviews.

#include <string>
#include <utility>
#include <vector>

class Buffer {
  char* data_ = nullptr;
  size_t n_ = 0;
public:
  explicit Buffer(size_t n) : data_(new char[n]), n_(n) {}
  ~Buffer() { delete[] data_; }
  Buffer(const Buffer&) = delete;

  Buffer(Buffer&& o) noexcept                 // noexcept is load-bearing
    : data_(std::exchange(o.data_, nullptr)),
      n_(std::exchange(o.n_, 0)) {}

  Buffer& operator=(Buffer&& o) noexcept {
    if (this != &o) {
      delete[] data_;
      data_ = std::exchange(o.data_, nullptr);
      n_ = std::exchange(o.n_, 0);
    }
    return *this;
  }
};

static_assert(std::is_nothrow_move_constructible_v<Buffer>);
// vector<Buffer> now MOVES on regrow; without noexcept it would copy

Key Points

  • std::move is just a cast to rvalue reference
  • const members/objects silently copy instead of move
  • return std::move(local) pessimizes: disables NRVO
  • vector regrows with moves only if move ctor is noexcept
Q26

What are RVO and NRVO, and what exactly did C++17 guarantee about copy elision?

IntermediateMove Semantics

Answer

Copy elision means the compiler constructs a returned object directly in the caller's storage, skipping the copy or move that the source code appears to request. RVO (return value optimization) applies to returning a prvalue: return Widget(a, b); constructs the Widget straight into the caller's variable. NRVO (named RVO) applies to returning a named local: Widget w; ...; return w; where the compiler allocates w in the caller's slot from the start.

Before C++17, both were optional optimizations, and the type still had to have an accessible copy or move constructor even if it was never called. C++17 changed the object model: a prvalue is no longer 'a temporary that gets copied' but a recipe for initializing an object, so RVO for prvalues is guaranteed by the language, not an optimization. Consequence: you can return non-movable, non-copyable types by value, factory functions returning std::mutex-holding objects, std::atomic, or lock guards became legal.

NRVO remains non-guaranteed (though universally implemented for simple cases); when it cannot apply, C++ falls back to an implicit move of the returned local. The interview traps: return std::move(w) defeats both NRVO and the guaranteed elision, forcing a real move, compilers warn via -Wpessimizing-move; returning a member (return this->widget_;) copies, because elision never applies to objects that outlive the function; multiple return paths returning different named locals can defeat NRVO, so single-exit or prvalue returns help; and returning a function parameter by value moves rather than elides, since the parameter lives in the caller's frame area. Practical takeaway you should say out loud: 'return by value, plainly, and let the compiler do its job'; out-parameters for performance are a pre-2017 habit.

#include <mutex>
#include <vector>

struct Registry {
  std::mutex m;                      // non-copyable, non-movable member
  std::vector<int> items;
};

Registry makeRegistry() {
  return Registry{};                 // OK since C++17: guaranteed elision,
}                                    // no copy/move ctor required at all

std::vector<int> build() {
  std::vector<int> v;
  v.reserve(1000);
  for (int i = 0; i < 1000; ++i) v.push_back(i);
  return v;            // NRVO in practice; implicit move as fallback
  // return std::move(v);   // WORSE: forces move, blocks NRVO
}

Key Points

  • C++17: prvalue returns construct in place, guaranteed, no ctor needed
  • NRVO still optional; implicit move is the fallback
  • return std::move(local) is a pessimization, warned by compilers
  • Members and parameters are never elided, only true locals
Q27

How do forwarding references and std::forward implement perfect forwarding, and where does it break?

IntermediateTemplates

Answer

A forwarding reference is T&& where T is a deduced template parameter (or auto&&). It is not an rvalue reference: through reference collapsing, passing an lvalue deduces T as X&, and X& && collapses to X&, while passing an rvalue deduces T as X, giving X&&. So one signature binds to everything and remembers the value category in T. std::forward<T>(arg) completes the trick: it casts arg back to the original category, an lvalue stays an lvalue, an rvalue becomes an rvalue again (necessary because a named parameter is itself always an lvalue inside the function).

This is how emplace_back, make_unique, and every wrapper template pass arguments to constructors with zero copies and correct move behavior. The distinction interviewers demand: void f(Widget&&) is an rvalue reference (binds only rvalues, no deduction); template<class T> void f(T&&) is a forwarding reference; and const T&& is NOT forwarding, constness kills the deduction dance. Where perfect forwarding breaks, the part that separates senior candidates: braced init-lists ({1,2,3}) cannot be deduced, so emplace_back({1,2,3}) fails where push_back({1,2,3}) works; NULL and 0 forward as integers, losing pointer-ness (use nullptr); bitfields cannot bind to references; overloaded function names and templates cannot deduce; and forwarding into an overload set picks the wrong function when the forwarded type is slightly off. The most practical bug: a forwarding-reference constructor template<class T> Widget(T&&) out-competes the copy constructor for non-const lvalue arguments (Widget w2(w1) matches T = Widget& exactly, beating the const Widget& copy ctor), hijacking copies; the fix is constraining it with a concept or requires clause excluding Widget itself, which is also a neat segue into C++20 concepts.

#include <utility>
#include <type_traits>

template <typename F, typename... Args>
decltype(auto) timed(F&& f, Args&&... args) {
  // forward each argument with its original value category
  return std::forward<F>(f)(std::forward<Args>(args)...);
}

class Widget {
public:
  Widget() = default;
  Widget(const Widget&) = default;

  // constrained so it stops hijacking the copy constructor
  template <typename T>
    requires (!std::is_same_v<std::remove_cvref_t<T>, Widget>)
  explicit Widget(T&& src) { /* construct from anything else */ }
};

void demo() {
  Widget a;
  Widget b(a);   // copy ctor, because the template excluded Widget
}

Key Points

  • T&& with deduced T + reference collapsing = binds everything
  • std::forward restores the original value category
  • Braced lists, NULL, bitfields, overload sets break forwarding
  • Unconstrained forwarding ctors hijack the copy ctor: constrain them
Q28

Why must template definitions live in headers, and what are explicit instantiation and extern template for?

IntermediateTemplates

Answer

A template is not code; it is a recipe. The compiler generates actual functions and classes only at instantiation, when it sees the template used with concrete types, and to generate that code it needs the full definition visible in the same translation unit. If you declare template<class T> T maxOf(T, T); in a header but define it in util.cpp, then main.cpp instantiating maxOf<int> has only the declaration: the compiler emits a call to a symbol that nothing ever generates (util.cpp never saw maxOf<int> being used), and the linker fails with 'undefined reference to maxOf<int>(int, int)'.

Hence the rule: templates are defined in headers (or in .tpp/.ipp files included by the header, a purely cosmetic split). Every TU that uses maxOf<int> generates its own copy; the linker deduplicates identical instantiations (vague linkage), which is correct but costs compile time, the same template getting compiled dozens of times across a build is a major contributor to slow C++ builds. Explicit instantiation is the escape hatch: writing 'template class std::vector<MyType>;' in exactly one .cpp forces generation there, and 'extern template class std::vector<MyType>;' in the header tells all other TUs to suppress their own instantiation and link against that one.

Libraries with a known, closed set of instantiations use this to move template code out of headers entirely: header declares, one .cpp explicitly instantiates for the supported types. Related things worth stating: two-phase lookup (non-dependent names are checked at definition, dependent ones at instantiation, which is why errors sometimes only appear on use, and why 'typename' and 'template' disambiguators are needed for dependent names); and that C++20 modules genuinely help here, since a module interface compiles the template definition once into a BMI instead of re-parsing it per includer.

// matrix.h
template <typename T>
class Matrix {
public:
  Matrix(int r, int c);
  T& at(int r, int c);
  // ... definition must be visible to instantiate, so either here
  // or in matrix.tpp included below
};
#include "matrix.tpp"

// Suppress per-TU instantiation for the common cases:
extern template class Matrix<float>;    // in matrix.h
extern template class Matrix<double>;

// matrix.cpp: the ONE place these are compiled
// #include "matrix.h"
template class Matrix<float>;            // explicit instantiation
template class Matrix<double>;

Key Points

  • Instantiation needs the definition in the same TU
  • Template-in-.cpp = classic 'undefined reference' linker error
  • extern template suppresses duplicate instantiation, cuts build time
  • Dependent names resolve at instantiation: two-phase lookup
Q29

How has compile-time branching evolved: SFINAE, if constexpr, and tag dispatch? When do you still see each?

IntermediateTemplates

Answer

SFINAE (substitution failure is not an error) is the pre-C++17 mechanism: when substituting template arguments produces an invalid type or expression in the declaration, that overload is silently removed from the candidate set rather than causing a compile error. Combined with std::enable_if, it let library authors switch implementations on type properties: template<class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>. It works, but the error messages are notorious, the syntax is noise, and the conditions live in the signature where they obscure the API.

Tag dispatch was the cleaner classic alternative: a public function forwards to overloads distinguished by an empty tag type (std::true_type/std::false_type), letting ordinary overload resolution do the branching; the iterator category machinery in the standard library (std::advance choosing O(1) for random access vs O(n) otherwise) is the canonical example. C++17's if constexpr collapsed most of this into straight-line code: the condition is evaluated at compile time, and the false branch is discarded, not instantiated, so it may contain code that would not even compile for the current T. One function, readable branches, sane errors.

C++20 concepts finished the job at the interface level: requires clauses and named concepts constrain templates declaratively, produce readable diagnostics, and rank overloads by constraint subsumption. What you should say about current practice: new code uses concepts for constraining interfaces and if constexpr for internal branching; tag dispatch survives inside standard libraries and where overload sets must remain open for user extension; raw enable_if SFINAE is legacy, still read daily in older codebases and pre-C++17 library internals, so you must be able to read it, but writing it in new code is a smell. Detection idioms (std::void_t tricks) have similarly been replaced by requires-expressions you can use inline as boolean predicates.

#include <type_traits>
#include <string>
#include <concepts>

// 2026 style: concepts constrain, if constexpr branches
template <typename T>
concept Stringish = std::convertible_to<T, std::string_view>;

template <typename T>
std::string describe(const T& v) {
  if constexpr (std::is_integral_v<T>) {
    return "int:" + std::to_string(v);       // discarded unless integral
  } else if constexpr (Stringish<T>) {
    return "str:" + std::string(v);
  } else {
    static_assert(std::is_floating_point_v<T>, "unsupported type");
    return "float:" + std::to_string(v);
  }
}

// legacy SFINAE you must still be able to READ:
template <typename T,
          std::enable_if_t<std::is_integral_v<T>, int> = 0>
T twice(T v) { return v * 2; }

Key Points

  • SFINAE removes overloads on substitution failure; enable_if is legacy
  • if constexpr discards the untaken branch without instantiating it
  • Tag dispatch still powers iterator-category selection in the stdlib
  • Concepts are the modern interface-level constraint tool
Q30

What problems do C++20 concepts solve, and how do you write and use one?

IntermediateTemplates

Answer

Concepts are named, reusable compile-time predicates over types, and they attack three chronic template problems. Error quality: calling std::sort on a std::list used to produce hundreds of lines of instantiation backtrace from deep inside the algorithm; with the constrained std::ranges::sort, the error says directly that std::list's iterators do not satisfy random_access_iterator. Interface documentation: template<typename T> says nothing; template<std::floating_point T> is a contract readable by humans and enforced by the compiler at the call site, before instantiation.

Overload control: constrained overloads participate in resolution by subsumption, a more-constrained overload wins over a less-constrained one, replacing brittle enable_if mutual exclusion. Syntax spectrum you should demonstrate: a concept definition (concept Hashable = requires(T t) { { std::hash<T>{}(t) } -> std::convertible_to<size_t>; }); a requires clause (template<class T> requires Hashable<T>); the terse forms (template<Hashable T>, and void f(Hashable auto x)); and a standalone requires-expression used as a boolean inside if constexpr. A requires-expression can check four things: an expression compiles (t.serialize()), a type exists (typename T::value_type), an expression's type satisfies another concept (the compound { expr } -> concept form), and nested requirements.

Standard library concepts worth naming: std::integral, std::floating_point, std::convertible_to, std::same_as, std::invocable, ranges concepts like std::ranges::range and random_access_iterator. Gotchas for depth: concepts check syntax, not semantics (a type can satisfy std::equality_comparable with a nonsensical operator==, the semantic requirements are documentation); constraints are not instantiation, the body can still fail; and putting concepts on everything is over-constraint, constrain public API boundaries, not every internal helper.

#include <concepts>
#include <string>

template <typename T>
concept Serializable = requires(const T& t) {
  { t.serialize() } -> std::convertible_to<std::string>;
  typename T::id_type;                      // member type must exist
  requires std::default_initializable<T>;   // nested requirement
};

// three equivalent ways to constrain:
template <Serializable T>
void store(const T& obj);

template <typename T> requires Serializable<T>
void archive(const T& obj);

void upload(Serializable auto const& obj);   // terse form

// subsumption: the more-constrained overload wins for both
template <std::integral T>       void log(T v);   // ints go here
template <typename T>            void log(T v);   // everything else

Key Points

  • Errors move to the call site and become readable
  • requires-expressions test expressions, member types, nested concepts
  • Subsumption ranks constrained overloads without enable_if tricks
  • Concepts check syntax only; semantics remain a documented contract
Q31

Differentiate constexpr, consteval, and constinit. What can constexpr functions do in recent standards?

IntermediateModern C++

Answer

constexpr on a variable means it is a compile-time constant (and implies const). constexpr on a function means it CAN be evaluated at compile time when its arguments are constant expressions, but it remains a perfectly normal function at runtime otherwise, dual-use. consteval (C++20) declares an immediate function: every call MUST evaluate at compile time, and a call with runtime arguments is a compile error; use it for things that make no sense at runtime, like compile-time hashing of format strings (std::format's format-string checking is built on consteval) or generating lookup tables. constinit (C++20) is different in kind: it does not make anything const, it asserts that a variable with static or thread storage duration is initialized by constant initialization, eliminating the static initialization order fiasco for that variable; a mutable global counter can be constinit. What constexpr functions may contain has expanded every standard, and interviewers use it to date your knowledge: C++11 allowed a single return statement; C++14 allowed loops, branches, and local mutation; C++20 allowed virtual calls, try/catch, and, crucially, dynamic allocation with new/delete provided the memory is freed before evaluation ends, which made std::vector and std::string usable inside constant evaluation; C++23 relaxed further (constexpr goto and labels, non-literal locals in unreached paths, and static constexpr locals in constexpr functions). The escape hatch std::is_constant_evaluated() (C++20), and the cleaner 'if consteval' (C++23), lets one function pick a compile-time-safe algorithm during constant evaluation and a faster intrinsic at runtime. Real production uses to cite: precomputed CRC and sine tables, compile-time parsing of config DSLs and regexes (CTRE), unit-checked physical quantities, and eliminating runtime init order hazards in embedded firmware where dynamic initializers are forbidden.

#include <array>
#include <cstdint>

constexpr std::uint32_t crc32_step(std::uint32_t c) {
  for (int k = 0; k < 8; ++k)
    c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
  return c;
}

// table built entirely at compile time, lands in .rodata
constexpr auto kCrcTable = [] {
  std::array<std::uint32_t, 256> t{};
  for (std::uint32_t i = 0; i < 256; ++i) t[i] = crc32_step(i);
  return t;
}();

consteval int square(int n) { return n * n; }  // compile time ONLY
constexpr int sq2(int n) { return n * n; }     // both worlds

constinit int g_requests = 0;   // mutable, but init order is guaranteed

int main(int argc, char**) {
  static_assert(kCrcTable[1] == 0x77073096u);
  int a = square(4);       // OK: constant argument
  // int b = square(argc); // error: consteval needs compile-time args
  int c = sq2(argc);       // OK: runs at runtime
  return a + c;
}

Key Points

  • constexpr functions are dual-use; consteval is compile-time only
  • constinit kills static init order fiasco without adding const
  • C++20 allowed constexpr new/vector/string; C++23 relaxed further
  • if consteval / is_constant_evaluated() picks per-context algorithms
Q32

Give concrete examples of undefined behavior and explain why compilers are allowed to do surprising things with it.

IntermediateUndefined Behavior

Answer

Undefined behavior is a contract term: for certain operations the standard places no requirements whatsoever on the implementation. The optimizer's license flows from one inference rule: the compiler may assume UB never happens, and optimize under that assumption. Concrete examples you should rattle off: signed integer overflow (INT_MAX + 1); out-of-bounds array access; dereferencing null or dangling pointers; use-after-free; data races on non-atomic variables; reading an uninitialized variable; shifting by the type's width or more; violating strict aliasing (accessing an object through an incompatible pointer type); modifying a string literal; infinite loops without side effects (before C++26 relaxed this for trivial cases).

The 'surprising' part comes from the assumption propagating backwards. Example one: 'if (x + 1 < x)' as an overflow check on signed x is deleted entirely, since overflow 'cannot happen', the condition is provably false. Example two: dereferencing a pointer and later null-checking it lets the compiler delete the null check, the dereference already 'proved' non-null; a famous Linux kernel vulnerability came from exactly this deletion pattern.

Example three: a function whose only path with defined behavior calls opendir() can be 'optimized' into calling it unconditionally. Why the license exists: assuming no overflow lets loops like for(int i = 0; i < n; ++i) be vectorized and their trip counts computed; assuming no aliasing enables register caching of memory. Interview-ready mitigations: -fsanitize=undefined (UBSan) instruments overflow, bad shifts, misaligned access at ~20% cost; -fwrapv makes signed overflow wrap (defining it, at optimization cost); -O0 hides symptoms but does not fix bugs; and constexpr evaluation rejects UB at compile time, one of its underrated virtues. Distinguish UB from unspecified (one of several valid outcomes, like evaluation order) and implementation-defined (documented choice, like sizeof(long)).

#include <climits>
#include <cstdio>

int bad_overflow_check(int x) {
  // UB when x == INT_MAX, so the compiler ASSUMES it never is:
  if (x + 1 < x) return -1;   // entire branch deleted at -O2
  return x + 1;
}

int null_check_deleted(int* p) {
  int v = *p;                 // 'proves' p != nullptr
  if (p == nullptr) return 0; // dead code after the deref: removed
  return v;
}

// build with: g++ -O2 -fsanitize=undefined,address demo.cpp
// UBSan at runtime prints e.g.:
// runtime error: signed integer overflow: 2147483647 + 1

Key Points

  • Compiler assumes UB is unreachable and optimizes accordingly
  • Overflow checks and null checks get silently deleted
  • UBSan (-fsanitize=undefined) catches it at runtime cheaply
  • Know UB vs unspecified vs implementation-defined
💡 Pro Tip: Quote a real deletion example (overflow check or null check). It proves you have seen UB fight back, not just read the definition.
Q33

AddressSanitizer vs Valgrind vs the other sanitizers: what does each catch, and how do you run them in practice?

IntermediateTooling

Answer

AddressSanitizer (ASan, -fsanitize=address on GCC/Clang) is compile-time instrumentation: every load and store checks shadow memory recording which bytes are addressable. It catches heap and stack use-after-free, heap/stack/global buffer overflows, use-after-return (with ASAN_OPTIONS=detect_stack_use_after_return=1), and double-free, with precise reports: the faulting access, the allocation stack, and the free stack. Overhead is roughly 2x CPU and 2-3x memory, fast enough to run your whole test suite under it, which is exactly what serious CI does.

LeakSanitizer piggybacks on ASan (detect_leaks=1, default on Linux) for leak reports at exit. Valgrind's memcheck needs no recompilation, it runs the unmodified binary under dynamic translation, which makes it the tool for third-party binaries and for catching reads of uninitialized memory (ASan does not track that; MemorySanitizer does, but MSan requires rebuilding every dependency, so in practice Valgrind fills the gap). Valgrind's cost is 10-30x slowdown, so it is a targeted-repro tool, not a CI default.

The rest of the family: UBSan (-fsanitize=undefined) for overflow, bad shifts, misaligned access, cheap enough to combine with ASan in one build; TSan (-fsanitize=thread) for data races, ~5-10x slowdown, incompatible with ASan in the same binary, so CI runs a separate TSan job. Practical realities interviewers respect: sanitizers only see executed paths, coverage matters; combine with fuzzing (libFuzzer is literally -fsanitize=fuzzer plus ASan) to explore inputs; suppression files handle known third-party noise; and -fno-omit-frame-pointer plus -g make reports readable. A strong closing line: 'our definition of done includes the test suite green under ASan+UBSan, TSan for anything touching threads, and Valgrind on the rare uninitialized-read hunts.'

# Build once with sanitizers (Debug or RelWithDebInfo):
g++ -g -O1 -fno-omit-frame-pointer \
    -fsanitize=address,undefined app.cpp -o app

./app
# ==12345==ERROR: AddressSanitizer: heap-use-after-free on address ...
#     READ of size 4 at 0x60200000eff0 thread T0
#     #0 0x... in Widget::poll() widget.cpp:42
#   freed by thread T0 here: ...
#   previously allocated by thread T0 here: ...

# Data races: separate build, TSan and ASan cannot combine
g++ -g -O1 -fsanitize=thread server.cpp -o server_tsan

# Unmodified binary / uninitialized reads:
valgrind --leak-check=full --track-origins=yes ./app

Key Points

  • ASan: instrumented builds, UAF/overflows, ~2x cost, CI-friendly
  • Valgrind: no rebuild, uninitialized reads, 10-30x, targeted use
  • TSan for races (separate build), UBSan combines with ASan
  • Sanitizers only see executed paths: pair with tests and fuzzing
Q34

How do std::optional, std::variant, and std::expected change error handling and API design?

IntermediateModern C++

Answer

These vocabulary types encode 'maybe', 'one-of', and 'value-or-error' in the type system, replacing conventions like magic return values, out-parameters with bool returns, and exceptions for expected failures. std::optional<T> (C++17) is a stack-allocated maybe-value: no heap, contains either a T or nothing. Use it for lookups and parses where absence is normal (findUser returning optional<User>). Access: has_value()/operator bool, * and -> (UB if empty, this is the gotcha), value() (throws std::bad_optional_access), value_or(default).

C++23 added the monadic operations and_then, transform, or_else, which chain computations without nested if-checks. Anti-patterns to name: optional<T&> does not exist (use T* or reference_wrapper), and optional<bool> is a three-state trap. std::variant<A, B, C> (C++17) is a type-safe tagged union: exactly one alternative alive at a time, no heap, index() says which. Visit with std::visit and the overloaded-lambdas idiom; get<T> throws std::bad_variant_access, get_if returns nullptr.

It replaces unions-plus-enum hand-rolls and enables sum-type modeling: parser AST nodes, state machines where each state carries different data, protocol messages. Know valueless_by_exception(), the rare state after a throwing move during assignment. std::expected<T, E> (C++23) is the big one for 2026 interviews: either a value or an error object, forcing callers to confront failure at the type level. Unlike exceptions it has zero hidden control flow and works with -fno-exceptions builds (games, embedded, some HFT); unlike error codes it cannot be ignored silently and composes monadically (and_then, or_else, transform_error). Guidance you should state: exceptions for exceptional, non-local failures (constructor failure, resource exhaustion), expected for routine, local failures (parse errors, validation, I/O status), and optional when the absence needs no explanation.

#include <expected>
#include <optional>
#include <variant>
#include <string>
#include <charconv>

enum class ParseErr { Empty, NotANumber, OutOfRange };

std::expected<int, ParseErr> parsePort(std::string_view s) {
  if (s.empty()) return std::unexpected(ParseErr::Empty);
  int v = 0;
  auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
  if (ec == std::errc::invalid_argument)
    return std::unexpected(ParseErr::NotANumber);
  if (ec == std::errc::result_out_of_range || v > 65535)
    return std::unexpected(ParseErr::OutOfRange);
  return v;
}

using Shape = std::variant<struct Circle, struct Rect>;
struct Circle { double r; };
struct Rect { double w, h; };

template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };

double area(const Shape& s) {
  return std::visit(overloaded{
    [](const Circle& c) { return 3.14159 * c.r * c.r; },
    [](const Rect& r)   { return r.w * r.h; },
  }, s);
}

Key Points

  • optional: absence without heap; * on empty is UB
  • variant + visit: type-safe sum types, exhaustive handling
  • expected (C++23): value-or-error, no hidden control flow
  • C++23 monadic ops chain fallible steps cleanly
Q35

What is std::string_view, when should APIs take it, and what are its dangling traps?

IntermediateModern C++

Answer

std::string_view (C++17) is a non-owning view over a contiguous character sequence: a pointer plus a length, 16 bytes, trivially copyable, pass it by value. It unifies read-only string parameters: one signature accepts std::string (implicit conversion), string literals (no allocation, unlike const std::string& which constructs a temporary string from a literal), substrings (substr on a view is O(1) pointer math versus std::string::substr's allocation-and-copy), and buffers from C APIs. For parsing and tokenizing, views are transformative: remove_prefix, remove_suffix, and substr slice without a single allocation, which is why modern parsers and JSON libraries are built on them.

Now the traps, which is what the question is really about. A view does not keep its target alive. Dangling pattern one: std::string_view v = getName(); where getName returns std::string by value, the temporary dies at the end of the full expression and v points into freed memory (ASan flags it; compilers with -Wdangling-gsl catch some cases, and C++23/26 lifetime annotations are improving diagnosis).

Pattern two: returning a view of a local string from a function. Pattern three: storing a view as a class member while the source string is mutated or destroyed, string mutation can reallocate (SSO to heap transition included), silently invalidating the view. Pattern four: views are not null-terminated, so v.data() must never be handed to C APIs expecting a terminator (printf %s, strlen); construct a std::string or use %.*s with the length.

Rules that survive production: string_view for parameters, almost never for return values (except views into the argument itself, clearly documented, like substr-style utilities), and never as a stored member unless the owner's lifetime is structurally guaranteed longer. Same logic extends to std::span<T> for non-character buffers.

#include <string>
#include <string_view>
#include <vector>

// GOOD: one signature, zero allocation for literals and substrings
std::vector<std::string_view> split(std::string_view s, char sep) {
  std::vector<std::string_view> out;
  while (!s.empty()) {
    auto pos = s.find(sep);
    out.push_back(s.substr(0, pos));       // O(1), no copies
    if (pos == std::string_view::npos) break;
    s.remove_prefix(pos + 1);
  }
  return out;   // CALLER must keep the original string alive
}

std::string makeName();

void traps() {
  std::string_view v = makeName();  // DANGLING: temporary died
  (void)v;

  std::string s = "ok";
  std::string_view w = s;
  s += std::string(100, 'x');       // realloc: w now dangles too
  (void)w;
}

Key Points

  • 16-byte non-owning view; pass by value; accepts literals free
  • substr/remove_prefix slice in O(1) with zero allocation
  • Binding to temporaries and storing as members are the killers
  • Not null-terminated: never pass data() to C string APIs
Q36

Explain the erase-remove idiom, why it existed, and what C++20's std::erase_if changed. Cover container-specific erase rules.

IntermediateContainers

Answer

std::remove and remove_if never remove anything, that is the joke and the interview question. Algorithms only see iterators, not containers, so remove_if compacts the kept elements to the front and returns an iterator to the new logical end; the tail contains moved-from garbage and the container's size is unchanged. The erase-remove idiom completes the job: v.erase(std::remove_if(v.begin(), v.end(), pred), v.end()); one pass, O(n), and the single erase call at the end avoids the O(n^2) trap of erasing matches one by one inside a loop (each mid-vector erase shifts the entire tail).

C++20 finally packaged it as free functions: std::erase(container, value) and std::erase_if(container, pred), which work uniformly across vector, deque, string, list, and the associative containers, picking the right strategy per container and returning the number of elements removed. That uniformity matters because hand-written erase loops are container-specific minefields. For vector and string: erase(it) invalidates it and everything after; the correct manual loop is it = v.erase(it) on match, ++it otherwise.

For map/set/unordered_map: remove_if does not apply at all (keys are const, elements cannot be shifted), so pre-C++20 you wrote the it = m.erase(it) loop, and the classic crash is m.erase(it); ++it; using the invalidated iterator, every C++ team has fixed this bug at least once. For list, member remove_if relinks nodes without moving elements, cheaper than the algorithm. Two adjacent facts that earn depth points: erase on associative containers with a key (m.erase(key)) returns the count removed; and C++20 node handles (m.extract(it)) let you remove and re-insert elements between maps without reallocation, including changing the key in place, something erase-plus-insert cannot do without copying the element.

#include <vector>
#include <map>
#include <string>

void demo() {
  std::vector<int> v{1, 2, 3, 4, 5, 6};

  // pre-C++20 idiom:
  v.erase(std::remove_if(v.begin(), v.end(),
                         [](int x) { return x % 2 == 0; }),
          v.end());

  // C++20: uniform, returns removed count
  std::erase_if(v, [](int x) { return x > 3; });

  std::map<std::string, int> m{{"a", 1}, {"b", 2}};
  // manual associative erase loop (pre-C++20), the safe form:
  for (auto it = m.begin(); it != m.end(); ) {
    if (it->second == 1) it = m.erase(it);   // erase returns next
    else ++it;
  }
  // C++20: std::erase_if(m, [](auto& kv){ return kv.second == 1; });
}

Key Points

  • remove_if only compacts; erase shrinks: two-step idiom
  • Per-element mid-vector erasing is O(n^2); the idiom is O(n)
  • Associative containers need it = c.erase(it) loops
  • C++20 std::erase/erase_if unify all of it and return counts
Q37

How do C++20 ranges and views change algorithm code, and what are the lazy-evaluation traps?

IntermediateModern C++

Answer

The ranges library (C++20, <ranges>) rebuilds the algorithms around whole ranges instead of iterator pairs, and adds composable, lazy views. Concretely: std::ranges::sort(v) replaces std::sort(v.begin(), v.end()); constrained by concepts, it gives readable errors (sorting a std::list fails with 'does not satisfy random_access_iterator', not a 200-line backtrace). Projections are the sleeper feature: std::ranges::sort(people, {}, &Person::age) sorts by a member without writing a comparator lambda, and most range algorithms take one.

Views (std::views::filter, transform, take, drop, reverse, split, iota, zip and chunk in C++23, enumerate in C++23) are lazy, non-owning adaptors composed with operator|: nothing computes until iteration, and a pipeline of filter-then-transform-then-take does one pass over the data with no intermediate containers. To materialize, C++23 added std::ranges::to<std::vector>(). Now the traps, which is where interviews go.

Laziness re-evaluates: iterating a filter view twice runs the predicate twice per element, and a transform view's function runs on every access, so an expensive transform accessed repeatedly should be materialized. Statefulness: filter caches begin() on first call, so mutating the underlying container between iterations of the same view object gives stale results, and modifying elements through a filter view in a way that changes the predicate's verdict is undefined. Dangling: views do not own; returning a view over a local container is exactly the string_view mistake, and the library marks some cases with std::ranges::dangling to fail compilation when an algorithm would return an iterator into a destroyed temporary.

Also honest to mention: pipelines can optimize worse than hand-written loops in some compilers, and compile times grow; hot inner loops in HFT code are still often plain loops. But for everyday transformation code, ranges cut real line count and eliminate off-by-one iterator bugs.

#include <ranges>
#include <vector>
#include <string>
#include <algorithm>

struct Person { std::string name; int age; };

std::vector<std::string> adultNames(std::vector<Person>& people) {
  // sort by projection: no comparator lambda needed
  std::ranges::sort(people, {}, &Person::age);

  auto pipeline = people
    | std::views::filter([](const Person& p) { return p.age >= 18; })
    | std::views::transform(&Person::name)
    | std::views::take(10);            // lazy: nothing ran yet

  return std::ranges::to<std::vector>(pipeline);  // C++23: materialize
}

// TRAP: returning a view over a local
// auto broken() {
//   std::vector<int> v{1,2,3};
//   return v | std::views::filter(...);   // dangles: v dies here
// }

Key Points

  • Range algorithms + projections kill boilerplate comparators
  • Views are lazy and non-owning; compose with |, one pass
  • filter caches begin(); re-iteration and mutation are traps
  • ranges::to (C++23) materializes; views over locals dangle
Q38

What are the basic, strong, and nothrow exception guarantees, and how does noexcept interact with them?

IntermediateError Handling

Answer

The guarantees classify what a caller can rely on when an operation throws. Basic guarantee: no leaks and all invariants intact, but the object may be in a different (valid) state; this is the minimum any correct C++ code must provide, and RAII members give it to you nearly for free. Strong guarantee: commit-or-rollback, if the operation throws, the state is exactly as before the call.

Nothrow: the operation cannot throw, period. The standard library models all three: std::vector::push_back gives the strong guarantee (if regrowth throws, the vector is unchanged), which is precisely why regrowth moves elements only when the move constructor is noexcept, a throwing move halfway through a relocation could not be rolled back, so the library copies instead (std::move_if_noexcept is the mechanism). That single fact connects the whole topic and is the most common probe.

The strong guarantee is implemented with copy-and-swap: do all throwing work on a copy, then commit with a nothrow swap; the swap member must therefore be noexcept, as must move constructors and move assignment, and destructors are implicitly noexcept since C++11 (a throwing destructor during unwinding calls std::terminate). noexcept is both a promise and a query: as a specifier it declares the function will not throw, and if it does anyway, std::terminate is called immediately, no unwinding to catch blocks; as an operator, noexcept(expr) is a compile-time check other code (like vector) uses to choose algorithms. Conditional noexcept (noexcept(noexcept(...))) propagates the property through templates. Practical guidance to state: mark moves, swaps, and destructors noexcept always; mark other functions noexcept only when the no-throw property is a real design commitment, because removing noexcept later is an API break; and never use noexcept as an optimization sticker on functions that can throw, terminate-on-throw in production is the failure mode.

#include <utility>
#include <vector>

class Config {
  std::vector<int> data_;
public:
  // strong guarantee via copy-and-swap:
  Config& operator=(const Config& other) {
    Config tmp(other);        // all throwing work happens here
    swap(tmp);                // commit: nothrow
    return *this;
  }
  void swap(Config& o) noexcept { data_.swap(o.data_); }

  Config(Config&&) noexcept = default;             // vector will MOVE
  Config& operator=(Config&&) noexcept = default;
  Config(const Config&) = default;
  Config() = default;
};

static_assert(std::is_nothrow_move_constructible_v<Config>);
// without noexcept moves, std::vector<Config> regrowth would COPY
// (std::move_if_noexcept chooses per-type at compile time)

Key Points

  • Basic: invariants + no leaks; strong: rollback; nothrow: never throws
  • vector regrowth copies unless the move ctor is noexcept
  • Copy-and-swap needs a noexcept swap to commit
  • Throwing from noexcept calls std::terminate, no unwinding
Q39

What does virtual dispatch actually cost, and when is CRTP the right replacement?

IntermediatePerformance

Answer

The mechanical cost of a virtual call is small: load the vptr, load the function pointer from the vtable, indirect call. On a modern out-of-order core with a warm branch target buffer, that is a few cycles. The real costs are secondary.

Inlining is blocked: the compiler cannot see through an indirect call, so a one-line virtual getter in a loop cannot be folded, hoisted, or vectorized with its surroundings, and that lost optimization frequently outweighs the call itself by an order of magnitude. Branch prediction: a call site that dispatches to many different concrete types (a megamorphic site) mispredicts its indirect branch; sorting a heterogeneous vector of Shape* by concrete type before processing measurably speeds up the loop purely through prediction. Memory layout: virtual designs push toward vectors of pointers to heap-allocated objects, and the resulting pointer-chasing cache misses dwarf everything else; this is the data-oriented design critique.

Devirtualization mitigates some of it: final on classes or methods, and LTO with whole-program visibility, let compilers prove the concrete type and inline anyway. CRTP (curiously recurring template pattern), class Derived : Base<Derived>, provides static polymorphism: the base calls static_cast<Derived*>(this)->impl(), resolved at compile time, fully inlinable, zero per-object vptr. It is the right tool when the set of types is closed at compile time and the polymorphism is about code reuse rather than runtime substitution: mixins (operator generation, instrumentation, reference counting), the pattern behind Eigen's expression templates, and policy-based designs.

Its costs: every Derived instantiates separate code (binary bloat), no heterogeneous containers (Base<D1> and Base<D2> are unrelated types), and worse error messages. C++23's 'deducing this' expresses many CRTP mixins more directly. The mature answer interviewers want: virtual for genuine runtime variation at architecture boundaries, CRTP or std::variant+visit for closed sets on hot paths, and measure before assuming dispatch is the bottleneck.

#include <variant>
#include <vector>

// CRTP: static polymorphism, fully inlinable
template <typename Derived>
class Counter {
public:
  void tick() { static_cast<Derived*>(this)->onTick(); }  // no vtable
};

class FastSensor : public Counter<FastSensor> {
public:
  void onTick() { ++n_; }     // inlined into tick() at the call site
private:
  long n_ = 0;
};

// closed-set runtime polymorphism without heap or vtables:
using Event = std::variant<struct Trade, struct Quote>;
struct Trade { double px; };
struct Quote { double bid, ask; };

double handle(const Event& e) {
  return std::visit([](const auto& ev) { return price(ev); }, e);
}
double price(const Trade& t);
double price(const Quote& q);

Key Points

  • Lost inlining and cache misses cost more than the indirect call
  • final + LTO enable devirtualization
  • CRTP: compile-time dispatch for closed type sets and mixins
  • variant+visit is the modern closed-set alternative with values
Q40

How does virtual inheritance solve the diamond problem, and what does it cost at runtime?

IntermediateObject Model

Answer

The diamond: class B and class C both inherit from A, and class D inherits from both B and C. With ordinary inheritance, D contains two separate A subobjects, one via each path. Every unqualified access to A's members from D is ambiguous (compile error), and semantically the object is usually wrong, two copies of what should be one identity or state.

Virtual inheritance (class B : virtual public A) declares that A should be shared: the most derived class D holds exactly one A subobject, and it is D's constructor, not B's or C's, that initializes it directly; B's and C's own constructor calls to A are ignored when they are intermediate bases, a rule that surprises people because B's constructor argument list for A silently does not run. The runtime mechanics are the interesting part. Because the shared A's offset within the complete object depends on the most derived type (B-as-part-of-D locates A differently than a standalone B), the compiler cannot use fixed offsets; access to virtual base members goes through an offset stored in the vtable (vbase offsets in the Itanium ABI).

Costs: an extra indirection on member access from intermediate bases, fatter vtables, more expensive downcasting (static_cast from a virtual base to derived is illegal, you must use dynamic_cast), and constructor complexity. The famous real-world instance is iostreams: istream and ostream both inherit basic_ios virtually so iostream has one stream state. In interviews, after explaining the mechanics, the strong move is questioning the design: diamonds usually indicate inheritance being used for code reuse rather than substitutability; composition, interface-only multiple inheritance (abstract bases with no data, where duplicated empty bases are harmless), or policy templates avoid the problem outright. Most style guides allow multiple inheritance of pure interfaces and treat virtual inheritance of stateful bases as a last resort.

#include <iostream>

struct Device {
  explicit Device(int id) : id(id) {}
  int id;
};

struct Scanner : virtual Device {
  Scanner(int id) : Device(id) {}     // ignored when not most-derived
};
struct Printer : virtual Device {
  Printer(int id) : Device(id) {}
};

struct Copier : Scanner, Printer {
  // most-derived class MUST initialize the shared virtual base:
  Copier(int id) : Device(id), Scanner(id), Printer(id) {}
};

int main() {
  Copier c(7);
  std::cout << c.id;        // one Device: unambiguous, prints 7
  // without 'virtual': error: request for member 'id' is ambiguous
}

Key Points

  • virtual base = one shared subobject in the most derived class
  • Most-derived constructor initializes the virtual base directly
  • Access goes via vtable offsets; downcasts need dynamic_cast
  • Prefer interface-only MI or composition; iostream is the real case
Q41

How does operator<=> (the spaceship operator) change writing comparisons in C++20?

IntermediateModern C++

Answer

Before C++20, a properly comparable class needed six hand-written operators (==, !=, <, <=, >, >=), usually as boilerplate lexicographic member comparisons, and inconsistencies between them were a real bug class (a < b and b < a both true breaks std::sort with UB). C++20 introduces three-way comparison: a <=> b returns a category type, and the compiler rewrites relational expressions in terms of it. Declaring 'auto operator<=>(const T&) const = default;' generates lexicographic member-wise comparison in declaration order, and a defaulted <=> also implies a defaulted ==, giving you all six operators from one line.

The return categories encode semantics: std::strong_ordering (equal values are substitutable, e.g. integers), std::partial_ordering (some values are unordered, e.g. floating point with NaN: a <=> NaN yields partial_ordering::unordered, and all of <, >, == are false), and std::weak_ordering (equivalent but distinguishable, e.g. case-insensitive strings). When defaulting, the compiler computes the weakest category among members. Details that show depth: == is deliberately separate from <=> for performance, string equality checks lengths first and bails in O(1), while a <=> would compare lexicographically, so the rewrite rules use == for equality and <=> only for relational operators; defining <=> manually without defaulting == means == is not generated.

Rewritten candidates also add reversed forms: the compiler tries b <=> a with the result negated, so heterogeneous comparisons (Widget vs int) need only one direction. Member ordering matters: default <=> compares members in declaration order, so a struct whose 'importance' order differs from declaration order needs a manual implementation. In interviews, the practical claim to make: every value type you write in 2026 should carry 'auto operator<=>(const T&) const = default;' unless there is a documented reason for custom semantics, and containers, sorting, and maps all pick it up automatically.

#include <compare>
#include <string>
#include <set>

struct Version {
  int major, minor, patch;
  // all six operators, lexicographic, one line:
  auto operator<=>(const Version&) const = default;
};

struct CaseInsensitive {
  std::string s;
  std::weak_ordering operator<=>(const CaseInsensitive& o) const {
    for (size_t i = 0; i < s.size() && i < o.s.size(); ++i) {
      char a = std::tolower(s[i]), b = std::tolower(o.s[i]);
      if (a != b) return a <=> b;
    }
    return s.size() <=> o.s.size();   // equivalent, not equal: weak
  }
  bool operator==(const CaseInsensitive& o) const {
    return (*this <=> o) == 0;        // == NOT auto-generated here
  }
};

static_assert(Version{1, 2, 3} < Version{1, 3, 0});

Key Points

  • Defaulted <=> + implied == replaces six operators
  • strong/weak/partial ordering encode comparison semantics
  • Float NaN yields partial_ordering::unordered
  • == stays separate so equality can short-circuit on size
Q42

std::thread vs std::jthread: what problems does jthread fix, and how does cooperative cancellation work?

IntermediateConcurrency

Answer

std::thread has a destructor landmine: if a joinable thread object is destroyed, the process calls std::terminate. That means every exit path from a scope owning a std::thread must call join() or detach(), and an exception thrown between construction and join crashes the program, precisely the kind of cleanup problem RAII exists to solve, which std::thread inexplicably did not. std::jthread (C++20) fixes it: its destructor requests stop, then joins, so early returns and exceptions are safe by default. The second fix is built-in cooperative cancellation via stop tokens.

A jthread owns a std::stop_source; calling request_stop() flips a shared flag, and the thread function can either accept a std::stop_token as its first parameter (jthread passes it automatically) and poll stop_requested() at loop boundaries, or register a std::stop_callback to run when stop is requested. This is cooperative: nothing preempts the thread, it must check, which is the correct design because asynchronous thread killing (pthread_cancel style) leaves locks held and invariants broken. Integration goes deeper: the condition_variable_any overloads taking a stop_token wake the waiter when stop is requested, solving the classic 'thread blocked on a CV never sees the shutdown flag' bug without hand-rolled wake sentinels.

Points that earn seniority credit: detach() is almost always wrong in production (the process can exit while the detached thread touches destroyed globals; give threads owned lifetimes and join them); pass arguments carefully, std::thread copies its args into internal storage, so passing a reference requires std::ref and passing raw pointers to stack locals is a lifetime bug; and thread creation costs microseconds plus a stack (default 1-8 MB), so hot paths use thread pools rather than per-task threads. In codebases still on C++17, the standard workaround is a five-line RAII joining wrapper, which interviewers sometimes ask you to write on the spot.

#include <thread>
#include <stop_token>
#include <chrono>
#include <iostream>

void poller(std::stop_token st) {
  using namespace std::chrono_literals;
  while (!st.stop_requested()) {          // cooperative check
    // ... poll work queue ...
    std::this_thread::sleep_for(50ms);
  }
  std::cout << "clean shutdown\n";
}

int main() {
  std::jthread t(poller);   // stop_token injected automatically

  std::stop_callback cb(t.get_stop_token(),
                        [] { std::cout << "stop requested\n"; });

  std::this_thread::sleep_for(std::chrono::milliseconds(200));
  // no explicit join needed: ~jthread() -> request_stop() -> join()
}

Key Points

  • ~thread on a joinable thread calls std::terminate; ~jthread joins
  • stop_token/stop_source give standard cooperative cancellation
  • condition_variable_any + stop_token fixes blocked-at-shutdown bugs
  • detach() in production is almost always a lifetime bug
Q43

Compare lock_guard, unique_lock, and scoped_lock. How do you structure code to avoid deadlocks?

IntermediateConcurrency

Answer

All three are RAII lock owners over std::mutex-family types, differing in capability and cost. std::lock_guard is the minimal one: locks in the constructor, unlocks in the destructor, no other operations, zero overhead, the default for simple critical sections. std::unique_lock adds flexibility at the price of a stored flag: deferred locking (std::defer_lock), try_lock and timed try (with timed_mutex), manual unlock()/lock() mid-scope, and movability; it is required by std::condition_variable::wait, which must unlock during the wait and relock on wake. std::scoped_lock (C++17) is the multi-mutex answer: it takes any number of mutexes and acquires them with the same deadlock-avoiding algorithm as std::lock (try-and-back-off), so locking {m1, m2} in one place and {m2, m1} in another cannot deadlock. With one mutex it degenerates to lock_guard, which is why many teams just default to scoped_lock everywhere. Deadlock discipline you should recite: the four conditions require circular waiting, so break the cycle.

Establish a global lock ordering (document it; assert it in debug builds with a lock-rank checker); acquire multiple locks only via scoped_lock; keep critical sections tiny and never call unknown code (callbacks, virtual functions, logging that itself locks) while holding a lock; and prefer designs that need one lock at a time, message passing, or per-shard locks keyed by hash to reduce cross-lock interactions. Diagnosis tools to name: TSan detects lock-order inversions even when the deadlock has not fired yet ('WARNING: ThreadSanitizer: lock-order-inversion'), and a hung process's stacks (gdb thread apply all bt) show two threads each blocked in pthread_mutex_lock holding the other's mutex. Bonus depth: std::shared_mutex for read-heavy data (shared_lock for readers, unique ownership for writers), std::recursive_mutex as a smell that usually indicates unclear ownership, and the C++20 std::counting_semaphore/std::latch/std::barrier family for coordination that mutexes express poorly.

#include <mutex>
#include <shared_mutex>

class Account {
  mutable std::mutex m_;
  long balance_ = 0;
public:
  void deposit(long amt) {
    std::lock_guard<std::mutex> lk(m_);      // simple section
    balance_ += amt;
  }
  // transfer locks TWO mutexes: scoped_lock avoids deadlock
  friend void transfer(Account& from, Account& to, long amt) {
    std::scoped_lock lk(from.m_, to.m_);     // any order at call sites
    from.balance_ -= amt;
    to.balance_ += amt;
  }
};

class Cache {
  mutable std::shared_mutex m_;
  int value_ = 0;
public:
  int read() const {
    std::shared_lock lk(m_);   // many concurrent readers
    return value_;
  }
  void write(int v) {
    std::unique_lock lk(m_);   // exclusive writer
    value_ = v;
  }
};

Key Points

  • lock_guard: minimal; unique_lock: CV waits + deferred/timed
  • scoped_lock acquires multiple mutexes deadlock-free
  • Global lock ordering + tiny sections + no callbacks under locks
  • TSan reports lock-order-inversion before it ever deadlocks
Q44

What does std::atomic actually guarantee, and why is a plain int (or volatile int) not enough for a shared counter?

IntermediateConcurrency

Answer

Concurrent unsynchronized access to a non-atomic variable, where at least one access writes, is a data race, and a data race is undefined behavior, not 'you might read a stale value' but full UB the optimizer exploits. Concretely: counter++ on a plain int compiles to load, increment, store; two threads interleaving lose increments. Worse, the compiler may hoist the load out of a loop entirely (while (!done) {} becomes if (!done) infinite-loop, since a non-atomic done 'cannot' change concurrently), keep the variable in a register forever, or tear multi-word writes. volatile fixes none of this: it forbids the compiler from eliding accesses (its actual purpose is memory-mapped I/O) but provides no atomicity, no cache-coherence ordering guarantees between other variables, and races on volatile are still UB; 'volatile is for hardware, atomics are for threads' is the line to say. std::atomic<T> guarantees three things: atomicity (each load/store/RMW is indivisible, no torn reads), visibility (a value written becomes visible to other threads' subsequent reads), and ordering (by default sequentially consistent, all threads agree on one global interleaving of seq_cst operations).

Operations: load, store, exchange, fetch_add/fetch_sub and operator++ for counters, and compare_exchange_weak/strong for read-modify-write of arbitrary logic. is_lock_free() tells you whether the hardware does it without a hidden mutex; atomic<int> and atomic<T*> are lock-free everywhere that matters, while a 32-byte struct in atomic falls back to locks. C++20 additions worth naming: atomic wait/notify_one/notify_all (futex-backed blocking on an atomic value, building block for fast semaphores), std::atomic_ref for atomic access to existing non-atomic objects, and std::atomic<std::shared_ptr<T>> replacing the deprecated free-function atomics. Guidance: counters and flags with atomics are fine; more than two atomics coordinating state is where you switch back to a mutex until you can argue the ordering formally, which is the next question.

#include <atomic>
#include <thread>
#include <vector>
#include <cassert>

std::atomic<long> hits{0};
std::atomic<bool> done{false};

void worker() {
  while (!done.load(std::memory_order_relaxed)) {
    hits.fetch_add(1, std::memory_order_relaxed);  // never lost
  }
}

int main() {
  std::vector<std::thread> pool;
  for (int i = 0; i < 4; ++i) pool.emplace_back(worker);

  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  done.store(true);              // visible to all workers
  for (auto& t : pool) t.join();

  assert(hits.load() > 0);
  static_assert(std::atomic<long>::is_always_lock_free);
  // plain 'long hits' here = data race = UB, and TSan will prove it
}

Key Points

  • Data race on non-atomics is UB; compilers hoist and cache freely
  • volatile: hardware I/O semantics, zero thread guarantees
  • atomics give atomicity + visibility + ordering (seq_cst default)
  • C++20: atomic wait/notify, atomic_ref, atomic<shared_ptr>
Q45

How do condition variables work, why do you always wait with a predicate, and what is the lost wakeup problem?

IntermediateConcurrency

Answer

A std::condition_variable lets threads sleep until some condition over shared state becomes true. The protocol has three parts that must all be present: a mutex protecting the shared state, the condition itself (a predicate over that state), and the CV for blocking. The waiter takes a std::unique_lock, then calls cv.wait(lock, predicate); wait atomically releases the mutex and blocks, and on wakeup reacquires the mutex before rechecking.

The notifier mutates the state under the same mutex, then calls notify_one() or notify_all(). Why the predicate form is mandatory, the two failure modes interviewers want named: spurious wakeups, the OS may wake a waiter with no notify having occurred (an explicit allowance that makes CVs implementable efficiently on all platforms), so a bare wait() that assumes wakeup implies readiness is wrong; and lost wakeups, if the waiter checks the condition, finds it false, and the notifier fires notify between that check and the block, the notification evaporates and the waiter sleeps forever. The predicate overload, equivalent to while (!pred()) wait(lock), solves both: the check happens under the mutex, and wait's atomic unlock-and-block closes the race window.

Correctness details with production consequences: the notifier must modify the shared state while holding the mutex (mutating without it reopens the lost-wakeup race), though the notify call itself may happen after unlocking, a micro-optimization avoiding a pointless wake-then-block-on-mutex; notify_one for single-consumer handoff, notify_all when waiters have different predicates or shutdown must wake everyone; wait_for/wait_until return cv_status::timeout for bounded waits. C++20 upgrades to mention: condition_variable_any::wait(lock, stop_token, pred) integrates cancellation, and atomic wait/notify plus std::counting_semaphore/std::latch/std::barrier cover the simple signaling patterns people previously misbuilt from CVs. The classic exercise, a bounded blocking queue, is worth having fluent: two CVs (not_empty, not_full), push waits on not_full and notifies not_empty, pop the reverse.

#include <condition_variable>
#include <mutex>
#include <queue>
#include <optional>

template <typename T>
class BlockingQueue {
  std::mutex m_;
  std::condition_variable not_empty_;
  std::queue<T> q_;
  bool closed_ = false;
public:
  void push(T v) {
    { std::lock_guard lk(m_); q_.push(std::move(v)); } // state under lock
    not_empty_.notify_one();                            // notify after
  }
  std::optional<T> pop() {
    std::unique_lock lk(m_);
    not_empty_.wait(lk, [&] { return !q_.empty() || closed_; });
    if (q_.empty()) return std::nullopt;   // woke for shutdown
    T v = std::move(q_.front()); q_.pop();
    return v;
  }
  void close() {
    { std::lock_guard lk(m_); closed_ = true; }
    not_empty_.notify_all();               // wake EVERY waiter
  }
};

Key Points

  • wait(lock, pred) = while(!pred()) wait: handles spurious wakeups
  • Mutate state under the mutex or you reopen lost-wakeup races
  • notify_one for handoff; notify_all for shutdown/mixed predicates
  • C++20: CV waits with stop_token; semaphores/latches for simple cases
Q46

How do you structure a modern CMake project: targets, find_package, FetchContent, and vcpkg or Conan?

IntermediateTooling

Answer

Modern CMake (3.15+, and most teams now sit on 3.28+ for C++20 modules support) is target-based: you describe libraries and executables as targets, attach requirements to them, and let usage requirements propagate, no global include_directories or CMAKE_CXX_FLAGS mutation. The verbs that matter: add_library/add_executable create targets; target_include_directories, target_link_libraries, target_compile_features, target_compile_definitions attach properties with PUBLIC (users need it too), PRIVATE (only this target), or INTERFACE (only users, e.g. header-only libraries) visibility. target_link_libraries(app PRIVATE fmt::fmt) pulls in fmt's includes, definitions, and link line transitively, that propagation is the entire point. target_compile_features(lib PUBLIC cxx_std_20) states the language level as a requirement instead of hardcoding flags. Dependencies come in three tiers. find_package(OpenSSL REQUIRED) locates something already installed, using either CMake's find modules or the package's own exported config; you then link the imported target (OpenSSL::SSL).

FetchContent downloads and builds a dependency from source inside your build (FetchContent_Declare with a git tag, FetchContent_MakeAvailable), zero setup for consumers, ideal for small or header-only libraries like fmt, Catch2, GoogleTest, but it recompiles per build tree and vendors transitively. Package managers scale past that: vcpkg in manifest mode (a vcpkg.json listing dependencies, integrated via -DCMAKE_TOOLCHAIN_FILE=.../vcpkg.cmake) or Conan 2 (conanfile.txt/py, profiles for cross-building) give you prebuilt binary caches, lockfiles, and reproducibility; both feed straight into find_package. The other pieces of a credible 2026 setup: CMakePresets.json committing configure/build/test presets so IDEs and CI run identical builds; Ninja as the generator; ccache; CMAKE_EXPORT_COMPILE_COMMANDS=ON producing compile_commands.json for clangd and clang-tidy; CTest wired via add_test or gtest_discover_tests; and separate Debug (with sanitizers) and RelWithDebInfo presets. Anti-patterns to call out on sight: GLOBbing sources (new files silently missed until rerun), and mutating global flags instead of per-target properties.

cmake_minimum_required(VERSION 3.28)
project(orderbook LANGUAGES CXX)

add_library(core src/engine.cpp src/matching.cpp)
target_compile_features(core PUBLIC cxx_std_20)
target_include_directories(core PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)

find_package(fmt CONFIG REQUIRED)          # from vcpkg/conan/system
target_link_libraries(core PUBLIC fmt::fmt)

include(FetchContent)                       # small dep from source
FetchContent_Declare(Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG v3.7.1)
FetchContent_MakeAvailable(Catch2)

add_executable(tests test/engine_test.cpp)
target_link_libraries(tests PRIVATE core Catch2::Catch2WithMain)

include(CTest)
add_test(NAME engine COMMAND tests)

# configure: cmake --preset debug   (presets in CMakePresets.json)
# build:     cmake --build --preset debug

Key Points

  • Targets + PUBLIC/PRIVATE/INTERFACE propagation, never global flags
  • find_package for installed, FetchContent for vendored-from-source
  • vcpkg manifest / Conan 2 for binary caching and lockfiles
  • Presets + Ninja + compile_commands.json is the standard loop
Q47

How do you test C++ in practice: GoogleTest fixtures, parameterized tests, death tests, and what belongs in CI?

IntermediateTesting

Answer

GoogleTest remains the dominant framework (Catch2 second, doctest for compile-time-sensitive codebases). Basics: TEST(SuiteName, CaseName) defines a case; EXPECT_* macros record failures and continue, ASSERT_* abort the current test on failure, use ASSERT for preconditions whose failure makes the rest meaningless (ASSERT_NE(ptr, nullptr) before dereferencing). EXPECT_EQ/NE/LT, EXPECT_THAT with matchers (from GMock: testing::ElementsAre, HasSubstr, AllOf) give readable failure output; EXPECT_DOUBLE_EQ and EXPECT_NEAR handle floating point, never EXPECT_EQ on doubles.

Fixtures: TEST_F(MyFixture, Case) runs SetUp()/TearDown() around each case with a fresh fixture object per test, which is the isolation guarantee people forget, member state does not leak across tests. Parameterized tests (TEST_P plus INSTANTIATE_TEST_SUITE_P) run one body over a value table, the right tool for parser edge cases and boundary matrices; typed tests (TYPED_TEST) run one body over a type list, ideal for testing a container template against int, string, move-only types. Death tests (EXPECT_DEATH(call, "regex")) verify that code aborts, the way to test assert()s and CHECK-failures.

GMock supplies interface mocking: MOCK_METHOD, EXPECT_CALL with cardinalities and argument matchers; in C++ this pushes design toward small virtual interfaces injected via constructor, or template parameters for compile-time seams. What CI must run for a serious C++ codebase in 2026: the suite under ASan+UBSan (catching memory bugs tests would otherwise 'pass' over), a separate TSan job for concurrent components, a Debug and a Release build (optimizers expose UB that Debug hides), clang-tidy as a gate, and coverage via --coverage/llvm-cov if the team tracks it. Fuzzing with libFuzzer (LLVMFuzzerTestOneInput plus -fsanitize=fuzzer,address) belongs on anything parsing untrusted bytes. CTest orchestrates with ctest --output-on-failure, and gtest_discover_tests registers each case individually so CI reports and parallelizes per-test.

#include <gtest/gtest.h>
#include <gmock/gmock.h>

class OrderBookTest : public ::testing::Test {
protected:
  void SetUp() override { book.addSymbol("RELIANCE"); }
  OrderBook book;                 // fresh per test case
};

TEST_F(OrderBookTest, RejectsCrossedSpread) {
  ASSERT_TRUE(book.hasSymbol("RELIANCE"));   // abort if not
  auto err = book.quote("RELIANCE", /*bid*/101.0, /*ask*/100.0);
  EXPECT_EQ(err, QuoteError::Crossed);
}

class ParseCase : public ::testing::TestWithParam<const char*> {};
TEST_P(ParseCase, RejectsMalformed) {
  EXPECT_FALSE(parsePort(GetParam()).has_value());
}
INSTANTIATE_TEST_SUITE_P(Bad, ParseCase,
    ::testing::Values("", "-1", "99999", "80x"));

TEST(Invariants, DiesOnNullHandle) {
  EXPECT_DEATH(processHandle(nullptr), "handle != nullptr");
}

Key Points

  • ASSERT aborts the test, EXPECT continues; fixtures are per-test fresh
  • TEST_P for value tables, TYPED_TEST for type lists
  • Death tests verify assert/CHECK behavior
  • CI: ASan+UBSan suite, TSan job, Debug AND Release, clang-tidy gate
Q48

A large C++ codebase takes 40 minutes to build. What are the real levers for cutting compile times?

IntermediateTooling

Answer

Attack it empirically, then structurally. Measure first: Clang's -ftime-trace emits per-file flame graphs (view in chrome://tracing or Speedscope) showing whether time goes to parsing headers, instantiating templates, or the optimizer; ninja -d stats and tools like ClangBuildAnalyzer aggregate across the build and rank the most expensive headers and templates. The usual findings and their fixes, in rough order of payoff.

Header hygiene: heavyweight headers (<regex>, <iostream>, Boost umbrellas) included transitively everywhere; fix with include-what-you-use, forward declarations for types used only by pointer/reference, and PIMPL to strip private members (and their includes) out of public headers, which also cuts rebuild cascades when internals change. Template instantiation: the same std::vector<Foo>, fmt instantiations, or handwritten template machinery compiled in 300 TUs; extern template declarations centralize the common instantiations into one .cpp. Precompiled headers: CMake's target_precompile_headers for the stable std/third-party set gives 20-40% on parse-heavy code cheaply.

Caching and distribution: ccache (or sccache with cloud storage) makes rebuilds and CI branch builds nearly free; distributed builds (distcc, Incredibuild, or Bazel/Buck2 remote execution) parallelize beyond one machine. Linker: switching to lld or mold routinely cuts link times by 5-10x over GNU ld on big binaries, the single cheapest win in many projects; split DWARF (-gsplit-dwarf) shrinks what the linker must copy. Unity/jumbo builds batch TUs to amortize header parsing, at the cost of ODR-collision risk and worse incrementality.

Structurally: reduce coupling so a header edit does not rebuild the world (layered targets, interface stability), and C++20 modules, where the toolchain is ready (CMake 3.28+, Ninja, MSVC/Clang), eliminate repeated header parsing outright, import std alone measurably beats including <vector> and friends per TU. Also mundane but real: make sure Debug CI is not building with -O2, that Ninja replaced Make, and that antivirus is not scanning the build directory on Windows.

# 1. Find where the time goes (Clang):
cmake -DCMAKE_CXX_FLAGS=-ftime-trace -G Ninja ..
ninja 2>&1 | tail -1 && ClangBuildAnalyzer --all . capture.bin \
  && ClangBuildAnalyzer --analyze capture.bin   # ranks headers/templates

# 2. Cheap wins in CMake:
# target_precompile_headers(core PRIVATE <vector> <string> <fmt/core.h>)
# set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
# add_link_options(-fuse-ld=mold)          # 5-10x faster links

# 3. Centralize hot instantiations:
# header: extern template class Matrix<float>;
# one .cpp: template class Matrix<float>;

# 4. Verify includes are earned:
iwyu_tool -p compile_commands.json src/ | fix_includes.py

Key Points

  • Measure with -ftime-trace/ClangBuildAnalyzer before touching anything
  • IWYU + forward decls + PIMPL kill header cascades
  • PCH, extern template, ccache, mold: the standard cheap wins
  • C++20 modules and import std remove repeated parsing entirely
Q49

Explain the C++ memory model: sequential consistency vs acquire-release vs relaxed, and what false sharing does to all of it.

AdvancedConcurrency

Answer

The C++11 memory model defines which reorderings of memory operations, by the compiler and the CPU, are observable across threads, expressed through memory_order arguments on atomic operations. memory_order_seq_cst (the default) is the strongest: all seq_cst operations across all threads appear in one single total order everyone agrees on. It is the only ordering under which the classic Dekker pattern (two threads each store their flag then load the other's) forbids both loads seeing false. Cost: on x86 a seq_cst store compiles to an XCHG or MFENCE-protected store; on ARM it needs heavier barriers.

Acquire-release is the workhorse pairing: a store with memory_order_release publishes, everything written before it becomes visible to, and only to, a thread whose memory_order_acquire load reads that stored value; that pairing creates a synchronizes-with edge and hence happens-before. It is exactly the semantics of unlocking and locking a mutex, and it is what a producer-consumer flag or an SPSC ring buffer needs: release on the write index, acquire on the read. On x86 acquire/release are nearly free (the hardware's TSO already provides them; only compiler reordering is constrained), which is why 'seq_cst everywhere' mostly hurts on ARM/POWER. memory_order_relaxed guarantees atomicity and per-variable modification order but no cross-variable ordering: correct for statistics counters and reference-count increments (decrements need acq_rel, the reason shared_ptr's decrement is more expensive than its increment).

What you must be able to say about consume: memory_order_consume exists for dependency-ordered publication (RCU-style), but compilers promote it to acquire; do not use it. False sharing is orthogonal but always paired in interviews: two unrelated atomics (or an atomic and hot plain data) on the same 64-byte cache line force cross-core cache-line ping-pong, each core's write invalidates the other's copy, and a 'lock-free' counter array becomes slower than a mutex. Fix with alignas(std::hardware_destructive_interference_size) padding between per-thread slots. Tools: perf c2c finds false sharing on Linux; TSan validates the logical ordering.

#include <atomic>
#include <new>

// SPSC publication: release/acquire pairing
struct Slot { int payload = 0; };
Slot slot;
std::atomic<bool> ready{false};

void producer() {
  slot.payload = 42;                              // A
  ready.store(true, std::memory_order_release);   // publishes A
}
void consumer() {
  while (!ready.load(std::memory_order_acquire)) {}
  int v = slot.payload;    // guaranteed 42: happens-before established
  (void)v;
}

// false sharing fix: one counter per cache line
struct alignas(std::hardware_destructive_interference_size) PaddedCount {
  std::atomic<long> n{0};
};
PaddedCount perThread[8];   // no cross-core line ping-pong

Key Points

  • release-store/acquire-load creates happens-before; mutexes use it
  • relaxed: atomicity only; right for counters, wrong for flags
  • seq_cst adds a global total order; costly on ARM, near-free reads on x86
  • False sharing: pad with hardware_destructive_interference_size
Q50

How does compare_exchange work, weak vs strong, and what is the ABA problem in lock-free structures?

AdvancedConcurrency

Answer

compare_exchange is the primitive from which lock-free algorithms are built: atomic.compare_exchange_strong(expected, desired) atomically compares the atomic's value with expected; if equal, it stores desired and returns true; if not, it loads the current value into expected (an in-out parameter, the detail people miss) and returns false. The canonical usage is a CAS loop: load the current value, compute the desired next value, attempt the exchange, and on failure retry with the freshly-loaded value that the failed call conveniently wrote into expected. compare_exchange_weak may fail spuriously, returning false even when the values matched, because on LL/SC architectures (ARM, RISC-V) the underlying load-linked/store-conditional can be interrupted by cache traffic; weak maps directly to one LL/SC pair, while strong wraps it in a retry loop. Rule: inside your own retry loop use weak (the loop absorbs spurious failures for free); for a single-shot attempt where a spurious failure would be treated as a real conflict, use strong.

Both take separate success and failure memory orderings, typically acq_rel on success, acquire (or relaxed) on failure. The ABA problem: CAS compares values, not histories. In a lock-free stack, thread 1 reads head A and prepares to CAS head from A to A->next (B).

It stalls. Thread 2 pops A, pops B, then pushes a node that reuses A's address (allocators love reusing hot addresses). Thread 1 resumes: head equals A again, the CAS succeeds, and it installs B, which is freed memory; the stack is corrupted with no diagnostic.

The mitigations you should enumerate: tagged pointers, pack a generation counter alongside the pointer (a 64-bit counter+index pair via 128-bit CAS, cmpxchg16b, or steal the unused high bits of x86-64 addresses), so reuse changes the tag and the CAS fails; deferred reclamation, never reuse memory while any thread may still hold a reference, via hazard pointers or epoch-based reclamation (both standardized as std::hazard_pointer and RCU in C++26), or simply garbage-collected node pools. Honest closing note interviewers respect: lock-free is a latency-tail and progress-guarantee tool, not a throughput cheat code; a well-tuned mutex beats a naive lock-free structure, and the correct-by-construction option is using a proven library (folly, boost::lockfree, moodycamel) rather than hand-rolling.

#include <atomic>

template <typename T>
class LockFreeStack {          // teaching version: has ABA unless nodes
  struct Node { T v; Node* next; };  // are never reused (pool/epoch)
  std::atomic<Node*> head_{nullptr};
public:
  void push(T v) {
    Node* n = new Node{std::move(v), head_.load(std::memory_order_relaxed)};
    // weak inside a loop: spurious failure just retries
    while (!head_.compare_exchange_weak(
        n->next, n,
        std::memory_order_release,     // success: publish node
        std::memory_order_relaxed)) {} // failure: n->next refreshed
  }
  bool pop(T& out) {
    Node* n = head_.load(std::memory_order_acquire);
    while (n && !head_.compare_exchange_weak(
        n, n->next,
        std::memory_order_acq_rel, std::memory_order_acquire)) {}
    if (!n) return false;
    out = std::move(n->v);
    // delete n;  // UNSAFE without hazard pointers / epochs: ABA + UAF
    return true;
  }
};

Key Points

  • expected is in-out: failure refreshes it for the retry
  • weak for CAS loops (LL/SC spurious failure), strong for one-shots
  • ABA: same value, different history; CAS cannot tell
  • Fixes: tagged pointers, hazard pointers, epochs (RCU in C++26)
Q51

When do custom allocators and std::pmr actually help, and how does a monotonic arena work?

AdvancedPerformance

Answer

General-purpose malloc pays for generality: thread-safe metadata, size-class management, fragmentation control. Custom allocation wins when you know something malloc cannot: that a group of objects dies together (arena), that all objects share one size (pool/slab), or that allocation happens on a latency-critical path where a syscall or lock is unacceptable. The classical C++03 allocator template parameter was nearly unusable (allocator type infects the container type: vector<int, MyAlloc> is a different type from vector<int>, poisoning every API boundary).

C++17's std::pmr fixed that with runtime polymorphism: std::pmr::vector<int> is vector<int, polymorphic_allocator<int>>, one type regardless of where memory comes from, and the allocator holds a pointer to a memory_resource, an abstract interface with allocate/deallocate. The stdlib supplies the resources that cover most needs: monotonic_buffer_resource, bump-pointer allocation from a buffer you provide (stack or preallocated slab), individual deallocate is a no-op, everything is released at once when the resource is destroyed; unsynchronized_pool_resource and synchronized_pool_resource, size-class pools that fight fragmentation and lock contention. The arena pattern is the big win: a request handler or a game frame creates a monotonic resource backed by a stack buffer, every temporary container inside allocates by pointer bump (nanoseconds, perfectly cache-local, zero fragmentation), and teardown is one pointer reset, no destructor-driven free-list traffic.

This is how per-frame allocation works in game engines and per-message allocation in trading systems. Costs and gotchas for depth: polymorphic_allocator dispatches through a virtual call (usually devirtualized-or-cheap, but measure); pmr containers do not propagate the resource on move across different resources (a move can silently become an element-wise copy); the default_resource is process-global mutable state (set_default_resource), so libraries should take resources explicitly; and monotonic resources leak by design until reset, wrong for long-lived mixed-lifetime data. Alternative worth naming: replacing global malloc wholesale with jemalloc/tcmalloc/mimalloc, which is the zero-code-change fix for multithreaded allocation contention and often the first thing to try.

#include <memory_resource>
#include <vector>
#include <string>

void handleRequest(std::span<const char> payload) {
  // 64 KB on the stack backs ALL temporaries for this request
  std::byte buf[64 * 1024];
  std::pmr::monotonic_buffer_resource arena(buf, sizeof(buf));

  std::pmr::vector<std::pmr::string> tokens(&arena);  // bump-pointer
  tokens.reserve(256);
  for (auto tok : tokenize(payload))
    tokens.emplace_back(tok.begin(), tok.end());      // no malloc

  process(tokens);
}   // one shot teardown: no per-object free, arena just dies

// pool resource for mixed-size, long-lived churn:
// std::pmr::unsynchronized_pool_resource pool;
// std::pmr::map<int, Order> orders(&pool);

Key Points

  • Arenas exploit group-lifetime knowledge malloc lacks
  • pmr: one container type, resource chosen at runtime
  • monotonic_buffer_resource: bump alloc, no-op dealloc, bulk reset
  • Try jemalloc/mimalloc first: zero code change, big contention wins
Q52

What does cache-friendly C++ look like: AoS vs SoA, data-oriented design, and how do you prove a cache problem exists?

AdvancedPerformance

Answer

The performance hierarchy that dominates modern C++: an L1 hit costs ~4 cycles, L2 ~12, L3 ~40, DRAM 200+. Code whose working set streams through memory linearly gets hardware prefetching and full 64-byte cache-line utilization; code that pointer-chases eats a DRAM-latency stall per hop. That single fact explains the standard advice: vector beats list in practice for almost everything (even middle insertion benchmarks often favor vector until sizes get large, because traversal-to-the-point dominates), node-based maps lose to open-addressing flat maps (absl::flat_hash_map, boost::unordered_flat_map), and vectors of objects beat vectors of pointers-to-heap-objects.

AoS vs SoA is the layout decision: array-of-structs (vector<Particle> with pos, vel, mass, color) is natural for whole-object access, but a loop touching only pos wastes most of every cache line on unused fields; struct-of-arrays (separate vector<float> per field) makes single-field sweeps dense and, critically, vectorizable, SIMD units load 8-16 contiguous floats at a time, which is why physics engines, particle systems, and quant backtesting engines are SoA. Data-oriented design generalizes this: organize data by access pattern, not by object taxonomy; the entity-component-system architecture in game engines is DoD applied wholesale. Other levers with production impact: member ordering to eliminate padding (order members largest-first; static_assert the sizeof), keeping hot and cold fields in separate structs (error strings and debug info out of the hot struct), avoiding false sharing between threads, and branchless/sorted processing so the predictor and prefetcher see patterns.

Proving it, since interviewers reject vibes: perf stat -e cache-misses,LLC-load-misses,instructions,cycles gives IPC and miss rates (IPC well under 1 on a memory-bound loop is the signature); perf record/report attributes misses to lines; perf c2c catches false sharing; and Intel VTune's memory-access analysis names the offending data structures. The honest caveat that lands well: DoD trades abstraction for mechanical sympathy, apply it to the measured hot 5%, keep the rest of the codebase optimized for humans.

#include <vector>
#include <cstddef>

// AoS: natural, but a pos-only sweep wastes cache-line bytes
struct ParticleAoS { float px, py, pz, vx, vy, vz, mass; int id; };

// SoA: dense, prefetch-friendly, auto-vectorizable
struct Particles {
  std::vector<float> px, py, pz, vx, vy, vz, mass;
  std::vector<int> id;

  void integrate(float dt) {
    const size_t n = px.size();
    for (size_t i = 0; i < n; ++i) {   // compiles to SIMD at -O2/-O3
      px[i] += vx[i] * dt;
      py[i] += vy[i] * dt;
      pz[i] += vz[i] * dt;
    }
  }
};

// prove it, don't guess:
//   perf stat -e cycles,instructions,cache-misses ./sim
//   perf c2c record ./sim && perf c2c report   (false sharing)

Key Points

  • DRAM is 50x L1; layout beats micro-optimization
  • SoA densifies single-field sweeps and unlocks SIMD
  • Flat/open-addressing containers beat node-based in practice
  • perf stat IPC + c2c turn 'cache problem' from vibe to fact
Q53

How do C++20 coroutines work under the hood: promise_type, co_await, and where does the coroutine frame live?

AdvancedModern C++

Answer

C++20 coroutines are a compiler transformation, not a library: any function containing co_await, co_yield, or co_return is rewritten into a resumable state machine. The compiler allocates a coroutine frame (by default via operator new, this is the part latency-sensitive people care about) holding the function's parameters, locals that live across suspension points, and bookkeeping; suspension saves the current state into the frame and returns control, resumption jumps back in via std::coroutine_handle<>::resume(). The customization protocol is the interview core.

The coroutine's return type must expose a promise_type, whose members script the lifecycle: get_return_object() manufactures what the caller receives (your Task/Generator wrapper owning the coroutine_handle); initial_suspend() returns std::suspend_always for lazy coroutines (generators, most task types) or suspend_never for hot-start; final_suspend() (noexcept mandatory) usually suspends so the owner controls frame destruction, and in task types its awaiter resumes the continuation, the awaiting parent, implementing symmetric transfer; yield_value(v) powers co_yield; return_value/return_void handle co_return; unhandled_exception() typically stores std::current_exception() for rethrow on the consumer side. co_await expr itself expands through an awaiter with three calls: await_ready() (skip suspension if the result is already available), await_suspend(handle) (stash the handle wherever will resume it: an epoll reactor, a timer wheel, another coroutine; returning a coroutine_handle here performs symmetric transfer, tail-resuming without growing the stack), and await_resume() (produce the awaited value when resumed). Practical facts that separate readers from users: the frame allocation can often be elided (HALO) when the coroutine's lifetime is provably enclosed, but you must not rely on it, hence custom operator new on the promise for pooled frames in low-latency code; dangling references are the top bug class, a coroutine capturing a parameter by reference outlives its caller's frame (lambda-coroutines capturing anything are notoriously unsafe); and C++20 shipped no task type, so production code uses std::generator (C++23) for synchronous yielding, and cppcoro/folly::coro/Boost.Cobalt or the C++26 std::execution ecosystem for async tasks. Coroutines shine for async I/O and parsers/state machines; they are a control-flow tool, not a parallelism tool.

#include <coroutine>
#include <optional>

template <typename T>
class Generator {
public:
  struct promise_type {
    T current;
    Generator get_return_object() {
      return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
    }
    std::suspend_always initial_suspend() { return {}; }  // lazy
    std::suspend_always final_suspend() noexcept { return {}; }
    std::suspend_always yield_value(T v) { current = v; return {}; }
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
  };

  std::optional<T> next() {
    if (h_.done()) return std::nullopt;
    h_.resume();
    return h_.done() ? std::nullopt : std::optional<T>{h_.promise().current};
  }
  ~Generator() { if (h_) h_.destroy(); }   // owner destroys the frame
private:
  explicit Generator(std::coroutine_handle<promise_type> h) : h_(h) {}
  std::coroutine_handle<promise_type> h_;
};

Generator<int> fib() {
  int a = 0, b = 1;
  while (true) { co_yield a; int t = a + b; a = b; b = t; }
}

Key Points

  • co_await/co_yield trigger a state-machine rewrite + heap frame
  • promise_type scripts lifecycle; awaiter scripts each suspension
  • await_suspend returning a handle = symmetric transfer, no stack growth
  • Reference captures into coroutines are the top dangling-bug source
Q54

C++20 modules promised the end of headers. How do they work, and why is adoption still partial in 2026?

AdvancedCompilation Model

Answer

A module interface unit (export module net; followed by export declarations) is compiled once into a binary module interface (BMI), a serialized AST; importers (import net;) load that instead of re-parsing text. That kills the header model's chronic costs: no more parsing <vector> thousands of times per build, no macro leakage into or out of the module (macros do not cross module boundaries, ending include-order fragility), no include guards, and strong ownership of names (non-exported entities are genuinely invisible, better than unnamed namespaces because it holds at the ABI level too). import std; (standardized in C++23, shipping in current MSVC, and in recent libc++/libstdc++ toolchains) imports the entire standard library faster than including a handful of individual headers. Module partitions (export module net:tcp;) structure large modules internally; header units (import <vector>;) exist as a migration bridge with mixed toolchain support.

Why adoption lagged, and this candid list is what the question is really testing: the build-system problem was genuinely hard, imports create a compilation DAG between TUs (the interface must compile before importers), which broke the decades-old 'compile all TUs in parallel, then link' model; CMake only stabilized C++20 module support in 3.28 (with Ninja and MSVC/Clang first), and the P1689 dependency-scanning protocol between compilers and build systems took years to converge. BMIs are compiler-, version-, and flag-specific, so they cannot be shipped like headers; package managers must distribute module interface sources and build them per-consumer configuration, which vcpkg/Conan support is still smoothing out. Tooling trailed: clangd, clang-tidy, coverage, and distributed-build caches all needed rework for BMI awareness, and mixed header/module builds (every real migration) hit the awkward seams, like a header included both traditionally and as part of a module.

Third-party libraries mostly still ship headers, so big codebases run hybrid: modules for first-party code on teams with modern toolchains (notably MSVC shops, whose support matured earliest), headers at the ecosystem boundary. The 2026 interview take: modules are real and worth adopting for new first-party code on CMake 3.28+/Ninja with a current compiler, with measured full-build wins that grow with how header-heavy the code was, but 'delete all headers' remains a multi-year ecosystem transition, not a flag flip.

// math.cppm: module interface unit
export module math;

import std;                       // C++23: whole stdlib, one import

export namespace math {
  double mean(const std::vector<double>& xs) {
    double s = 0;
    for (double x : xs) s += x;
    return xs.empty() ? 0.0 : s / xs.size();
  }
}

namespace math {
  double internalHelper();        // NOT exported: invisible to importers
}

// app.cpp
import math;                      // loads the BMI, no text parsing
// int main() { return math::mean({1,2,3}) > 0 ? 0 : 1; }

// CMake >= 3.28 (Ninja):
// target_sources(mathlib PUBLIC FILE_SET CXX_MODULES FILES math.cppm)

Key Points

  • BMI = compiled interface; importers skip parsing entirely
  • No macro leakage; non-exported names truly invisible
  • Imports create a build DAG: the hard build-system problem
  • BMIs are not distributable; hybrid header/module is the 2026 norm
Q55

What does std::function cost, and how does type erasure actually work under the hood?

AdvancedPerformance

Answer

std::function<R(Args...)> is type erasure: it stores any callable with a matching signature behind one concrete type, hiding the callable's real type. The mechanism is worth being able to sketch: internally it holds storage plus a pointer to a manager/invoker (either a hand-rolled vtable of function pointers or a templated wrapper class with virtual-like dispatch); construction from a lambda instantiates an invoker template for that exact lambda type, and calling goes through an indirect call to it. Costs, in order of pain: allocation, std::function has small buffer optimization (typically 16-32 bytes of inline storage, implementation-specific), so a lambda capturing two pointers stays inline, but exceed the buffer and construction heap-allocates, a real problem when functions are created per event on hot paths; indirect dispatch, every call is an opaque indirect call the optimizer cannot inline through, same penalty class as virtual functions; copyability tax, std::function requires copyable callables, so a lambda capturing a unique_ptr simply does not compile into it, which is why C++23 added std::move_only_function (no copy requirement, and const/noexcept/ref qualifiers on the signature actually enforced, fixing a const-correctness hole where const std::function happily invokes mutable state).

The alternatives you are expected to weigh: templates (template<class F> void forEach(F f)) monomorphize, fully inline, and cost nothing at runtime, at the price of code in headers and one instantiation per callable, the right default for library-internal callbacks; function pointers when there is no state; std::function_ref (C++26, and present in many codebases as a backport) for the extremely common 'borrow a callable for the duration of this call' parameter, two words, no allocation ever, non-owning so it must not be stored. Decision rule that lands in interviews: templates for hot internal paths, function_ref for non-stored callback parameters, move_only_function/function for stored, type-erased callbacks at architecture boundaries (event queues, plugin APIs) where compile-time genericity would leak template machinery through an ABI.

#include <functional>
#include <memory>
#include <vector>

// 1. template: zero-cost, inlined, header-bound
template <typename F>
void forEachTick(const std::vector<double>& xs, F&& f) {
  for (double x : xs) f(x);        // inlined at each instantiation
}

// 2. stored, type-erased, move-only callables (C++23)
class EventQueue {
  std::vector<std::move_only_function<void()>> q_;
public:
  void post(std::move_only_function<void()> task) {
    q_.push_back(std::move(task));
  }
  void drain() { for (auto& t : q_) t(); q_.clear(); }
};

void demo(EventQueue& q) {
  auto res = std::make_unique<int>(7);
  q.post([r = std::move(res)] { /* use *r */ });  // move-only capture:
}                                                  // std::function refuses

Key Points

  • Erasure = hidden vtable + possible heap alloc past the SBO
  • Indirect call blocks inlining, like virtual dispatch
  • move_only_function (C++23) fixes copyability and const holes
  • function_ref for borrowed callables; templates for hot paths
Q56

A binary works in Debug but misbehaves in Release, and you suspect an ODR violation. How do ODR bugs arise and how do you hunt them?

AdvancedCompilation Model

Answer

The One Definition Rule has a vicious clause: inline functions, templates, and class definitions may be defined in multiple translation units only if the definitions are token-for-token identical with identical meaning; if they differ, the program is ill-formed, no diagnostic required. The linker keeps one copy per symbol and silently discards the rest, so which definition 'wins' depends on link order, and the program computes with a mix of incompatible layouts and code. How this arises in real codebases: a macro changes a class definition between TUs (#ifdef DEBUG adding a member, a pack(push) pragma active in one TU, NDEBUG toggling an assert-carrying inline function), two versions of the same third-party header on the include path (the diamond dependency problem: static libs A and B each vendor different versions of fmt or protobuf), a class defined differently in two .cpps with the same name in the global namespace (two anonymous helpers both named Impl), or -D flags differing between library and consumer (the infamous _GLIBCXX_DEBUG mixing, where one TU thinks a std::string is bigger than another does).

Symptoms: sizeof mismatch corruption, virtual calls landing in the wrong function, crashes that move around with link order or vanish in Debug (different inlining hides the discarded definition). The hunt, in escalating order: -Wodr with -flto is the best tool available, GCC and Clang's LTO sees all TUs' internal representations and diagnoses mismatched types and functions ('type X violates the C++ One Definition Rule; a different type is defined in another translation unit'); ASan's ODR checker (detect_odr_violation=2, on by default for globals) catches duplicate global variable definitions at startup; nm -C over all objects finds duplicate weak symbols worth eyeballing; and diffing preprocessed output (g++ -E) of the suspect header from two TUs exposes macro-driven divergence. Prevention is architectural: one version of every dependency in the link (enforced by the package manager's lockfile), never define types in headers conditioned on macros that vary per TU, wrap file-local helpers in unnamed namespaces (giving them internal linkage, exempting them from cross-TU ODR entirely), and run an LTO+-Wodr build in CI even if you do not ship LTO.

// The classic silent killer, split across two TUs:

// a.cpp                          // b.cpp
// #define COMPACT 1              // (no COMPACT define)
// #include "record.h"          // #include "record.h"

// record.h
struct Record {
#ifdef COMPACT
  int id;                    // a.cpp thinks sizeof(Record) == 4
#else
  int id;
  char note[64];             // b.cpp thinks sizeof(Record) == 68
#endif
};
// linker keeps ONE Record-related inline fn; heap corruption follows

# Detection:
g++ -flto -Wodr a.cpp b.cpp        # LTO cross-TU type checking
# warning: type 'struct Record' violates the C++ One Definition Rule
ASAN_OPTIONS=detect_odr_violation=2 ./app   # dup globals at startup
nm -C a.o b.o | grep ' W '          # inspect weak (vague) symbols

Key Points

  • Divergent inline/class definitions across TUs: ill-formed, no diagnostic
  • Causes: per-TU macros, duplicate dependency versions, name collisions
  • -flto -Wodr and ASan's ODR checker are the real detectors
  • Unnamed namespaces exempt file-local helpers from cross-TU ODR
Q57

How do you profile and optimize a CPU-bound C++ service: perf workflow, compiler flags, LTO, and PGO?

AdvancedPerformance

Answer

Workflow first, flags second. Build RelWithDebInfo (-O2 -g) with -fno-omit-frame-pointer so stacks unwind cheaply; profile the real workload with perf record -g --call-graph=fp ./service (or dwarf unwinding when frame pointers are absent), then perf report or a flame graph (Brendan Gregg's scripts, or perf script | speedscope) to find where cycles actually go. Read perf stat before micro-reading profiles: IPC (instructions per cycle) tells you the bottleneck class, IPC near 3-4 means compute-bound (look at algorithms and vectorization), IPC under 1 means stalls, then branch-misses and cache-misses tell you which. perf annotate drops to per-instruction heat within a hot function.

For latency systems, measure tails, not means: histogram p99/p99.9 per stage, and use perf sched or eBPF tools for off-CPU time, a service can be 'slow' while burning no CPU. Now the compiler toolbox in the order I would apply it. Baseline: -O2 is the production default; -O3 adds aggressive vectorization and unrolling and must be benchmarked, it loses on some codebases via icache pressure. -march=native (or a deployment-appropriate -march=x86-64-v3) unlocks AVX2/FMA; without it the compiler targets lowest-common-denominator SIMD.

LTO (-flto, thin LTO on Clang for build speed) optimizes across TU boundaries: cross-TU inlining, dead code elimination, devirtualization with whole-program visibility, typically low-single-digit percent wins, occasionally much more on layered codebases. PGO is the heavyweight: compile with -fprofile-generate, run a representative workload, recompile with -fprofile-use; the compiler then knows real branch probabilities and hot/cold splits, getting layout, inlining, and register allocation right, 5-15% on branchy server code is typical, and BOLT (post-link binary layout optimization) stacks another few percent on large binaries by fixing icache layout from perf samples. Source-level, only after the profile names a culprit: kill allocations in loops (reserve, arenas, SSO awareness), fix data layout (previous question), hoist indirect calls, check that the hot loop actually vectorized (-Rpass=loop-vectorize on Clang, -fopt-info-vec on GCC), and use [[likely]]/[[unlikely]] or __builtin_expect only where PGO cannot reach. The discipline that reads as senior: every change is validated A/B under the same benchmark harness with variance reported, because a 3% 'win' inside run-to-run noise is how performance folklore starts.

# 1. Build for profiling (keep optimizations, keep symbols)
g++ -O2 -g -fno-omit-frame-pointer -march=x86-64-v3 svc.cpp -o svc

# 2. Where do cycles go?
perf stat -e cycles,instructions,branch-misses,cache-misses ./svc
perf record -g --call-graph=fp ./svc && perf report
perf annotate --symbol=OrderBook::match   # per-instruction heat

# 3. Cross-TU optimization
g++ -O2 -flto=auto ...                     # LTO: inline across TUs

# 4. Profile-guided optimization (two-phase)
g++ -O2 -fprofile-generate svc.cpp -o svc && ./svc < replay.bin
g++ -O2 -fprofile-use -fprofile-correction svc.cpp -o svc

# 5. Did the hot loop vectorize?
clang++ -O3 -Rpass=loop-vectorize -Rpass-missed=loop-vectorize svc.cpp

Key Points

  • perf stat IPC classifies the bottleneck before you read profiles
  • -O2 default; -O3 and -march must be benchmarked, not assumed
  • LTO: cross-TU inlining/devirt; PGO: real branch data, 5-15%
  • Off-CPU time and tail latency need their own measurement
Q58

What is an ABI break, why did GCC 5 ship a dual ABI for std::string, and what keeps C++ libraries binary-compatible?

AdvancedCompilation Model

Answer

The ABI (application binary interface) is everything two separately-compiled binaries must agree on to interoperate: type sizes and layouts, vtable format, name mangling, calling conventions, exception tables. An API break fails your compile; an ABI break compiles fine and corrupts memory at runtime, because caller and callee disagree about what bytes mean. The GCC 5 story is the canonical case study: C++11 made copy-on-write std::string non-conforming (COW cannot satisfy the new iterator-invalidation and complexity rules), so libstdc++ had to change std::string's layout (and std::list, which needed an O(1) size member).

Since std::string appears in millions of function signatures, changing it breaks every prebuilt binary. GCC's solution was a dual ABI: both implementations coexist in libstdc++, selected per-TU by the _GLIBCXX_USE_CXX11_ABI macro, distinguished at link time by mangling the new types into the __cxx11 namespace ('std::__cxx11::basic_string'). The visible symptom, which every C++ engineer eventually meets, is a linker error mentioning std::__cxx11::basic_string against a vendor .so built with the old ABI: the fix is rebuilding, or setting the macro to 0, dropping back to the old ABI.

What silently breaks ABI in your own libraries: adding/removing/reordering data members (size and offsets change), adding a first virtual function (injects a vptr) or reordering virtual functions (vtable slots shift), changing inheritance, changing template default arguments that appear in mangled names, and even changing an inline function's behavior when old binaries baked in the old body. What is safe: adding non-virtual functions, adding static members, adding new classes. The engineering toolkit for stability: PIMPL (public class holds one opaque pointer; members live in the hidden Impl, so layout never changes), pure C interfaces at .so boundaries (extern "C", the reason plugin systems and drivers export C APIs, C++ mangling and layout are compiler-version-coupled), symbol versioning (GNU version scripts), inline namespaces for versioned coexistence (the __cxx11 trick, usable by your own libraries), and CI checks with abi-compliance-checker or libabigail's abidiff on every release. This topic is also why the committee agonizes over std:: changes: std::regex remains slow partly because fixing it would be an ABI break, and 'ABI: now or never' was a genuine WG21 debate; know that context and you sound plugged in.

// The classic linker symptom against an old vendor .so:
// undefined reference to
//   Vendor::parse(std::__cxx11::basic_string<char, ...> const&)
// => the .so was built with the pre-C++11 string ABI. Rebuild, or:
//    g++ -D_GLIBCXX_USE_CXX11_ABI=0 app.cpp -lvendor

// PIMPL: layout-stable public class for a shipped .so
// widget.h  (this header NEVER changes layout)
class Widget {
public:
  Widget();
  ~Widget();               // defined in .cpp (Impl complete there)
  void refresh();
private:
  struct Impl;             // members hide here; add/remove freely
  std::unique_ptr<Impl> p_;
};

# Release gate: diff ABI vs previous version
# abidiff libwidget.so.1.2 libwidget.so.1.3   (libabigail)

Key Points

  • ABI breaks compile clean and corrupt at runtime
  • GCC 5: C++11 outlawed COW string; __cxx11 dual ABI resulted
  • Member/vtable changes break; PIMPL and C boundaries protect
  • abidiff in CI turns ABI review from folklore into a gate
Q59

What is landing in C++26 (reflection, contracts, std::execution), and how is C++ answering the memory-safety pressure?

AdvancedModern C++

Answer

Three headline features were voted into C++26. Static reflection (the P2996 line of work) lets code inspect the program at compile time: the reflection operator (^^T) yields a meta-value describing a type, queried through std::meta functions (members_of, name_of) inside consteval code, and splicers ([: refl :]) turn meta-values back into code. The killer apps are the boilerplate graveyards: serialization (JSON/protobuf bindings generated from the struct itself instead of macro or codegen toolchains), enum-to-string without hand-maintained tables or the magic_enum template tricks, ORM row mapping, and RPC stubs.

Contracts add pre(), post(), and contract_assert conditions on functions with configurable enforcement (ignore, observe-and-log, or enforce-and-terminate), standardizing what assert macros and Bloomberg-style contract libraries did ad hoc, and giving optimizers and static analyzers a machine-readable spec. std::execution (senders/receivers, P2300) is the standard async model: composable sender algorithms (then, when_all, on) over schedulers abstracting thread pools and I/O contexts, the interoperability layer the coroutine ecosystem lacked, so libraries can return senders instead of each inventing a task type. Also accepted: std::hazard_pointer and RCU for safe lock-free reclamation, and further hardening of the constexpr and ranges surface. The safety story is the strategic context every senior interview touches in 2026: government agencies (CISA/NSA memory-safety guidance) and big-tech migration announcements have put sustained pressure on C++, with Rust as the explicit comparison.

The committee's answer is incremental, not a borrow checker: erroneous behavior (C++26) reclassifies reading an uninitialized automatic variable from UB to a defined-but-wrong outcome (well-defined diagnosable error, with a specified value read), shrinking the UB surface; hardened standard library modes standardize bounds-checked preconditions (libc++ hardening being the working model, which Google measured at fractions-of-a-percent cost while killing real vulnerability classes); safety profiles, opt-in enforceable subsets (banning pointer arithmetic, enforcing initialization) remain the direction under active development, alongside lifetime annotation experiments in Clang. The honest position to state: C++ is not becoming memory-safe by construction; it is systematically shrinking UB, making bounds checking the default, and betting that profiles plus tooling (sanitizers, fuzzing, static analysis) keep it defensible where its performance and ecosystem matter, while new greenfield network-facing components increasingly get written in Rust alongside C++ cores. Knowing both halves of that sentence is what the question is testing.

// C++26 reflection (P2996): enum -> string without macros/codegen
#include <meta>

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string_view enum_name(E value) {
  template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
    if (value == [:e:])                 // splice the enumerator back in
      return std::meta::identifier_of(e);
  }
  return "<unknown>";
}

enum class Side { Buy, Sell };
static_assert(enum_name(Side::Buy) == "Buy");

// C++26 contracts:
double sqrt_checked(double x)
  pre (x >= 0.0)                        // enforced per build mode
  post (r : r >= 0.0)
{
  return std::sqrt(x);
}

Key Points

  • Reflection: consteval introspection + splicing kills codegen boilerplate
  • Contracts: pre/post with ignore/observe/enforce build modes
  • std::execution standardizes composable async (senders/receivers)
  • Safety: erroneous behavior, hardened stdlib, profiles, not a borrow checker
Q60

How do low-latency trading systems use C++? Name the techniques an HFT interview in Gurgaon will expect.

AdvancedLow Latency

Answer

India's HFT firms (Tower Research, Graviton, Quadeye, AlphaGrep, and quant desks at DE Shaw and Goldman Sachs in Bangalore/Hyderabad) run tick-to-trade pipelines where wire-in to order-out is measured in hundreds of nanoseconds to single-digit microseconds, and their interviews test whether you know which techniques buy which nanoseconds. The hot path discipline: zero heap allocation (everything preallocated: object pools, ring buffers, fixed-capacity containers; an unexpected malloc is a fireable offense on the tick path), zero locks (SPSC lock-free queues with acquire/release semantics between pinned threads), zero syscalls (kernel-bypass networking, Solarflare ef_vi/Onload or DPDK, delivering packets to user space without interrupts), and zero surprises (no exceptions on the hot path, and often -fno-exceptions entirely; no RTTI; no virtual dispatch in the inner loop, replaced by CRTP, templates, or variant dispatch). System configuration is half the job: isolate cores (isolcpus, nohz_full) and pin threads (pthread_setaffinity_np), spin instead of sleeping (a blocked thread pays wakeup latency; busy-poll loops with _mm_pause), disable frequency scaling and deep C-states, use huge pages for predictable TLB behavior, and interleave-aware NUMA placement so the NIC, memory, and core sit on one socket.

Code-level techniques: cache-line-aligned data structures sized to powers of two, branch-free arithmetic and [[likely]] annotations informed by PGO, warm the icache/dcache by running the full path on synthetic ticks between real ones (keeping the 'fast path hot'), constexpr precomputation of every table, and template-monomorphized strategy code so the compiler sees through everything. Measurement culture: rdtsc/rdtscp timestamping with serialization awareness, histograms of p50/p99/p99.9 per pipeline stage (means are meaningless; the tail is the product), hardware timestamping at the NIC for ground truth, and A/B replay of recorded market data. The stack beneath: FPGAs increasingly own the sub-100ns triggers, with C++ doing strategy and the slow path; know where that boundary sits.

Interviews concretely feature: build an SPSC ring buffer with correct memory ordering, explain why std::unordered_map is banned (allocation + pointer chasing) and what replaces it (open addressing, intrusive containers, flat arrays keyed by instrument id), estimate cache-miss budgets from latency targets, and defend every abstraction you keep. Compensation context: these roles are the top of the Indian C++ market, fresher offers at the elite Gurgaon shops exceed most senior product-company bands, precisely because this skill intersection is rare.

#include <atomic>
#include <array>
#include <new>

// SPSC ring buffer: the canonical HFT interview exercise
template <typename T, size_t N>   // N = power of two
class SpscQueue {
  static_assert((N & (N - 1)) == 0);
  std::array<T, N> buf_;
  alignas(std::hardware_destructive_interference_size)
      std::atomic<size_t> head_{0};   // consumer-owned
  alignas(std::hardware_destructive_interference_size)
      std::atomic<size_t> tail_{0};   // producer-owned
public:
  bool push(const T& v) {             // producer thread only
    const size_t t = tail_.load(std::memory_order_relaxed);
    if (t - head_.load(std::memory_order_acquire) == N) return false;
    buf_[t & (N - 1)] = v;
    tail_.store(t + 1, std::memory_order_release);  // publish
    return true;
  }
  bool pop(T& out) {                  // consumer thread only
    const size_t h = head_.load(std::memory_order_relaxed);
    if (h == tail_.load(std::memory_order_acquire)) return false;
    out = buf_[h & (N - 1)];
    head_.store(h + 1, std::memory_order_release);
    return true;
  }
};

Key Points

  • Hot path: no malloc, no locks, no syscalls, no exceptions/RTTI
  • Kernel bypass (ef_vi/DPDK), core pinning, spinning, huge pages
  • Measure p99.9 with rdtsc/NIC timestamps; means are meaningless
  • SPSC ring with acquire/release + padded indices is the set piece
💡 Pro Tip: When asked to design anything 'fast', state your latency budget and cache-miss arithmetic before writing code. Quantified reasoning is what separates HFT hires.

Companies Hiring C++

NVIDIA
Qualcomm
Adobe
Samsung R&D
DE Shaw
Tower Research Capital
Graviton Research Capital
MathWorks

Salary Insights

Average in India
₹8-28 LPA

Frequently Asked Questions

How much do C++ developers earn in India in 2026?

The spread is wider than almost any other language because the domains differ so much. Product companies (Adobe, NVIDIA, Qualcomm, Samsung R&D, MathWorks) pay roughly ₹8-28 LPA for mid-to-senior systems roles, with staff-level going higher. Services companies (TCS, Infosys, Wipro) doing embedded and telecom C++ sit lower, around ₹4-12 LPA. The outlier is HFT: Tower Research, Graviton, Quadeye, and AlphaGrep in Gurgaon regularly make fresher offers above ₹50 LPA (some crossing ₹1 Cr with bonuses) for candidates who combine competitive-programming-grade problem solving with real low-latency knowledge. GPU/ML infrastructure roles (CUDA, inference runtimes) are the fastest-growing premium segment and pay near HFT levels at NVIDIA and AI startups.

How long does it take to prepare for a C++ interview?

If you already write C++ at work, 4-6 weeks of focused preparation is realistic: two weeks consolidating the object model, ownership, and move semantics; two weeks on concurrency, the STL's complexity and invalidation guarantees, and modern-standard features; and the rest on DSA practice in C++ plus mock interviews. Coming from another language, budget 3-4 months, C++ punishes surface knowledge because interviewers probe one level below whatever you claim (say 'smart pointers' and the follow-up is the control block; say 'vector' and it is iterator invalidation). For HFT specifically, add dedicated time for the memory model, lock-free structures, and cache-behavior questions, plus timed competitive programming, since firms like Graviton screen with contest-style rounds first.

What do interviewers expect from freshers vs experienced C++ candidates?

Freshers are tested on fundamentals executed precisely: the Rule of Five, virtual dispatch mechanics, stack vs heap, references vs pointers, STL container complexities, and clean DSA implementations in C++ (know why you reserve a vector and when unordered_map degrades). Copies of copies, dangling references, and memory leaks in your interview code are instant signals. At 3-5 years, expect move semantics in depth, exception safety, templates and concepts, threading primitives, sanitizer-driven debugging stories, and CMake fluency; you should have production war stories about a race, a leak, or an ODR/ABI issue. Senior and HFT interviews add the memory model, lock-free reasoning, allocators, profiling methodology, and design questions where you must defend every abstraction's runtime cost with numbers.

Is C++ still worth learning in 2026 given Rust's momentum?

Yes, and the honest framing helps you in interviews. Rust is winning greenfield network-facing infrastructure and has government safety guidance behind it, but the installed base of C++ (browsers, game engines, HFT, GPU/ML runtimes like CUDA and TensorRT, databases, embedded, telecom) is measured in billions of lines that will be maintained and extended for decades, and India's highest-paying engineering jobs (HFT, NVIDIA-ecosystem roles) remain overwhelmingly C++. The language is also actively modernizing: C++23's expected/print/mdspan, C++26's reflection and contracts, hardened standard libraries, and safety profiles. The strongest market position is C++ plus one adjacency: C++ with CUDA, C++ with Rust, or C++ with deep Linux systems knowledge each command a premium over either skill alone.

Which C++ standard should I learn, and do old standards still matter?

Learn modern C++ first: make C++17 your floor (it is the default in most production codebases in 2026), be conversant with C++20's concepts, ranges, and coroutines, and know C++23's std::expected and std::print because interviewers increasingly use them as currency checks. You still need to READ older code: legacy telecom and banking codebases run C++11/14, and pre-C++17 idioms like SFINAE and manual enable_if appear in library internals everywhere. What you should not do is write pre-modern C++ in an interview: raw new/delete, NULL, typedef-heavy code, or hand-rolled loops where an algorithm exists all signal outdated habits. If a job posting says C++11 only (common in embedded), the modern-first foundation still transfers down easily; the reverse journey is much harder.

How does C++ compare with Java and Go for backend roles in India?

They occupy different territories. Java dominates enterprise backend hiring volume in India (banking, e-commerce at Flipkart/Amazon scale), and Go owns cloud-native infrastructure tooling; both trade control for productivity via garbage collection and simpler mental models. C++ backend roles are fewer but concentrated where GC pauses or per-request costs are unacceptable: exchange systems, market data, ad-serving hot paths, storage engines, and real-time bidding. Salary-wise, median Java and C++ overlap at product companies, but C++'s tail is much higher because of HFT and GPU infrastructure. Practically: if you want maximum job liquidity, Java or Go wins; if you want the specialized high-ceiling track and enjoy systems-level control, C++ is the better investment, and it makes learning any other language easier, never the reverse.

Introduction

C++ in 2026 is not the C-with-classes language your college syllabus taught. Modern C++ (the C++11 through C++23 standards, with C++26 close behind) is built around RAII, move semantics, smart pointers, constexpr computation, concepts, and ranges. It still owns the domains where every microsecond and every byte matters: trading systems, game engines, GPU and ML infrastructure (CUDA, TensorRT, the PyTorch and TensorFlow C++ cores), browsers, databases, and embedded firmware. Interviewers today expect you to write C++ that never calls new directly, knows exactly when a copy happens, and can explain what the optimizer is allowed to assume about your code.

India has one of the strongest C++ job markets in the world because of the Gurgaon and Mumbai HFT cluster: Tower Research Capital, Graviton Research Capital, Quadeye, and AlphaGrep pay fresher packages that cross ₹50 LPA for engineers who deeply understand the memory model, cache behavior, and lock-free programming. Outside finance, NVIDIA, Qualcomm, Adobe, Samsung R&D, MathWorks, and DE Shaw hire C++ engineers at scale in Bangalore, Hyderabad, Noida, and Pune. These interviews go far beyond syntax: expect deep probes on virtual dispatch cost, iterator invalidation, undefined behavior, sanitizers, CMake, and what changed in C++20 and C++23.

This guide contains 60 interview questions ordered from basic through advanced. The basic section locks down the object model, ownership, and the compilation pipeline. The intermediate section covers what actually decides offers: move semantics, templates, concepts, the concurrency primitives, tooling, and testing. The advanced section goes where senior and HFT interviews live: the memory model, lock-free structures, custom allocators, coroutines, ABI stability, and profiling. Each answer explains real behavior, the gotcha an interviewer is fishing for, and includes compilable modern C++ where code makes the idea concrete.

Ready to practice C++ interviews?

Don't just read, practice these C++ questions live with an AI interviewer that asks follow-ups and scores your answers.

AI-powered practice
Instant feedback
Free to start
Start Free Mock Interview