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:
| Storage | Lifetime | Cost | Example |
|---|---|---|---|
| Automatic (stack) | Scope of declaration | Zero | int x; Foo f; |
| Static / thread_local | Program / thread | Zero (init once) | static Foo f; |
| Dynamic (heap) | Until delete/destructor | malloc + bookkeeping | new 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
| Form | Nullable | Rebindable | Storage |
|---|---|---|---|
T | n/a | n/a | by value |
T& | No | No | reference; aliases another object |
T* | Yes | Yes | pointer; an address |
const T& | No | No | read-only view |
T&& | No | No | rvalue 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; breaksshared_ptrcycles.
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
| Container | Insert | Erase | Lookup | Iter Invalidation | Memory |
|---|---|---|---|---|---|
vector | O(1)* end / O(N) middle | O(N) | O(N), O(1) by index | All on grow / from pos | Contiguous |
array | n/a | n/a | O(1) | None | Contiguous, fixed N |
deque | O(1) ends, O(N) middle | O(N) | O(1) | All except ends | Block array |
list | O(1) anywhere (with iter) | O(1) | O(N) | None on insert; affected pos on erase | Doubly linked |
forward_list | O(1) after iter | O(1) | O(N) | None on insert | Singly linked |
set/map | O(log N) | O(log N) | O(log N) | None on insert; pos on erase | Red-black tree |
unordered_set/map | O(1) avg, O(N) worst | O(1) avg | O(1) avg | All on rehash | Buckets + 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++.
| Container | Operation | What invalidates |
|---|---|---|
vector | push_back, insert, reserve triggering grow | All iterators/refs/pointers |
vector | erase | Iterators/refs at and after pos |
deque | any insert/erase except at ends | All iterators (refs to non-affected elements survive) |
list / forward_list | insert, push_* | None |
list / forward_list | erase | Only iterators to erased element |
unordered_* | rehash (insert that exceeds load factor) | All iterators (refs/pointers survive!) |
map / set | insert | None |
map / set | erase | Only 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
| Order | Guarantees | Use |
|---|---|---|
relaxed | Atomicity only, no ordering | Stat counters |
acquire (load) | No subsequent reads/writes can move before | Read of a flag protecting data |
release (store) | No prior reads/writes can move after | Write that publishes data |
acq_rel (RMW) | Both | CAS retry loops |
seq_cst (default) | Sequential consistency, single total order | Default; 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
- Read of uninitialized memory.
- Out-of-bounds access (
v[v.size()]is UB). - Signed integer overflow (unsigned wraps, signed is UB).
- Use-after-free / double-free.
- Race conditions (concurrent unsynchronized access to mutable data).
- Strict aliasing violations (reinterpreting a
float*asint*). - Null pointer deref — including for member access on a null pointer.
- Lifetime violations — using a moved-from object beyond what’s specified.
- Integer division by zero, INT_MIN / -1.
- 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
autofor local types — but spell out parameter and return types where they’re API.- Range-for —
for (const auto& x : container). - Lambdas — capture defaults:
[](none),[&](by ref),[=](by value),[this]. enum class— strongly typed, scoped enums. No implicit int conversion.structured bindings—auto [k, v] = *it;.if constexpr— compile-time branch in templates.std::optional—Maybe<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));overv.push_back(s);. emplace_backoverpush_backwhen constructing in place.- Pass by value +
std::movein 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 -fltofor production. - Avoid
virtualin 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
memcpybetween systems without endian conversion.
17. C++ — What To Memorize Cold
- RAII. RAII. RAII.
- Rule of 0/3/5. Default to Rule of 0.
unique_ptrcheap,shared_ptrhas atomic refcount,weak_ptrbreaks cycles.- Move = transfer of ownership. Moved-from = valid but unspecified.
noexceptmove ops matter. - C++17 mandates prvalue copy elision — return by value is fine.
- Iterator invalidation rules per container — memorize the table in §8.
vectoris the default;unordered_mapis slow on cache locality.- Sort is introsort — O(N log N) worst, unstable.
stable_sortallocates. memory_order:relaxedfor counters,acquire/releasefor publication,seq_cstdefault.- 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 -Wextrafor 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.