C++ Runtime Deep Dive

Target audience: candidates interviewing in C++ for HFT/quant, game engines, embedded, browsers, databases, systems-programming, or any role where the interviewer asks “what does this allocate?”, “is that UB?”, or “trace the move.”

Scope: ISO C++17 baseline with C++20/23 features called out. GCC, Clang, and MSVC all behave alike on the spec — vendor-specific behavior is noted only when it changes interview answers.

C++ punishes superficial knowledge harder than any other language on this list. The senior interviewer will set a trap (a dangling reference, a missed move, an iterator invalidation, a UB), and either you see it or you don’t. There is no bluffing through C++. Everything in this guide pays interest.


1. Memory Model — Stack, Heap, RAII

C++ gives you control over object lifetime. Every object you create lives somewhere:

StorageLifetimeCostExample
Automatic (stack)Scope of declarationZeroint x; Foo f;
Static / thread_localProgram / threadZero (init once)static Foo f;
Dynamic (heap)Until delete/destructormalloc + bookkeepingnew Foo / make_unique<Foo>
void f() {
    int x = 5;                            // stack
    static int y = 0;                     // static, init once
    auto p = std::make_unique<int>(42);   // heap, freed at scope end
}

RAII

Resource Acquisition Is Initialization. Tie the lifetime of a resource (memory, file, lock, socket) to the lifetime of an object on the stack.

{
    std::lock_guard<std::mutex> lk(mtx);   // acquires
    // ... critical section ...
}                                          // destructor releases

RAII is the single most important C++ idea. It makes exceptions safe, makes resource leaks impossible if you stick to it, and is the foundation of all modern C++. Every interview answer that involves “what if it throws?” reduces to “RAII handles it.”

Stack frames

A function call pushes a frame: arguments, return address, locals, callee-saved registers. Frame size is fixed at compile time. Stack overflow on deep recursion or huge stack arrays.

alloca / VLAs

alloca(n) allocates on the stack. C99 VLAs (int arr[n]) are not in C++. Modern code uses std::vector or std::array (compile-time n).


2. Pointers, References, Values

FormNullableRebindableStorage
Tn/an/aby value
T&NoNoreference; aliases another object
T*YesYespointer; an address
const T&NoNoread-only view
T&&NoNorvalue reference (see §4)
int x = 1;
int& r = x;     // r is x — no separate object
int* p = &x;
*p = 2;         // x is now 2
r = 3;          // x is now 3
p = nullptr;    // p reseats; r cannot be reseated

When to use which

  • Pass by value: small types (int, Point), or you want a copy / will move from the parameter.
  • Pass by const T&: large/expensive types you only read.
  • Pass by T&: out-parameters (rare in modern C++; prefer return values).
  • Pass by pointer: nullable, or you need a C-API.

Dangling refs

const std::string& bad() {
    std::string tmp = "hi";
    return tmp;             // returns reference to dead local — UB
}

The compiler may warn; the runtime will silently corrupt. Sanitizers (ASan) catch many cases.


3. Smart Pointers — unique_ptr, shared_ptr, weak_ptr

The modern rule: never new/delete directly. Use:

  • std::unique_ptr<T> — exclusive ownership, zero overhead vs raw pointer.
  • std::shared_ptr<T> — shared ownership, atomic refcount.
  • std::weak_ptr<T> — non-owning observer; breaks shared_ptr cycles.
auto u = std::make_unique<Foo>(args...);   // unique
auto s = std::make_shared<Foo>(args...);   // shared
std::weak_ptr<Foo> w = s;                  // non-owning

Cost model

unique_ptr<T> is a single pointer. Move-only. Compiler optimizes away the wrapper.

shared_ptr<T> is two pointers (the object, the control block) + an atomic refcount. Copying = atomic increment. Destruction = atomic decrement.

make_shared vs shared_ptr<T>(new T)

make_shared<T> allocates the object and the control block in one block. Cheaper, better cache locality. Drawback: memory isn’t freed until the last weak_ptr dies (because the control block lives in the same allocation).

Cycles

struct Node { std::shared_ptr<Node> next; };
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; b->next = a;
// a and b never freed — refcount of each stays at 2

Fix: one direction weak_ptr. Or, redesign — most “cycles” represent ownership confusion.

Custom deleter

auto p = std::unique_ptr<FILE, decltype(&fclose)>(fopen("x", "r"), &fclose);

Useful for C-API resources.


4. Move Semantics, Rvalue References

A moved-from object is in a “valid but unspecified” state. The point of move is to transfer expensive resources (heap allocations, file handles) without copying.

std::string a = "hello";
std::string b = std::move(a);   // b owns the buffer; a is empty (typically)

std::move is a cast — it doesn’t move anything; it tells the compiler “treat this as an rvalue, please pick the move overload.”

Rule of 0/3/5

  • Rule of 0: design classes so the defaults are correct. Member variables are RAII types (vector, unique_ptr, string). Don’t write any of the special members.
  • Rule of 3 (pre-C++11): if you write any of dtor, copy ctor, copy assign, write all three.
  • Rule of 5 (C++11+): add move ctor and move assign.
class Buffer {
    std::unique_ptr<char[]> data_;
    std::size_t size_;
public:
    Buffer(std::size_t n)
        : data_(std::make_unique<char[]>(n)), size_(n) {}
    // copy/move auto-generated correctly because members are RAII.
};

noexcept matters

Move operations should be noexcept. If they aren’t, std::vector can’t use them when reallocating — it falls back to copy, defeating the purpose.

struct S {
    std::string name;
    S(S&&) noexcept = default;          // critical
    S& operator=(S&&) noexcept = default;
};

Forwarding references (T&& in templates)

template<class T>
void f(T&& x) {                       // forwarding ref, NOT rvalue ref
    g(std::forward<T>(x));            // preserves value category
}

Reference collapsing: T&& &&T&&, T&& &T&. This is the mechanism behind perfect forwarding (and std::forward).


5. Copy Elision and RVO

The compiler is allowed (and often required) to elide copy/move when constructing return values.

Foo make() { return Foo{};  }            // (N)RVO — direct construction in caller
Foo f = make();                          // no copy, no move

C++17 mandated copy elision for prvalues — the move you “see” in source code may not exist as an actual operation.

auto v = std::vector<int>(1'000'000);    // no copy of the temporary

Implication: return by value is the right default. The compiler will not copy a big vector.


6. Templates, SFINAE, Concepts

Templates are compile-time generators. Each instantiation produces a fresh type or function.

template<class T>
T max(T a, T b) { return a < b ? b : a; }

max(1, 2);          // T = int
max(1.0, 2.0);      // T = double
max(1, 2.0);        // ambiguous — different T's

SFINAE — “Substitution Failure Is Not An Error”

Failed substitutions are silently dropped from the overload set, not compile errors:

template<class T>
auto add(T a, T b) -> decltype(a + b) { return a + b; }

Older idiom: std::enable_if_t<...>. Crufty; use concepts instead in C++20:

template<class T>
concept Numeric = std::is_arithmetic_v<T>;

template<Numeric T>
T add(T a, T b) { return a + b; }

Compile-time error blasts

A template error message can be thousands of lines. Modern compilers (gcc 13+, clang 16+) and concepts dramatically reduce this. If you see a 5000-line error in an interview, don’t panic; isolate by typedef-ing intermediate types.

CRTP (Curiously Recurring Template Pattern)

Static polymorphism — virtual without the vtable cost.

template<class Derived>
struct Base { void f() { static_cast<Derived*>(this)->impl(); } };

struct D : Base<D> { void impl() { /* ... */ } };

7. STL Containers — Complexity

ContainerInsertEraseLookupIter InvalidationMemory
vectorO(1)* end / O(N) middleO(N)O(N), O(1) by indexAll on grow / from posContiguous
arrayn/an/aO(1)NoneContiguous, fixed N
dequeO(1) ends, O(N) middleO(N)O(1)All except endsBlock array
listO(1) anywhere (with iter)O(1)O(N)None on insert; affected pos on eraseDoubly linked
forward_listO(1) after iterO(1)O(N)None on insertSingly linked
set/mapO(log N)O(log N)O(log N)None on insert; pos on eraseRed-black tree
unordered_set/mapO(1) avg, O(N) worstO(1) avgO(1) avgAll on rehashBuckets + nodes

vector is the default. Reach for others only with a measured reason.

std::vector<int> v;
v.reserve(1'000'000);          // pre-size, avoid grows
for (int i = 0; i < 1'000'000; ++i) v.push_back(i);

unordered_map warnings

Open-chaining hash table. Each node is heap-allocated → bad cache locality. For perf-critical code, prefer absl::flat_hash_map, tsl::robin_map, or other open-addressing maps. State this in HFT/perf interviews; it’s a known weakness.

std::unordered_map<std::string, int> m;
m.reserve(N);                    // sets bucket count
m.max_load_factor(0.5);          // tighter than default 1.0

8. Iterator Invalidation

The single most common subtle bug in C++.

ContainerOperationWhat invalidates
vectorpush_back, insert, reserve triggering growAll iterators/refs/pointers
vectoreraseIterators/refs at and after pos
dequeany insert/erase except at endsAll iterators (refs to non-affected elements survive)
list / forward_listinsert, push_*None
list / forward_listeraseOnly iterators to erased element
unordered_*rehash (insert that exceeds load factor)All iterators (refs/pointers survive!)
map / setinsertNone
map / seteraseOnly iterators to erased
std::vector<int> v{1,2,3,4,5};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 3) v.push_back(99);   // UB — push_back may invalidate `it`
}

// Correct: collect, then mutate; or use erase-remove.
v.erase(std::remove_if(v.begin(), v.end(), pred), v.end());

9. STL Algorithms

<algorithm> and <numeric> provide a rich library. Use them — hand-rolled loops are usually slower and harder to read.

std::sort(v.begin(), v.end());                            // IntroSort, O(N log N)
std::stable_sort(v.begin(), v.end());                     // O(N log² N) generally
std::nth_element(v.begin(), v.begin()+k, v.end());        // O(N) avg, kth-element
std::partial_sort(v.begin(), v.begin()+k, v.end());       // top-k, O(N log K)
std::lower_bound(v.begin(), v.end(), x);                  // binary search, O(log N)
std::accumulate(v.begin(), v.end(), 0LL);                 // careful with init type

Sort algorithms

std::sort is introsort: quicksort, switching to heapsort if recursion gets too deep, switching to insertion sort for small ranges. Worst case O(N log N), unstable. std::stable_sort is typically merge sort with allocation; std::sort is usually preferred unless stability matters.

Ranges (C++20)

auto evens = v | std::views::filter([](int x){ return x%2==0; })
               | std::views::transform([](int x){ return x*x; });

Lazy, composable. Less verbose than iterator pairs.


10. Concurrency — std::thread, mutex, atomics, memory_order

std::thread t([]{ work(); });
t.join();                           // or t.detach() — but rarely

If a std::thread is destroyed while joinable, the program calls terminate. std::jthread (C++20) joins on destruction.

Mutex

std::mutex m;
std::lock_guard<std::mutex> lk(m);   // RAII lock

std::scoped_lock (C++17) locks multiple mutexes deadlock-free.

Condition variables

std::condition_variable cv;
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return ready; });    // releases lk, waits, reacquires

Always use the predicate form to handle spurious wakeups.

std::atomic<T>

std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);

memory_order

OrderGuaranteesUse
relaxedAtomicity only, no orderingStat counters
acquire (load)No subsequent reads/writes can move beforeRead of a flag protecting data
release (store)No prior reads/writes can move afterWrite that publishes data
acq_rel (RMW)BothCAS retry loops
seq_cst (default)Sequential consistency, single total orderDefault; safest
// Producer:
data = produce();
ready.store(true, std::memory_order_release);

// Consumer:
while (!ready.load(std::memory_order_acquire)) {}
use(data);     // safe — release/acquire pair

memory_order is interview territory at L6+ HFT/system roles. Default to seq_cst until measured.


11. Undefined Behavior (UB)

UB means the spec places no requirements. The compiler may eliminate code, “optimize” infinite loops, or generate code that does anything. Don’t rely on “well, it works on my machine.”

Common UB

  1. Read of uninitialized memory.
  2. Out-of-bounds access (v[v.size()] is UB).
  3. Signed integer overflow (unsigned wraps, signed is UB).
  4. Use-after-free / double-free.
  5. Race conditions (concurrent unsynchronized access to mutable data).
  6. Strict aliasing violations (reinterpreting a float* as int*).
  7. Null pointer deref — including for member access on a null pointer.
  8. Lifetime violations — using a moved-from object beyond what’s specified.
  9. Integer division by zero, INT_MIN / -1.
  10. Returning reference/pointer to a local.

Why it bites in interviews

The interviewer puts a for (int i = 0; i <= n; ++i) v[i] = ...; on the board and watches whether you flag the OOB. If you don’t, your perceived rigor drops a tier instantly.

Sanitizers

Compile + run tests under:

clang++ -fsanitize=address,undefined -g -O1 main.cpp
clang++ -fsanitize=thread     -g -O1 main.cpp   # for races

ASan: heap/stack/global OOB, use-after-free, double-free. UBSan: signed overflow, null derefs, alignment. TSan: data races.

State in interviews that you run sanitizers in CI. It signals discipline.


12. Common Interview Gotchas

Virtual destructor

If a class is meant to be derived-from and used polymorphically, the destructor must be virtual — otherwise delete base_ptr calls only the base’s destructor.

struct Base { virtual ~Base() = default; };
struct Derived : Base { /* ... */ };
Base* p = new Derived;
delete p;   // virtual dtor → Derived's runs

Object slicing

void f(Base b);              // by value
Derived d;
f(d);                        // d sliced — only Base portion copied

Always pass polymorphic types by reference or pointer, never by value.

vector<bool> is not a vector of bool

Specialized as a packed bitset → operator[] returns a proxy, not bool&. Don’t take its address.

std::vector<bool> v;
auto x = v[0];               // proxy reference, not bool&

Use std::vector<char> if you need real bools.

Self-assignment

T& operator=(const T& o) {
    if (&o == this) return *this;     // guard
    // ...
}

Or: copy-and-swap idiom — pass by value (copy happens at call site), swap, return.

Initialization order

Member variables are constructed in declaration order, not member-initializer-list order. Compiler warns when they differ.

static local init

Thread-safe since C++11 (Magic statics). One initialization, even with concurrent first access.

nullptr vs NULL vs 0

Use nullptr. NULL is 0 (an integer); 0 doesn’t overload-resolve cleanly.

Floating-point comparison

Same warning as Java — never == for float/double. Use tolerances or std::nextafter.

Implicit conversions

int → bool, bool → int, double → int. Use explicit for single-arg constructors:

struct Date { explicit Date(int y); };
Date d = 2024;        // error — explicit constructor
Date d{2024};         // OK

13. Modern C++ Idioms

  • auto for local types — but spell out parameter and return types where they’re API.
  • Range-forfor (const auto& x : container).
  • Lambdas — capture defaults: [] (none), [&] (by ref), [=] (by value), [this].
  • enum class — strongly typed, scoped enums. No implicit int conversion.
  • structured bindingsauto [k, v] = *it;.
  • if constexpr — compile-time branch in templates.
  • std::optionalMaybe<T>. Use for “may not exist.”
  • std::variant — tagged union.
  • std::string_view — non-owning view of a string. Don’t store across the string’s lifetime.
  • std::span — non-owning view of a contiguous range.
  • {} init — uniform initialization. Prevents narrowing conversions.
int a{3.14};       // error — narrowing
int a = 3.14;      // OK (silent truncation)

Modules (C++20)

Replacement for headers. Faster builds, better isolation. Adoption uneven; compilers still maturing.

Coroutines (C++20)

generator<int> ints() {
    for (int i = 0;; ++i) co_yield i;
}

The standard library lacks high-level types — you bring boost::asio or roll your own. Mention only if asked.


14. Compile-Time vs Runtime

C++ has a powerful compile-time computation toolkit. Use it to push work out of the runtime.

constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n-1); }
static_assert(factorial(5) == 120);

template<class T>
constexpr bool is_pod_v = std::is_trivial_v<T> && std::is_standard_layout_v<T>;

constexpr, consteval (C++20), if constexpr together let you write code that’s branchless and zero-cost when called with constant inputs.

Compile-time hash

Implement a consteval string hash, generate switch tables — common HFT trick to dispatch on string commands at runtime in O(1) without runtime hashing.


15. Performance Hot Tips

  • Cache friendliness wins. Arrays of structs with sequential access trounce trees of pointers, even when complexity is “the same.” A modern CPU handles ~1 cache miss per 100 cycles of compute.
  • Reserve. vector::reserve, unordered_map::reserve. Avoid grow churn.
  • Move into containers. v.push_back(std::move(s)); over v.push_back(s);.
  • emplace_back over push_back when constructing in place.
  • Pass by value + std::move in constructors and setters — modern idiom.
  • Avoid std::endl — it flushes. Use '\n'.
  • Prefer iteration over recursion for deep structures; the function-call overhead and stack pressure matter.
  • Profile before optimizing. perf, VTune, callgrind, sampling profilers. Algorithmic wins dwarf micro-optimizations.
  • Compile with -O2 -march=native -flto for production.
  • Avoid virtual in hot paths when possible. Devirtualization helps but a known-static dispatch is always cheaper.
  • Beware of false sharing — two atomics on the same cache line (typically 64B) bottleneck even when “independent.” Pad with alignas(std::hardware_destructive_interference_size).
struct alignas(64) Counter { std::atomic<long> v{0}; };

16. Tooling — Sanitizers, Compiler-Specific Behavior

Sanitizers (recap from §11)

  • ASan — memory errors.
  • UBSan — undefined behavior.
  • TSan — races.
  • MSan (Clang only) — uninitialized reads.

Run them in CI. Production: don’t ship with sanitizers (perf cost), but optionally enable a hardened mode (_FORTIFY_SOURCE=2, -fstack-protector-strong).

Warning flags

g++ -Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion

Treat warnings as errors. The C++ ecosystem assumes you do.

Standard library debug modes

-D_GLIBCXX_DEBUG (libstdc++) checks bounds, iterator invalidation. Only debug builds — slow.

Vendor-specific behavior

  • MSVC has different ABI rules (e.g., NRVO eligibility, exception spec). Don’t depend on inline assembly portability.
  • __attribute__((...)) is GCC/Clang. MSVC uses __declspec.
  • Endian-ness, padding, alignment are platform-dependent. Don’t memcpy between systems without endian conversion.

17. C++ — What To Memorize Cold

  • RAII. RAII. RAII.
  • Rule of 0/3/5. Default to Rule of 0.
  • unique_ptr cheap, shared_ptr has atomic refcount, weak_ptr breaks cycles.
  • Move = transfer of ownership. Moved-from = valid but unspecified. noexcept move ops matter.
  • C++17 mandates prvalue copy elision — return by value is fine.
  • Iterator invalidation rules per container — memorize the table in §8.
  • vector is the default; unordered_map is slow on cache locality.
  • Sort is introsort — O(N log N) worst, unstable. stable_sort allocates.
  • memory_order: relaxed for counters, acquire/release for publication, seq_cst default.
  • UB list: OOB, signed overflow, races, use-after-free, strict aliasing, null deref, uninitialized read. Sanitizers catch most.
  • Virtual destructor for polymorphic bases. Object slicing on by-value. vector<bool> is special.
  • nullptr, enum class, auto, string_view, optional, variant, structured bindings — modern toolkit.
  • Cache locality > algorithmic constants in modern hardware.
  • Compile with -O2 -march=native -flto -Wall -Wextra for production. Run sanitizers in CI.

When you’re shaky on any of those, write a 30-line program that demonstrates the issue and run it under ASan + UBSan. C++’s sanitizers are some of the best feedback in any language; use them.