The fifty C++ questions that actually get asked
Memory model, RAII and ownership, move semantics, virtual dispatch, templates, the standard library, and the concurrency model — each answered the way you'd want to answer it out loud, not the way a flashcard would.
These are grouped by the seven areas a C++ interview actually walks through, roughly in the order a good interviewer escalates: the object model first, then ownership, then the modern-C++ machinery, then concurrency — because almost every hard C++ question is really a question about lifetime wearing a different hat.
3.1The Object Model and Memory
What is the difference between new/delete and malloc/free?
malloc returns raw untyped storage; new acquires storage and runs a constructor, producing an object with a lifetime. Symmetrically delete runs the destructor before releasing storage, free does not. That means mixing them is undefined behaviour in both directions: free on a new-ed object skips the destructor (leaked resources, unbalanced invariants), and delete on a malloc-ed pointer destroys an object that was never constructed.
Two more differences that get overlooked. new reports failure by throwing std::bad_alloc rather than returning null, so the classic if (p == nullptr) check after new is dead code unless you wrote new (std::nothrow). And new[]/delete[] are a separate pair — the array form has to know the element count to run N destructors, which it usually stores in a cookie ahead of the returned pointer, so calling scalar delete on an array is also UB. In modern code you should be writing neither: make_unique, make_shared, and containers cover essentially all of it.
Walk me through where a variable actually lives: stack, heap, static, thread-local.
The standard talks about storage duration, not about stack and heap, and the distinction matters because it's what determines lifetime. Automatic storage (locals, parameters) is destroyed at end of scope, in reverse construction order — in practice a stack frame, cheap to allocate because it's a pointer bump. Dynamic storage (new, allocators) lives until you release it, which is the only duration where you own the timing and therefore the only one that can leak. Static storage (globals, static locals, class statics) is constructed once and destroyed at exit; zero-initialised before anything else runs. Thread-local (thread_local) is one instance per thread, destroyed at thread exit.
The practical consequences are all about size and lifetime. The stack is small — typically 1–8 MB, and on an embedded target or a thread you spawned yourself, far less — so a large local array is a stack overflow, which does not throw and often does not fault cleanly. And a pointer or reference to an automatic object is invalid the moment its scope exits: returning &local is the canonical dangling-reference bug.
What is object lifetime, and when is touching an object undefined behaviour?
An object's lifetime begins when its initialisation completes and ends when its destructor call starts (or when its storage is released or reused). Outside that window the storage may physically exist and even hold the old bit pattern, but the object does not, so reading it, calling a member function on it, or binding a reference to it is UB — and UB that frequently "works" in a debug build, which is exactly why it survives to production.
The four common ways to be outside the window: use-after-free; use-after-move on a type where the moved-from state doesn't support the operation; dangling reference to an expired temporary or expired stack frame; and use-during-destruction, where a base-class destructor calls a virtual function and gets the base override because the derived part is already gone. Sanitizers find most of these cheaply — -fsanitize=address,undefined in CI is a far better use of an afternoon than memorising the rules.
Undefined, unspecified, implementation-defined behaviour — what's the difference and why care?
Undefined: the standard imposes no requirements at all. The compiler may assume it never happens, and it optimises on that assumption — which is why UB deletes your null check or turns a signed-overflow loop into an infinite one. Examples: signed integer overflow, out-of-bounds access, dereferencing null, data races. Unspecified: several behaviours are legal, the implementation need not document which it picked or be consistent, but it must be one of them — e.g. the evaluation order of most function arguments. Implementation-defined: unspecified plus a documentation requirement — e.g. sizeof(int), whether plain char is signed.
The reason to care in an interview and in review: UB isn't "it might print garbage." It licenses the optimiser to reason backwards from the assumption that the UB path is unreachable, so the visible misbehaviour can appear in code hundreds of lines away from the mistake. That asymmetry — local error, non-local symptom — is why C++ shops lean so hard on sanitizers, warnings-as-errors, and bounds-checked accessors in debug builds.
Declaration vs definition, and what is the One Definition Rule?
A declaration introduces a name and its type; a definition additionally provides the body or storage. You may declare a thing many times, but the ODR says each entity gets exactly one definition per program (for functions and variables) and, for classes and templates, one definition per translation unit — with all such definitions being token-identical if they appear in more than one TU.
This is why the header/source split works the way it does: put declarations in the header, definitions in one .cpp. Things exempt from the "one per program" rule because they're implicitly inline — class member functions defined in-class, templates, constexpr and inline functions, and (since C++17) inline variables — may appear in every TU that includes the header. Violating the ODR is UB the linker is not required to catch, which is how two structs with the same name and different layouts in two TUs produce a program that corrupts memory and links cleanly.
What is the static initialisation order fiasco, and how do you avoid it?
Static-storage objects with dynamic initialisers are initialised in an order that is defined within a translation unit (top to bottom) but completely unspecified across translation units. So if a global in a.cpp uses a global in b.cpp during its constructor, you have a coin flip that depends on link order, and it changes when someone reorders the build.
The standard fix is the function-local static, which is initialised on first use and (since C++11) initialised thread-safely:
Registry& registry() {
static Registry instance; // constructed on first call, thread-safe
return instance;
}This trades the ordering problem for a destruction-ordering problem — local statics are destroyed in reverse order of construction, so an object that logs from its destructor can still find the logger already gone. Where that matters, either never destroy (leak deliberately via new, the "Nifty Counter"/immortal-singleton approach) or make ownership explicit and construct these things in main in a known order. The real answer in a design discussion is: prefer no mutable global state, and pass dependencies in.
Pointer or reference — how do you choose?
Mechanically: a reference must be initialised, can't be reseated, and can't (legally) be null; a pointer can be null, reassigned, and arithmetic'd. Both are usually the same machine address under the hood.
The useful framing is that the choice encodes an interface contract. A T& parameter says "this argument is required and I won't keep it." A const T& says the same plus "I won't modify it" — the default for passing anything bigger than a couple of words. A T* says "this may be absent" and forces the caller to think about that. A smart pointer says something about ownership: unique_ptr<T> in a signature means "I'm taking ownership," shared_ptr<T> means "I'm sharing it." So: reference by default, raw pointer for optional non-owning, smart pointer for transfer of ownership, and std::optional<T&>… doesn't exist, which is why the optional-reference case still uses a pointer.
Explain const correctness: const pointers, const member functions, mutable.
Read declarations right to left. const T* p is a pointer to const T — you can move the pointer, not modify the pointee. T* const p is a const pointer — the reverse. const T* const p is both. A const member function promises not to modify the observable state of *this; inside it, this is const T*, so you can only call other const members.
mutable on a member exempts it from that promise, and its legitimate use is logical constness: a cache, a memo, a mutex. A const member function that locks a mutable std::mutex is correct and idiomatic. Two things worth adding because they show depth: const is part of the signature for overload resolution, which is how containers give you both begin() and const begin(); and since C++11, const means thread-safe-to-read-concurrently by convention throughout the standard library, so a const method that mutates a non-atomic cache without a lock is a data race even though it compiles.
3.2RAII, Smart Pointers, Ownership
What is RAII, and why is it the centre of the language?
Resource Acquisition Is Initialisation: bind a resource's lifetime to an object's lifetime, acquire in the constructor, release in the destructor. Because the destructor runs deterministically at scope exit — including during exception unwinding — the release cannot be forgotten and cannot be skipped by an early return, a break, or a throw.
That last clause is the whole point, and it's why C++ doesn't need finally. Manual lock(); work(); unlock(); is broken the moment work() throws; std::lock_guard g(m); is not. The pattern generalises past memory to every resource with a paired acquire/release: files, sockets, mutexes, GPU contexts, transactions, a temporarily-changed global setting. When you're asked to design a class in an interview, the RAII question — "what does this own, and does its destructor release it exactly once?" — is usually the highest-value thing you can say.
unique_ptr, shared_ptr, weak_ptr: when do you reach for each?
unique_ptr is exclusive ownership, move-only, and zero-overhead — same size as a raw pointer with a stateless deleter, and no atomic traffic. It should be your default; roughly 90% of dynamic allocation in a healthy codebase is unique_ptr or a container.
shared_ptr is shared ownership via a reference count, and it costs real money: two pointers wide, a separately allocated control block, and atomic increment/decrement on every copy, which shows up under contention. Use it when lifetime genuinely cannot be determined statically — a cache handed out to callers who may outlive each other, an object captured by an async continuation. weak_ptr is a non-owning observer of a shared_ptr; you call lock() to atomically get a shared_ptr if the object is still alive, or an empty one if not. Its two jobs are breaking reference cycles and holding a safe back-pointer to something you don't own.
shared_ptr as a default is the most common ownership smell in real code. It doesn't say "shared," it says "I didn't want to think about lifetime," and it converts a design question into an atomic-refcount cost paid forever. Ask who the last owner is; usually it's obvious, and then a unique_ptr plus raw non-owning pointers or references is both faster and clearer.How does shared_ptr work internally, and why prefer make_shared?
A shared_ptr holds two pointers: one to the object and one to a control block containing the strong count, the weak count, the deleter and the allocator. Copies bump the strong count atomically; when it hits zero the object is destroyed; when the weak count also hits zero the control block itself is freed — which is why a lingering weak_ptr can keep the control block (though not the object) alive.
make_shared<T>(args...) allocates the object and the control block in one block, so it's one allocation instead of two and gives better locality. It is also exception-safe in a way the two-step is not: in f(shared_ptr<T>(new T), g()), argument evaluation order is unspecified, so new T can succeed, g() can throw, and the raw pointer leaks before it was ever adopted. The honest caveat: because the single block holds both, the object's storage isn't returned until the last weak_ptr also dies, so for a very large object with long-lived weak observers, the two-allocation form can be preferable. Also, make_shared can't take a custom deleter — that needs the constructor form.
What is a shared_ptr cycle and how do you break it?
If A holds a shared_ptr to B and B holds one back to A, each keeps the other's count at one forever: neither destructor runs, and the leak is invisible to a naive audit because nothing was "forgotten." Reference counting fundamentally cannot collect cycles — that's the price of not having a tracing GC.
The fix is to decide which direction is ownership and make the other direction a weak_ptr (or a plain non-owning pointer if the lifetime relationship is guaranteed by structure). Parent owns children with shared_ptr; children observe the parent with weak_ptr and call lock() before use. The same rule applies to observer/callback registries and to graph structures with back-edges — and note that a lambda captured into a member std::function that captures shared_from_this() is a self-cycle, which is the version of this bug that most often ships.
Why must a polymorphic base class have a virtual destructor?
Because delete base_ptr where the static type is Base* and the dynamic type is Derived is undefined behaviour unless ~Base is virtual. In practice you get only ~Base running: the derived members are never destroyed, so anything they own leaks, and with multiple inheritance or an adjusted this pointer you can corrupt the heap outright.
The rule of thumb: if a class has any virtual function, or is otherwise intended to be deleted through a base pointer, give it a public virtual destructor. If it isn't meant to be a polymorphic base, the modern alternative is to say so — make the destructor protected and non-virtual (so nobody can delete through the base), or mark the class final. Worth knowing as a genuine exception: shared_ptr captures the deleter at construction from the concrete type, so shared_ptr<Base> p = make_shared<Derived>() destroys correctly even without a virtual destructor. unique_ptr<Base> does not, because its deleter is part of its type.
State the Rule of Three, Five, and Zero.
Three (pre-C++11): if you need a custom destructor, copy constructor, or copy assignment, you almost certainly need all three — they exist because the class manages a resource, and all three have to agree on how it's duplicated and released. Five (C++11): add the move constructor and move assignment. Zero: best of all, design so you need none — hold members that already manage themselves (vector, string, unique_ptr) and let the compiler-generated versions be correct.
The trap in the middle is that declaring one of these suppresses others: writing a destructor suppresses the implicit move operations (so your class silently falls back to copying, and a "why is this slow" bug is born), and declaring a move operation deletes the copy operations. So the rule is all-or-nothing: either declare none of the five, or declare all of them (= default is fine for the ones you don't need to customise).
How do you use a smart pointer for something that isn't new/delete?
Give it a custom deleter. That turns unique_ptr into a general-purpose RAII wrapper over any C API handle:
using FilePtr = std::unique_ptr<FILE, decltype(&std::fclose)>;
FilePtr f(std::fopen("log.txt", "r"), &std::fclose);
// stateless deleter: no size cost at all
struct FreeDeleter { void operator()(void* p) const noexcept { std::free(p); } };
std::unique_ptr<char, FreeDeleter> buf(static_cast<char*>(std::malloc(n)));Two details worth mentioning unprompted. The deleter is part of unique_ptr's type, so a function-pointer deleter makes the object two words wide, while an empty functor type is optimised away to nothing by empty-base optimisation — prefer the functor. And unique_ptr<T[]> is a real specialisation that calls delete[] and gives you operator[], though std::vector is usually the better answer.
What problem does enable_shared_from_this solve?
Sometimes a member function needs to hand out a shared_ptr to itself — registering a callback, or keeping itself alive for the duration of an async operation. Writing shared_ptr<T>(this) creates a second, independent control block, so two owners each think they're the only one and the object is destroyed twice.
Inheriting publicly from std::enable_shared_from_this<T> gives the object a weak_ptr that the first shared_ptr construction populates, and shared_from_this() returns a shared_ptr sharing that same control block. The precondition is the thing to state: the object must already be owned by a shared_ptr. Calling it from a constructor, or on a stack object, throws std::bad_weak_ptr. That's why the pattern is usually paired with a private constructor and a static create() factory returning shared_ptr<T>, and why the async idiom is [self = shared_from_this()] in the lambda capture.
3.3Move Semantics and Value Categories
Explain value categories: lvalue, rvalue, xvalue, prvalue, glvalue.
Two independent properties: does the expression have identity (can you take its address / does it name something), and can it be moved from. lvalue = identity, not movable (a named variable). prvalue = no identity, movable (a literal, a function call returning by value). xvalue = identity and movable — an lvalue you've given permission to cannibalise, which is exactly what std::move(x) produces. glvalue is the union of lvalue and xvalue; rvalue is the union of xvalue and prvalue.
The rule that matters in practice: T&& binds to rvalues, const T& binds to everything, and overload resolution prefers the T&& overload for rvalues — which is how a container knows it may steal from a temporary but must copy from a named variable. The famous gotcha follows directly: inside void f(T&& x), the expression x is an lvalue (it has a name), so passing it onward copies unless you write std::move(x).
What does std::move actually do?
Nothing at runtime. It is a cast: static_cast<T&&>, an unconditional conversion of its argument to an xvalue. It moves no bytes and touches no memory. What it does is change overload resolution at the call site, so the move constructor or move assignment is selected instead of the copy version — and those are what actually transfer ownership of the internals.
Three corollaries that make good interview answers. Moving a type with no move operations (an int, a POD struct) silently copies — std::move is a request, not a guarantee. Moving out of a const object copies, because you can't steal from something you promised not to modify. And return std::move(local) is a pessimisation, not an optimisation: it turns a name into an xvalue and thereby disables NRVO, which would have constructed the object in place with no move at all.
What are RVO, NRVO, and guaranteed copy elision?
Return value optimisation constructs the returned object directly in the caller's storage instead of constructing a temporary and then copying or moving it. RVO applies to returning an unnamed temporary (return T(args);); NRVO applies to returning a named local (return local;). Both eliminate the copy/move entirely — and notably they may elide even when the copy constructor has side effects, which is why elision was carved out as a special exemption from the as-if rule.
Since C++17, elision for the prvalue case is mandatory: T x = T(args); and return T(args); perform no copy or move at all, and this now works even for types that are neither copyable nor movable. NRVO remains an optimisation the compiler is permitted but not required to perform. The actionable version: return locals by value and by name, don't wrap them in std::move, and don't contort a function signature to "avoid a copy" that the compiler was already going to elide.
What is a forwarding reference, and when do you use std::forward vs std::move?
T&& is a forwarding (or universal) reference only when T is a template parameter being deduced from that same parameter — then reference collapsing gives you T = U& for lvalue arguments and T = U for rvalues, so one signature binds both. std::vector<T>&& or auto&& inside a deduced context are forwarding references; std::string&& in a non-template function is a plain rvalue reference.
std::forward<T>(x) is a conditional move: it restores the original value category, moving only if the caller passed an rvalue. So the rule is mechanical — std::move when you know the thing is expendable, std::forward<T> inside a template that is passing an argument through and must preserve what the caller intended. Getting this wrong by writing std::move in a forwarding wrapper silently steals from the caller's live lvalue, which is one of the nastiest bugs in the language because the caller's code didn't change.
template <typename... Args>
auto make(Args&&... args) {
return Widget(std::forward<Args>(args)...); // preserve each category
}After you move from an object, what is its state?
"Valid but unspecified." The object is still a live object — its destructor will run and must not misbehave — but its value is whatever the move operation left behind, and you may not assume anything about it. So you may safely destroy it, or assign a fresh value to it, or call any operation whose precondition doesn't depend on the current value (clear(), size()). You may not call something with a precondition, like front() or pop_back(), without checking first.
Standard-library types are stricter than the minimum in practice: a moved-from vector, string, or unique_ptr is empty/null in every real implementation, and unique_ptr guarantees null. But for your own types, the design guidance is to leave the source in a cheap, well-defined state (usually the default-constructed one) rather than "whatever the swap left," because it makes the class safe to reuse and makes assertions possible. And when writing a move assignment, handle self-move-assignment — x = std::move(x) can happen through references and must not destroy the object.
Why does noexcept on a move constructor change vector's performance?
Because vector must give the strong exception guarantee on reallocation: if moving elements to the new buffer throws halfway, it can't put the old buffer back together, since the already-moved elements have been gutted. So vector::resize/reserve/push_back checks std::is_nothrow_move_constructible: if the move can throw, it copies every element instead, because a failed copy leaves the source intact and can be rolled back.
The practical effect is a type whose moves are cheap but not marked noexcept silently gets copy performance in every vector growth — a large, invisible regression. So: mark move constructors and move assignment noexcept whenever they genuinely cannot throw (which is almost always, since they're usually pointer swaps), and likewise swap and destructors. The flip side: noexcept is a hard promise. If an exception does escape a noexcept function, std::terminate is called — no unwinding, no catch.
When is returning a reference from a function correct, and when is it a bug?
Correct when the referent outlives the call: a reference to a member of *this (that's what vector::operator[] and a getter do), to something the caller passed in, or to an object with static storage duration. A bug whenever the referent is an automatic local or a temporary — the classic const std::string& name() { return first + " " + last; }, which returns a reference to a temporary that dies at the semicolon, and typically "works" in testing because the stack memory hasn't been reused yet.
Two modern versions of the same bug that are worth naming, because they catch people who know the classic one. std::string_view and std::span are non-owning, so returning one that views a local, or writing std::string_view sv = get_string(); where get_string returns by value, dangles immediately. And range-based for over a temporary sub-object — for (auto c : get_obj().items()) — dangled before C++23 fixed lifetime extension for the loop's range initialiser. In review, "does this reference outlive the thing it points at" deserves the same reflex as "who owns this."
3.4Classes, Inheritance, Polymorphism
How does virtual dispatch work, and what does it cost?
Each polymorphic class gets a static, per-class table of function pointers (the vtable), and each object of such a class carries a hidden pointer to its class's vtable (the vptr, conventionally at offset 0). A virtual call loads the vptr, indexes a known slot, and calls through it — so the cost is an extra load plus an indirect call.
The load is usually cheap because the vptr is hot; the real cost is that an indirect call through a target the branch predictor can't resolve stalls the pipeline, and it blocks inlining, which in turn blocks every optimisation that inlining would have enabled. That last effect typically dominates and doesn't show up in a naive microbenchmark. Per-object space cost is one pointer, which matters for small objects in large arrays. When it's genuinely hot, the escapes are: mark the class or method final so the compiler can devirtualise, use CRTP for static polymorphism, or use a std::variant plus std::visit for a closed set of types.
What happens if you call a virtual function from a constructor or destructor?
You get the base-class version, not the override. During Base's constructor the derived part doesn't exist yet, so the vptr still points at Base's vtable; during Base's destructor the derived part has already been destroyed, so it points back at Base's again. This is deliberate — the alternative would be dispatching to an override that reads uninitialised or already-destroyed derived members. If the function is pure virtual with no definition, it's undefined behaviour (in practice a "pure virtual method called" abort).
The consequence is that the "call a virtual init() from the base constructor" pattern silently doesn't work. The fixes: two-phase construction with an explicit init() the caller invokes after construction, a static factory that constructs then initialises, or pass the varying behaviour in as a constructor argument instead of getting it via override.
What is object slicing?
Assigning or copying a derived object into a base-class value keeps only the base sub-object — the derived data is sliced off and, because the copy is a genuine Base, virtual calls on it dispatch to Base. It's silent: Base b = derived; and void f(Base b) both compile without a warning and produce an object that has quietly lost its identity.
The most common real-world instance is std::vector<Base> holding derived objects — every push_back slices, and polymorphism is gone. Store std::vector<std::unique_ptr<Base>> instead. The prevention rules: pass polymorphic types by reference or pointer, never by value; and if a class is a polymorphic base, consider deleting its copy operations (or making them protected) so slicing becomes a compile error rather than a behaviour change.
Overloading vs overriding vs name hiding — and why do override and final matter?
Overloading: same name, different parameters, same scope, resolved at compile time. Overriding: a derived class replaces a base virtual with a matching signature, resolved at run time. Hiding: a derived member with the same name as a base member hides all base overloads of that name, regardless of signature — so adding one overload in the derived class makes the base's other overloads unreachable unless you write using Base::f;.
This is exactly why override exists. Without it, a signature mismatch — a missing const, int vs long, a differing reference qualifier — makes your function a brand-new virtual that never gets called, and it compiles cleanly. override turns that into a compile error. final forbids further overriding (or, on a class, further derivation), which both documents intent and lets the compiler devirtualise. Use override on every override, and drop the redundant virtual keyword there.
What is the diamond problem and what does virtual inheritance do about it?
If B and C both derive from A, and D derives from both, then D contains two separate A sub-objects. References to A's members from D are ambiguous, and a conversion to A* is ambiguous too — you have two of them, with independent state.
Declaring class B : virtual A and class C : virtual A makes the A sub-object shared: one copy, initialised by the most-derived class (D) rather than by B or C. The cost is real — access to virtual base members goes through an offset stored in the vtable, since the offset from a B* to the shared A depends on the runtime type; reinterpret_cast and pointer arithmetic games become invalid; and construction order gets subtle. The design advice: reserve multiple inheritance for mixing in stateless interfaces (pure-virtual classes), where the diamond never arises, and prefer composition for anything with state.
Abstract class vs interface in C++, and can a pure virtual function have a body?
C++ has no interface keyword. An abstract class is any class with at least one pure virtual function (= 0), which makes it non-instantiable; the "interface" idiom is an abstract class with only pure virtuals, no data members, and a virtual (or protected non-virtual) destructor.
And yes, a pure virtual function can have a definition — it just can't be called through normal dispatch on an object of the abstract type; a derived override calls it explicitly as Base::f(). That's how you say "you must override this, but here's the shared part you should call." The one place it's required: a pure virtual destructor still needs a body, because the derived destructor always calls it. Design-wise, prefer narrow interfaces — a class implementing a five-method interface where it only needs two is a Liskov/interface-segregation problem, and in modern C++ a concept or a std::function member is often a lighter way to express "something callable that does X" than an inheritance hierarchy.
static_cast vs dynamic_cast vs the other casts — and what does RTTI cost?
static_cast converts at compile time using known type relationships: numeric conversions, up-casts, and down-casts you assert are valid. A wrong down-cast is UB with no diagnostic. dynamic_cast checks at run time using RTTI: it returns nullptr for a failed pointer cast and throws std::bad_cast for a failed reference cast, and it's the only one that handles cross-casts and virtual bases correctly. const_cast only adds or removes const/volatile — and writing through it to an originally-const object is UB. reinterpret_cast reinterprets the bit pattern and is almost always the wrong tool outside of hardware-facing code (see also strict aliasing; use std::bit_cast or memcpy for type punning).
Cost: dynamic_cast is not a constant-time flag check — a typical implementation walks the inheritance graph comparing type_info, so it can be tens to hundreds of nanoseconds and is unattractive in a hot loop. Enabling RTTI also adds type_info records to your binary, which is why some embedded and game codebases build with -fno-rtti. If you're reaching for dynamic_cast frequently, that's usually a design signal: add a virtual function so the object decides, or model the closed set with std::variant and std::visit, which gets you compile-time exhaustiveness and no RTTI.
Inheritance or composition? And what is CRTP for?
Prefer composition. Public inheritance is an is-a claim that binds you to the base's interface and its invariants forever, and it's the tightest coupling the language offers — a protected member is effectively public to every future derived class. Composition (hold a member, expose what you want) keeps the coupling to the part you actually use, is easier to change, and doesn't force you to think about slicing, virtual destructors, or the diamond. Reach for inheritance when you genuinely need run-time substitutability through a base pointer.
CRTP — class Derived : public Base<Derived> — is the third option: the base knows its derived type at compile time, so it can call into the derived implementation with no vtable and full inlining. It's how you build mixins and static interfaces (enable_shared_from_this is one). The trade-offs are that there's no common base type to store heterogeneously (every instantiation is an unrelated type), and it moves errors into template diagnostics. Since C++20, concepts cover much of what CRTP was used to police, with dramatically better error messages.
3.5Templates and Compile-Time Programming
Why do template definitions have to live in headers?
Because a template isn't code — it's a recipe. The compiler can only emit machine code for vector<int> once it sees both the template definition and the argument int, and it does that per translation unit, with the linker later folding the duplicates. If the definition sits in a .cpp the compiler never sees it at the point of use, and you get an undefined-reference link error rather than a compile error.
The two escapes: put the definitions in the header (or in an -inl.h/.tpp the header includes at the bottom, which is purely cosmetic), or use explicit instantiation — declare extern template class Foo<int>; in the header and write template class Foo<int>; in one .cpp, which works when the set of instantiations is closed and is a real build-time win for heavily used templates. C++20 modules change this picture properly, since a module interface can export a template without textual inclusion.
Template specialisation: full, partial, and how does it interact with overloading?
Full (explicit) specialisation pins every parameter: template<> struct Hash<std::string> { ... };. Partial specialisation pins some or constrains their shape: template<typename T> struct Hash<T*> { ... };. Class templates support both; function templates support only full specialisation — you express the partial case with an extra overload instead.
And that asymmetry is a trap, because overload resolution runs first and only then picks a specialisation of the chosen template. So adding a plain overload after a full specialisation can make the specialisation unreachable, which is Sutter's classic advice: don't specialise function templates, overload them. The tidy modern answer to most of this is to constrain with if constexpr or requires clauses inside one function rather than scattering specialisations, because the dispatch logic then reads top-to-bottom in one place.
What is SFINAE, and what replaces it in C++20?
Substitution Failure Is Not An Error: if substituting template arguments produces an invalid type or expression in the immediate context of the signature, that candidate is quietly removed from the overload set instead of causing a compile error. That's the mechanism behind std::enable_if, the detection idiom, and every "does this type have a .size()" trick of the C++11/14 era. Note the limits: only the signature counts, not the body (an error inside the body is a hard error), and it detects only ill-formedness, not semantic wrongness.
C++20 concepts replace essentially all of it. template <std::integral T> void f(T); says what enable_if_t<is_integral_v<T>, void> said, but it's readable, it participates in overload ordering via subsumption (a more-constrained overload wins, so you no longer need mutually-exclusive enable_if conditions), and the diagnostic says "constraint not satisfied: T does not model integral" rather than four screens of substitution trace. If you're maintaining pre-C++20 code you still need to read SFINAE; if you're writing new code, use concepts.
const vs constexpr vs consteval vs constinit.
const means "I won't modify it" — a promise about use, not about when the value is known; const int n = read(); is fine. constexpr on a variable means the value must be computable at compile time (and implies const); on a function it means "usable in a constant expression if the arguments allow," so a constexpr function called with runtime arguments simply runs at runtime. consteval (C++20) is the stricter version: the function must be evaluated at compile time, and a runtime call is an error. constinit (C++20) says a static-storage variable must be constant-initialised — it doesn't make it const, it just guarantees no dynamic initialisation, which is a targeted fix for the initialisation-order fiasco.
What's worth adding is how much constexpr now covers: since C++20 it includes allocation, std::vector and std::string, virtual calls, try/catch, and std::is_constant_evaluated() for taking a different branch at compile time. So compile-time computation is no longer the metaprogramming puzzle it was in C++11 — you write ordinary code and mark it constexpr.
How do variadic templates work, and what are fold expressions?
A parameter pack typename... Ts holds zero or more types, Ts... args the corresponding values, and sizeof...(Ts) its count. Pre-C++17 you consumed a pack with recursion — peel off the head, recurse on the tail, terminate with a base-case overload. That works but generates a distinct instantiation per depth, which is slow to compile.
C++17 fold expressions collapse the common cases to one line: (args + ...) sums, (f(args), ...) calls f on each in order (comma fold, sequencing guaranteed), (... && preds) short-circuits. Combined with if constexpr, most recursive pack handling disappears:
template <typename... Ts>
void log(const Ts&... xs) {
((std::cout << xs << ' '), ...); // fold over comma, left to right
std::cout << '\n';
}The main real-world use you should be able to name is perfect-forwarding factories — make_unique, emplace_back, std::thread's constructor all take Args&&... and std::forward the pack through.
What is type erasure, and what does std::function cost?
Type erasure stores an object of unknown static type behind a uniform interface, keeping the operations and discarding the type. std::function, std::any, std::shared_ptr's deleter, and a hand-rolled Base* hierarchy are all instances. The usual implementation is a small buffer plus a pointer to a table of function pointers for the operations you kept (call, copy, destroy).
std::function's costs: an indirect call that can't be inlined, a possible heap allocation when the callable doesn't fit the small-object buffer (typically 16–32 bytes — capture a few things by value and you're allocating), and copy semantics that may allocate again. In a hot loop that's often 5–10× a direct call. The alternatives, in order of preference: a template parameter (template <typename F> void each(F f)) which inlines fully and costs nothing; std::function_ref/std::move_only_function or a hand-rolled non-owning callable_ref when you only need to borrow; std::function when you genuinely need to store a heterogeneous callable with value semantics, which is a real and common need.
auto, decltype, decltype(auto) — and where does deduction bite?
auto deduces using template-argument rules, which means it strips references and top-level cv-qualifiers: auto x = obj.get_ref(); gives you a copy, silently, even if get_ref returned T&. Write auto& or auto&& when you want to bind rather than copy. decltype(expr) asks "what is the declared type of this expression," and it preserves references — note that decltype(x) for a variable gives its declared type, while decltype((x)) with the extra parens gives T&, because a parenthesised name is an lvalue expression. decltype(auto) as a return type deduces with decltype rules, which is how a perfect-forwarding wrapper returns a reference when the wrapped call did.
The bites worth knowing: auto x{1,2} vs auto x = {1,2} and the whole std::initializer_list special case; auto with a proxy type, where auto b = vec_of_bool[0] gives you a proxy reference that dangles rather than a bool; and const auto& s = map["k"]-style binding to a temporary created by an implicit conversion, which lifetime-extends the temporary but not any object it referred to. As a rule: auto for iterators, lambdas, and long template types; spell the type out when the type is part of the interface you want readers to see.
3.6The Standard Library in Practice
How does std::vector grow, and when do iterators become invalid?
size() is how many elements exist, capacity() is how many fit before reallocating. On overflow it allocates a larger buffer (typically 1.5× or 2×), moves the elements, and frees the old one — geometric growth is what makes push_back amortised O(1). reserve(n) pre-allocates so a known-size fill does one allocation; shrink_to_fit is a non-binding request; and the only way to actually release capacity portably is vector<T>(v).swap(v).
Reallocation invalidates everything: all iterators, pointers, and references into the vector. Without reallocation, insert/erase invalidates from the modified position onward. The bug this produces in real code is a loop that holds a reference (or worse, a pointer to an element in another data structure) across a push_back, and it's a use-after-free that behaves fine until the vector happens to be at capacity. Two related notes worth having ready: std::deque keeps references valid on end-insertion (though not iterators), and std::vector<bool> is not a container of bool — it's a bit-packed specialisation returning proxy references, so use std::vector<char> or std::bitset when you want normal semantics.
std::map or std::unordered_map?
map is a balanced binary tree: O(log n), keys kept in sorted order, supports range and nearest-key queries (lower_bound), needs only operator<, and never invalidates existing iterators on insert. unordered_map is a hash table: O(1) average and O(n) worst case, no ordering, needs hash and operator==, and rehashing invalidates iterators (though not references to elements, since nodes aren't moved).
Default to unordered_map when you only need lookup, and to map when you need ordering or predictable worst-case latency. But the answer that shows experience is the third option: for small collections, or a container you build once and then only search, a sorted std::vector<std::pair<K,V>> with std::lower_bound beats both, often by a lot, because node-based containers scatter one allocation per element across the heap and every probe is a cache miss. Both standard maps are node-based by mandate — that's why absl::flat_hash_map and friends exist and typically outperform unordered_map substantially.
push_back vs emplace_back, insert vs emplace vs try_emplace.
push_back takes a built object and copies or moves it in; emplace_back takes constructor arguments and builds the element in place, saving a move and, for a non-movable type, making the operation possible at all. For an already-constructed lvalue there's no difference worth caring about, and push_back is marginally safer because it won't silently invoke an explicit or narrowing conversion the way perfect forwarding into a constructor can.
For maps the distinction is sharper. map::emplace may construct the pair — and therefore the value — before discovering the key already exists, then throw the work away; that's wasted allocation, and for a move-only value it's worse, since the argument may have been consumed. try_emplace (C++17) checks the key first and only constructs on insertion, and insert_or_assign covers the update case. Also remember operator[] on a map default-constructs a missing element, which makes if (m["k"] == v) a mutation; use find, contains, or at to read.
What is small-string optimisation, and what makes string_view dangerous?
SSO stores short strings (typically up to 15 or 22 bytes depending on the implementation) inside the std::string object itself, with no heap allocation. That's why sizeof(std::string) is 24–32 bytes, why short-string workloads are far faster than a naive "every string is an allocation" model predicts, and why moving a string is not always cheap — a short one is copied byte-for-byte, not stolen.
std::string_view is a pointer plus a length: it copies nothing and, crucially, owns nothing. Passing string_view as a read-only parameter is excellent, and it accepts std::string, const char*, and literals with no conversion allocation. The danger is that every lifetime bug is now silent: std::string_view sv = returns_string(); dangles at the semicolon; storing a string_view as a class member outlives its source sooner or later; and a string_view is not guaranteed null-terminated, so handing .data() to a C API that expects a C string reads past the end. Rule of thumb: string_view for parameters, std::string for members and return values.
Explain iterator categories and the erase-remove idiom.
Categories describe what an iterator supports, and they determine which algorithms are available and at what complexity: input/output (single pass), forward (multi-pass, ++), bidirectional (also --; list, map), random access (+n, [], subtraction; vector, deque, array), and contiguous (C++20, elements are adjacent in memory). This is why std::sort requires a vector-like container and std::list has its own sort member.
The erase-remove idiom exists because algorithms operate on iterators and can't resize a container. std::remove shuffles the surviving elements forward and returns the new logical end, leaving the tail in a moved-from state, so you must follow with erase:
v.erase(std::remove_if(v.begin(), v.end(), pred), v.end());
std::erase_if(v, pred); // C++20: same thing, one callCalling remove_if alone and forgetting the erase is a classic bug — size() is unchanged and the tail holds duplicates or hollowed-out objects. C++20's std::erase/std::erase_if free functions make this a non-issue for new code.
What are optional, variant, span, and tuple for?
std::optional<T> is "a T or nothing," stored inline with no allocation — the right return type for a lookup that can fail, replacing sentinel values, out-parameters plus a bool, and null pointers for the non-owning-absent case. std::variant<A,B,C> is a type-safe tagged union: it always holds exactly one alternative, you consume it with std::visit (which gets you compile-time exhaustiveness) and it throws bad_variant_access on a wrong get. It's the closed-set alternative to an inheritance hierarchy, with no allocation and no vtable.
std::span<T> is a non-owning view over contiguous memory — pointer plus length. It's the correct parameter type for "some contiguous range of T," and it collapses the C-style (T* p, size_t n) pair into one object that can't get out of sync, while accepting vector, array, and C arrays alike. It has the same lifetime hazard as string_view. std::tuple is a heterogeneous fixed-size collection, useful for returning several values (though a named struct is usually more readable) and for generic code that manipulates parameter packs; auto [a, b] = f(); structured bindings are what made it tolerable to use.
3.7Concurrency and the Memory Model
std::thread, std::jthread, std::async — and what happens if you don't join?
A std::thread must be either joined or detached before it's destroyed; destroying a joinable thread calls std::terminate. That's a deliberately harsh design, because the alternatives (implicit join, implicit detach) both hide bugs. std::jthread (C++20) fixes the ergonomics: it joins in its destructor and carries a std::stop_token so cooperative cancellation is built in. Prefer it.
std::async returns a std::future and, with the default launch policy, may run either asynchronously or lazily on the calling thread at .get() — so pass std::launch::async explicitly if you actually want concurrency. Its sharp edge is that the future returned by std::async blocks in its destructor until the task finishes, which means std::async(f); as a statement is synchronous, and a discarded future can stall a function you thought was fire-and-forget. In production code, a thread pool with an explicit queue beats raw threads for anything with more tasks than cores, because thread creation is on the order of tens of microseconds and unbounded thread counts thrash the scheduler.
Data race vs race condition, and what exactly does a mutex guarantee?
A data race is a precise, formal thing: two threads access the same memory location, at least one writes, and there's no synchronisation ordering them. It's undefined behaviour — not "you get a stale value," but UB, which is why a racy loop can be optimised into an infinite one. A race condition is a logic bug where correctness depends on timing; you can have one with perfect locking, e.g. a check-then-act across two separately-locked calls.
A mutex gives you two things, and people usually only name the first. Mutual exclusion: one holder at a time. And an ordering/visibility guarantee: everything written before an unlock is visible to a thread that subsequently locks the same mutex — the unlock is a release and the lock is an acquire, which is what stops the compiler and CPU from reordering your writes out of the critical section. Without that half, mutual exclusion alone wouldn't make shared data correct on a modern out-of-order machine with per-core caches.
Explain std::atomic memory orders. When would you use anything but seq_cst?
memory_order_seq_cst is the default: all seq_cst operations appear in one global total order that every thread agrees on. It's the easiest to reason about and the most expensive (on x86 a store needs an mfence-class instruction; on ARM/POWER the cost is larger). acquire/release is the pairwise version: a release store synchronises-with an acquire load of the same variable, so everything written before the release is visible after the acquire — exactly the mutex guarantee, which is why this pair implements most lock-free handoffs. relaxed guarantees atomicity only, with no ordering at all. consume is effectively deprecated; compilers promote it to acquire.
When to leave the default: a statistics counter you only read after joining is a textbook relaxed fetch_add, since nobody depends on ordering. A single-producer/single-consumer ring buffer or a published-pointer handoff is the textbook acquire/release case, and often measurably faster. Otherwise, start at seq_cst and only weaken with a benchmark and a written-down argument — relaxed-atomics bugs are non-deterministic, architecture-dependent, and often invisible on x86 (whose hardware model is already strong) while failing on ARM.
What causes deadlock and how do you prevent it?
Four conditions must hold simultaneously: mutual exclusion, hold-and-wait, no preemption, and circular wait. Breaking any one prevents deadlock, and the practical target is circular wait.
Concretely: (1) impose a global lock ordering and always acquire in that order — the most reliable fix, and it can be checked at runtime in debug builds; (2) acquire multiple locks atomically with std::scoped_lock(m1, m2), which uses a deadlock-avoidance algorithm internally; (3) don't hold a lock while calling anything you don't control — a callback or virtual call under a lock can re-enter and take locks you've never heard of; (4) use try_lock with a back-off where a retry is acceptable. Always use RAII lock guards (lock_guard, unique_lock, scoped_lock) rather than manual lock/unlock, so an early return or an exception can't skip the release. And note that std::mutex is not recursive — relocking it on the same thread is UB; wanting recursive_mutex is usually a sign that a public locking function is calling another public locking function, which is better fixed by splitting out a private unlocked implementation.
Why must you use a predicate loop with condition_variable?
Because of spurious and stolen wakeups. A wait may return without any notify at all (spurious — permitted by the standard and real on POSIX), and even with a genuine notify another thread may have consumed the state you were woken for before you re-acquired the mutex (stolen). So the condition must be re-checked after every wake:
std::unique_lock lk(m);
cv.wait(lk, [&]{ return !queue.empty() || done; }); // predicate overload loops for youTwo more rules that come up. Modify the shared state while holding the mutex and then notify; if you set the flag without the lock, a waiter can evaluate the predicate as false and go to sleep between your write and your notify, and the wakeup is lost forever. And use notify_all when waiters are waiting on different predicates — with notify_one the runtime may wake a thread whose condition is still false, which then goes back to sleep while the thread that could have made progress stays blocked.
What is false sharing, and is volatile the same as atomic?
False sharing is two threads writing to different variables that happen to sit in the same cache line (typically 64 bytes). There's no logical contention, but the hardware's coherence protocol works at cache-line granularity, so each write invalidates the other core's copy and the line ping-pongs — a per-thread counter array can run an order of magnitude slower than the same code with padding. The fix is to separate them: pad or align to std::hardware_destructive_interference_size, or better, keep a thread-local accumulator and merge once at the end.
And no, volatile is not atomic and gives you no thread-safety at all. It tells the compiler not to elide or reorder that variable's accesses — which is what memory-mapped hardware registers and signal handlers need. It does not make the access atomic (a volatile int64_t on a 32-bit target can still tear), and it emits no fences, so it orders nothing with respect to other variables. For inter-thread communication use std::atomic, which gives you atomicity and a specified ordering. The rule: volatile for hardware and signals, atomic for threads.
if (!q.empty()) q.pop(); is still a race, and now it's a race hidden behind an interface that claims to be safe. Say that out loud: the right granularity is the operation the caller actually needs to be atomic, which usually means designing the interface (try_pop() returning an optional) rather than wrapping the old one.unique_ptr and let ownership be visible in the type; mark move operations noexcept and let RVO do the rest; and reach for a sanitizer build rather than an argument when someone claims a lifetime is fine. Every one of them replaces a memorised rule with a mechanism that keeps working when you've forgotten it.