C++ Interview Questions · 2026

C++ Interview Questions (2026): Most Asked, With Answers

A candidate at a trading firm's infra team spent four minutes of a forty-minute interview staring at a segfault he'd triggered himself, on a whiteboard, with a function that returned a reference to a local std::string. He'd shipped C++ in production for six years. The bug wasn't exotic, it was the single most common way junior and mid-level engineers corrupt memory without realizing it, and the interviewer wasn't testing whether he knew the term "dangling reference." She was testing whether he'd actually internalized what a stack frame is, versus memorized the phrase "don't return references to locals" from a code review comment two jobs ago. C++ still sits at 21.8 percent usage among professional developers in the 2025 Stack Overflow survey, tenth among all languages and the highest-ranked systems language on the list (Stack Overflow, 2025). It's not going anywhere, and the interviews for it have gotten pickier, not easier, as the language itself has kept adding features on top of forty years of backward compatibility.

Here's the pattern I'd flag if you're prepping: most C++ interview guides spend disproportionate time on syntax trivia, virtual versus non-virtual destructors, the exact rules for operator overloading, because it's easy to write a quiz question about syntax. The questions that actually separate a strong C++ engineer from someone who's memorized the FAQ are about ownership: who allocated this memory, who's responsible for freeing it, and what happens to that answer the moment an exception gets thrown in the middle of a function. RAII is the whole answer to that question, and a shocking number of candidates who use std::unique_ptr every day can't explain why it exists in terms of the problem it solves, only that "it's the modern way."

This page covers C++ interview questions across eight areas: memory management, pointers, references and RAII; classes, inheritance and virtual dispatch; move semantics and rvalue references; templates and generic programming; STL containers, iterators and algorithms; concurrency and multithreading; the modern C++11 through C++23 feature set; and undefined behavior and the edge cases that show up in a real code review. Code examples are C++ throughout, compiled against C++17 or later unless a question specifically calls out an older or newer standard.

50Questions
RAII & OwnershipCore Concept
C++ CodeFormat
21.8%2025 Pro Dev Usage

Memory management, pointers, references, and RAII

Every C++ loop starts here, no matter how senior the candidate is on paper. It's the section where a shaky answer costs you the most credibility, because it's assumed baseline knowledge.

Easy questions

15

A pointer is a variable that stores a memory address, it can be reassigned to point somewhere else, it can be null, and you need to dereference it explicitly with * or ->. A reference is an alias for an existing object, it must be bound at declaration, it can never be reseated to refer to a different object afterward, and it can never be null in well-defined code.

cpp
int a = 1, b = 2;
int* p = &a;
p = &b; // legal, p now points at b

int& r = a;
r = b; // this assigns b's value INTO a, it does not rebind r

That last line trips people up constantly. r = b looks like it should make r refer to b, and it doesn't, it copies b's value into whatever r is already bound to.

RAII stands for Resource Acquisition Is Initialization. The idea is that a resource, heap memory, a file handle, a mutex lock, gets acquired in a constructor and released in the matching destructor, so the resource's lifetime is tied directly to an object's scope. When that object goes out of scope, whether through normal return or an exception unwinding the stack, its destructor runs and the resource gets released automatically.

cpp
void process() {
 std::lock_guard<std::mutex> lock(mtx); // acquires
 doWork(); // if this throws, lock still releases
} // lock released here, guaranteed

It matters more in C++ because there's no garbage collector cleaning up after you eventually. Without RAII, an exception thrown between acquiring a resource and manually releasing it leaks that resource every time. RAII makes cleanup a language guarantee tied to scope, not a discipline you have to remember to apply correctly on every exit path.

Stack allocation happens automatically for local variables, it's extremely fast because it's just moving a stack pointer, and the memory is reclaimed automatically when the variable goes out of scope. Heap allocation, via new or a smart pointer wrapping it, gives you memory that outlives the function that created it, but you're responsible for its lifetime and it costs more, both in allocation time and in cache locality.

Default to the stack. Reach for the heap when an object needs to outlive its creating scope, when its size isn't known at compile time, or when it's genuinely large enough that copying it around the stack would be wasteful. A lot of code I've reviewed heap-allocates small, short-lived objects out of habit picked up from Java or C#, where that distinction doesn't exist the same way.

std::unique_ptr owns the object it points to exclusively, and it deletes that object automatically when the unique_ptr itself goes out of scope. It can't be copied, only moved, which enforces single ownership at compile time rather than by convention. A raw pointer gives you none of that, it's just an address, with no attached information about who owns it or when, or whether, it should be freed.

cpp
std::unique_ptr<Widget> makeWidget() {
 return std::make_unique<Widget>();
}

auto w1 = makeWidget();
auto w2 = w1; // compile error, can't copy
auto w3 = std::move(w1); // fine, ownership transfers, w1 is now null

Marking a member function virtual tells the compiler to resolve calls to that function through a vtable, a hidden per-class table of function pointers, looked up at runtime based on the object's actual dynamic type, rather than resolving the call at compile time based on the static type of the pointer or reference used to call it.

cpp
struct Animal {
 virtual std::string speak() const { return "..."; }
};
struct Dog : Animal {
 std::string speak() const override { return "Woof"; }
};

Animal* a = new Dog();
a->speak(); // "Woof", resolved at runtime via the vtable

Without virtual, that same call would resolve at compile time based on the static type Animal*, and you'd get whatever Animal::speak returns regardless of the object's actual dynamic type.

override, added in C++11, doesn't change runtime behavior at all. It's a compile-time check: it tells the compiler this function is meant to override a virtual function in a base class, and if the signature doesn't actually match any virtual function in the base, the compiler rejects the code instead of silently compiling a brand-new, unrelated function.

cpp
struct Base {
 virtual void draw(int x) {}
};
struct Derived : Base {
 void draw(float x) override {} // error, doesn't match Base::draw's signature
};

Before override existed, that exact mistake, a small signature mismatch like int versus float, compiled cleanly and just silently failed to override anything, leaving a dead function sitting next to the real virtual one. It's the kind of bug that sits undetected for months until someone calls through a base pointer expecting the derived behavior and gets the base's instead.

A pure virtual function, declared with = 0, has no implementation in that class and forces every concrete derived class to provide one, since a class with even one pure virtual function is abstract and can't be instantiated directly. A virtual function with an empty body is a perfectly instantiable, concrete function, derived classes are free to override it or just inherit the empty no-op behavior.

cpp
struct Shape {
 virtual double area() const = 0; // pure virtual, Shape can't be instantiated
};
struct Circle : Shape {
 double radius;
 double area() const override { return 3.14159 * radius * radius; }
};

Shape s; // compile error
Circle c{5}; // fine

Nothing, at runtime. std::move doesn't move anything itself, it's a cast, it converts its argument to an rvalue reference so that overload resolution picks the move constructor or move assignment operator instead of the copy versions. All the actual work, whatever "moving" means for that specific type, happens inside the move constructor or move assignment operator that gets selected as a result.

cpp
std::string a = "hello";
std::string b = std::move(a);
// a is now unspecified but valid, likely empty
// b holds what a used to hold, no allocation or copy of the characters happened

This is exactly why calling std::move on something and then continuing to use the original variable as if nothing happened is a real bug, not a style nitpick. The moved-from object is guaranteed to be in a valid but unspecified state, valid meaning you can still destroy it or assign a new value to it safely, unspecified meaning you can't rely on what it currently contains.

The compiler generates a real, concrete function for that specific type, a process called instantiation, and compiles that generated function as if you'd written it by hand. Call the same template with three different types and the compiler produces three separate functions in the final binary, each one type-checked and compiled independently, not one generic function that branches on type at runtime.

cpp
template<typename T>
T max_of(T a, T b) { return a > b ? a : b; }

max_of(3, 5);    // instantiates max_of<int>
max_of(3.1, 2.9);  // instantiates max_of<double>
max_of<std::string>("a", "b"); // instantiates max_of<std::string>

std::vector stores elements contiguously, which makes random access O(1) and gives excellent cache locality for iteration, at the cost of O(n) insertion or deletion in the middle since everything after that point has to shift. std::list is a doubly linked list, insertion and deletion anywhere is O(1) once you have an iterator to the spot, but random access is O(n) and every node lives at a separate, possibly scattered, heap allocation.

In practice, reach for vector as the default almost always. Reach for list only when you're doing frequent insertion or deletion in the middle of a large sequence and genuinely don't need random access, which is rarer in real code than most people expect coming out of a data structures course. The cache-locality cost of list is large enough in practice that a vector with O(n) shifting frequently outperforms a list with O(1) insertion, once the sequence isn't huge.

std::find from the algorithms header works on any range given by two iterators, and it does a linear O(n) scan regardless of what container backs it, since it has no knowledge of the container's internal structure. Calling .find() directly as a member function of unordered_map or map uses that container's own lookup mechanism, hash-based O(1) average for unordered_map, tree-based O(log n) for map, which is dramatically faster for large containers.

cpp
std::unordered_map<std::string, int> m = { {"a", 1}, {"b", 2} };

std::find(m.begin(), m.end(), std::pair{"a", 1}); // O(n) linear scan, wrong tool here
m.find("a"); // O(1) average, the correct tool

Using the free function std::find on an associative container by mistake, instead of that container's member .find(), is a real performance bug that compiles cleanly and passes every unit test on small inputs.

The capture clause, the square brackets at the start of a lambda, decides how the lambda gets access to variables from its enclosing scope. Capturing by value, [x], copies the variable's current value into the lambda when it's created, and later changes to the original variable don't affect the lambda's copy. Capturing by reference, [&x], stores an actual reference, so the lambda always sees the variable's current value, including changes made after the lambda was created, but only as long as the original variable is still alive.

cpp
int count = 0;
auto byValue = [count]() { return count; };
auto byRef = [&count]() { return count; };

count = 5;
byValue(); // still 0, captured a copy at creation time
byRef();  // 5, sees the current value through the reference

Capturing by reference into a lambda that outlives the local variable, storing a lambda in a std::function that gets called after the enclosing function returns, is the same dangling-reference bug from the memory section, just wearing a different syntax.

Beyond typing less, auto prevents a whole class of subtle bugs caused by writing out a type slightly wrong, especially with iterator types and template return types that are genuinely painful or impossible to name correctly by hand. It also keeps code correct automatically when an underlying type changes, a function's return type gets refactored from int to int64_t, and every auto-declared caller adjusts with zero changes needed, while every explicitly-typed caller would need updating by hand.

cpp
std::map<std::string, std::vector<int>> data;
for (auto& [key, values] : data) { // structured binding + auto, C++17
 // without auto, this iterator type is genuinely painful to spell out correctly
}

Structured bindings, introduced in C++17, let you unpack a tuple, pair, struct, or array directly into named variables in one declaration, instead of accessing members by index or by .first/.second, which reads poorly and is easy to get backwards.

cpp
std::map<std::string, int> counts;

// before C++17
for (const auto& entry : counts) {
 std::cout << entry.first << ": " << entry.second;
}

// C++17
for (const auto& [name, count] : counts) {
 std::cout << name << ": " << count;
}

Reading or writing past the end of an array is undefined behavior, not a guaranteed crash. In practice, that memory location often just happens to belong to something else, another local variable, part of the stack frame, uninitialized heap memory, so the program reads or corrupts unrelated data silently and keeps running, sometimes for a long time, before the corruption surfaces somewhere completely unrelated to where it actually happened.

cpp
int arr[5] = {1, 2, 3, 4, 5};
arr[10] = 99; // undefined behavior, may silently corrupt an unrelated variable

This is exactly why an out-of-bounds bug is often harder to track down than a segfault would be. A crash tells you immediately something went wrong. Silent memory corruption tells you nothing until a symptom shows up somewhere downstream, potentially in a completely different function, minutes or files away from the actual mistake.

Medium questions

19

Reach for shared_ptr only when an object's lifetime genuinely needs to be shared across multiple owners that don't have a clear hierarchy, a cache that multiple independent subsystems can hold entries from, for example. The cost is real: shared_ptr maintains an atomic reference count, which means every copy and destruction does an atomic increment or decrement, and that's measurably slower under contention than a plain unique_ptr move, which touches no shared state at all.

My rule: default to unique_ptr everywhere, and only switch to shared_ptr when you can point at the specific reason ownership needs to be shared. A shocking amount of production code reaches for shared_ptr by default because it "feels safer," and pays the atomic-refcount tax on objects that only ever had one real owner the whole time.

A dangling reference or pointer refers to memory that has already been freed or gone out of scope. Using it afterward is undefined behavior, it might crash immediately, it might silently return garbage, or it might appear to work fine in testing and fail in production under different memory layout.

cpp
const std::string& getName() {
 std::string local = "temp";
 return local; // returns a reference to a local
} // local is destroyed here

// caller:
const std::string& name = getName(); // dangling, undefined behavior
std::cout << name; // may print garbage, may crash, may "work"

This exact pattern is the whiteboard bug I mentioned in the intro. Compilers will often warn about this specific case now, -Wreturn-local-addr or similar, but the underlying bug class, returning a reference or pointer into memory whose owner has already gone out of scope, shows up in far less obvious forms constantly, especially across container reallocation.

If you delete a derived object through a base class pointer and the base destructor isn't virtual, only the base class's destructor runs. The derived class's destructor, and by extension every resource it owns, never gets a chance to clean up. Making the base destructor virtual ensures deletion through a base pointer walks the full destructor chain from derived up to base, in the correct order.

cpp
struct Base {
 ~Base() { std::cout << "Base cleanupn"; } // not virtual, this is the bug
};
struct Derived : Base {
 int* data = new int[100];
 ~Derived() { delete[] data; std::cout << "Derived cleanupn"; }
};

Base* b = new Derived();
delete b; // only "Base cleanup" runs, data leaks, undefined behavior technically

The fix is one word, virtual ~Base(). It's a small enough change that people forget it constantly, and it's one of the first things a reviewer checks the moment they see a class designed to be inherited from.

The rule of three says if a class defines any one of a custom destructor, copy constructor, or copy assignment operator, it almost certainly needs to define all three, because needing one usually means the class manages a resource the compiler-generated defaults don't know how to copy or destroy correctly. C++11 added move semantics, so the rule grew to five: a move constructor and move assignment operator now belong in that same set, since a class managing a resource typically needs custom move behavior for the same reason it needs custom copy behavior.

cpp
class Buffer {
 int* data;
 size_t size;
public:
 Buffer(size_t n) : data(new int[n]), size(n) {}
 ~Buffer() { delete[] data; }
 Buffer(const Buffer& other); // copy ctor
 Buffer& operator=(const Buffer& other); // copy assign
 Buffer(Buffer&& other) noexcept; // move ctor
 Buffer& operator=(Buffer&& other) noexcept; // move assign
};

Slicing happens when you assign or pass a derived-class object into a variable or parameter typed as the base class by value. Only the base-class portion of the object gets copied, the derived-specific data members and behavior are literally sliced off, and any virtual call made through that copy resolves against the base class, not the original derived type.

cpp
void printSpeak(Animal a) { // by value, not by reference
 std::cout << a.speak();
}

Dog d;
printSpeak(d); // d is sliced down to Animal, prints the base's speak(), not Dog's

The fix is almost always to take a reference or pointer instead, const Animal& preserves the object's full dynamic type and lets virtual dispatch work correctly. Passing polymorphic types by value is close to always a mistake, and it's a fast way to reveal whether a candidate actually reasons about object layout or just writes function signatures by feel.

An rvalue reference, written T&&, binds to temporaries and other expressions that don't have a persistent memory location, values about to be destroyed anyway. A regular reference, T&, binds to lvalues, named variables and anything else with a stable address. The point of rvalue references is to let a function detect "this argument is a temporary I'm free to cannibalize" versus "this argument is a real variable someone else still needs."

cpp
void f(std::string& s);  // binds to lvalues only
void f(std::string&& s); // binds to rvalues only

std::string name = "ada";
f(name);      // calls the lvalue overload
f(std::string("x")); // calls the rvalue overload, x is a temporary
f(std::move(name)); // calls the rvalue overload, name is cast to an rvalue

After moving from an object, the standard guarantees it's left in a valid but unspecified state, valid meaning the object's invariants still hold and it's safe to call its destructor or assign a new value to it, unspecified meaning you cannot assume anything about its current contents. For std::string and std::vector specifically, moved-from usually, but not guaranteed to, means empty, since most implementations swap internal buffers rather than clearing them explicitly.

cpp
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);

v1.size();   // legal, some unspecified value, likely 0
v1.push_back(5); // legal, v1 is still a usable, valid vector
v1[0];     // legal only if v1 currently has an element at index 0

Template specialization lets you provide a different implementation for a specific type, or set of types, than the general template would generate. You'd write one when the general implementation is wrong or inefficient for a particular type, a generic swap that copies twice for a large object versus a specialized one that swaps internal pointers directly, for example.

cpp
template<typename T>
void printType(T val) { std::cout << "generic: " << val; }

template<>
void printType<bool>(bool val) { // full specialization for bool
 std::cout << (val ? "true" : "false");
}

Partial specialization, specializing for a family of types like all pointers or all std::vector<T> regardless of T, only works for class templates, not function templates. That asymmetry surprises people who assume specialization rules apply identically to both.

Concepts let you name a set of requirements a template parameter must satisfy, directly in the function signature, in plain readable syntax, instead of encoding the same constraint through a SFINAE trick that only reveals itself in a compiler error three template layers deep. The requirement gets checked at the point of instantiation, and a violation produces a clear message naming exactly which requirement failed.

cpp
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

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

add(3, 4);   // fine, int satisfies Numeric
add("a", "b"); // clear compile error naming the Numeric constraint, not a wall of template noise

std::map is a balanced binary search tree internally, typically a red-black tree, giving O(log n) lookup, insertion, and deletion, with keys always kept in sorted order during iteration. std::unordered_map is a hash table, giving average O(1) lookup and insertion, with no guaranteed iteration order at all.

Pick map when you actually need sorted iteration order, a leaderboard, a range query like "all keys between X and Y," or when your key type doesn't have a good hash function readily available but does have a natural ordering. Otherwise unordered_map wins on raw lookup speed for large data sets, and that's the more common default in performance-sensitive code.

push_back takes an already-constructed object, or something implicitly convertible to one, and copies or moves it into the container. emplace_back takes the constructor arguments directly and constructs the object in place inside the container's own memory, with no separate temporary object built and then copied or moved in.

cpp
std::vector<std::pair<int, std::string>> v;

v.push_back(std::make_pair(1, "hello")); // builds a temporary pair, then moves it in
v.emplace_back(1, "hello"); // constructs the pair directly inside the vector, no temporary

The difference matters most for expensive-to-construct types, especially anything with a nontrivial move constructor or none at all. For a small type like int, the difference is noise. Some teams reach for emplace_back everywhere by default, which is fine, but it's worth being able to explain what's actually happening rather than repeating "it's faster" without the mechanism behind it.

A data race happens when two or more threads access the same memory location concurrently, at least one of those accesses is a write, and there's no synchronization ordering one access before the other. The C++ standard defines this as undefined behavior, not merely "unspecified," which means the compiler is allowed to assume it never happens and optimize accordingly, including reordering, caching a value in a register instead of re-reading memory, or eliminating a check entirely.

cpp
bool ready = false;
int data = 0;

// thread A
data = 42;
ready = true;

// thread B
while (!ready) {} // compiler may cache 'ready' in a register and loop forever
if (ready) use(data); // 'data' may not be visible even if ready is

Without a mutex, atomic, or other synchronization primitive, the compiler has no obligation to make thread A's writes visible to thread B in any particular order, or at all, from thread B's point of view. This is the detail that trips people who think of a data race as "usually fine, occasionally wrong," when the standard actually permits the compiler to do something far stranger than that.

std::mutex provides mutual exclusion for an arbitrary block of code, only one thread can hold the lock at a time, and everything inside the locked region is protected regardless of how complex it is. std::atomic<T> provides lock-free (on most platforms, for the standard scalar types) atomic operations on a single variable specifically, no separate lock object, no risk of one thread blocking while holding it.

A mutex is the wrong tool when you're protecting a single counter or flag under high contention. Locking and unlocking a mutex for every increment of a shared counter costs far more than an atomic increment does, and it introduces the possibility of one thread holding the lock and getting descheduled, blocking every other thread waiting on it. Reach for std::atomic for single-variable state under contention, and reserve std::mutex for protecting multi-step invariants across more than one piece of shared state at once.

A deadlock happens when two or more threads each hold a resource the other needs, and neither can proceed, so both wait forever. The classic version: thread A locks mutex 1 then tries to lock mutex 2, while thread B locks mutex 2 then tries to lock mutex 1, at nearly the same moment. Each thread is now waiting on a lock the other already holds.

cpp
std::mutex m1, m2;

// thread A
std::lock_guard<std::mutex> lockA(m1);
std::lock_guard<std::mutex> lockB(m2); // waits for m2, which B holds

// thread B
std::lock_guard<std::mutex> lockB(m2);
std::lock_guard<std::mutex> lockA(m1); // waits for m1, which A holds

The standard fix is a consistent global lock ordering, always acquire m1 before m2 everywhere in the codebase, or use std::lock, which locks multiple mutexes together using a deadlock-avoidance algorithm so no ordering discipline is needed by hand.

std::optional<T>, added in C++17, models "a value of type T, or nothing," directly in the type system, forcing the caller to explicitly check whether a value is present before accessing it. A sentinel value like -1 only works if every valid result is guaranteed to exclude that sentinel, which isn't always true, and a raw pointer returning null works but forces heap allocation or lifetime tricks for types that would otherwise be perfectly fine returned by value.

cpp
std::optional<int> findAge(const std::string& name) {
 if (name == "ada") return 36;
 return std::nullopt;
}

if (auto age = findAge("ada")) {
 std::cout << *age; // must dereference, can't accidentally use a nonexistent value
}

std::span<T> is a lightweight, non-owning view over a contiguous sequence, a pointer and a size bundled into one object, that works uniformly whether the underlying data lives in a std::vector, a std::array, a raw C array, or a subrange of any of those. Before it existed, functions either took a container type directly, forcing a specific container choice on every caller, or took a raw pointer and length as two separate parameters that could silently get out of sync.

cpp
void printAll(std::span<const int> values) { // works with vector, array, C array, all the same
 for (int v : values) std::cout << v << " ";
}

std::vector<int> v = {1, 2, 3};
int arr[] = {4, 5, 6};
printAll(v);
printAll(arr);

C++23, ratified as ISO/IEC 14882:2024 and finalized by the committee through 2023 (isocpp.org, Standing document status), shipped std::expected<T, E>, a type for returning either a success value or an error, without throwing an exception or reaching for an out-parameter, and multidimensional subscript operators, operator[](i, j) instead of the older operator()(i, j) workaround matrix and tensor libraries had used for years.

cpp
std::expected<int, std::string> parseInt(const std::string& s) {
 try {
  return std::stoi(s);
 } catch (...) {
  return std::unexpected("not a valid integer");
 }
}

Most day-to-day code won't touch every C++23 feature immediately, compiler support still lags the standard by a version or two in practice, but std::expected specifically is worth knowing, since it's the standard library's answer to the "should this fail with an exception or a return code" debate that C++ codebases have argued about informally forever.

Undefined behavior means the standard places no requirements on what happens at all, the compiler is free to do literally anything, including something that looks correct on one run and wrong on the next, or nothing observably wrong until a compiler upgrade changes what optimizations kick in. Unspecified behavior means the standard allows more than one valid outcome without requiring the implementation to document which one it picked, evaluation order of function arguments before C++17 is a classic example. Implementation-defined behavior is unspecified behavior that the implementation is additionally required to document, the exact size of int on a given platform, for instance.

The practical difference: implementation-defined behavior is safe to depend on if you're only targeting one specific compiler and you've checked its documentation. Undefined behavior is never safe to depend on, full stop, even if it currently "happens to work," because the compiler owes you nothing and a future optimization pass can legally break it without warning.

Yes, since C++11 std::string is guaranteed to store its characters in contiguous memory, the same guarantee std::vector has, which is what makes .data() and .c_str() safe to hand off to a C API expecting a contiguous buffer. Before C++11, some implementations used copy-on-write strings that didn't necessarily hold this guarantee under every operation, but that's no longer a concern on any current standard library implementation.

cpp
std::string s = "hello";
const char* raw = s.data(); // guaranteed contiguous, safe to pass to a C function expecting char*

What's still worth knowing: modifying the string, appending, resizing, invalidates any pointer or reference you'd previously taken into it, same as vector. Holding onto raw above across a later s.append(...) call and dereferencing it afterward is the exact same iterator-invalidation bug from the containers section, just for a different container.

Hard questions

16

A std::vector stores its elements in one contiguous block. When an insertion exceeds the current capacity, the vector allocates a new, larger block, moves or copies every existing element into it, and frees the old block. Every pointer, reference, and iterator into the old block is now dangling, even though nothing about your code looks wrong at the call site.

cpp
std::vector<int> v = {1, 2, 3};
int& first = v[0];
v.push_back(4); // may reallocate if capacity was exactly 3
first = 10; // undefined behavior if reallocation happened

What makes it dangerous is that it's not deterministic from the caller's point of view without knowing the vector's current capacity, which most code doesn't check. A test that happens to run with enough spare capacity passes clean, and the exact same code fails once the vector grows past a threshold in production. .reserve() ahead of time, or switching to std::deque if you genuinely need stable references across insertion, are the two real fixes.

The diamond problem happens when a class inherits from two base classes that both inherit from the same common ancestor. Without virtual inheritance, the derived class ends up with two separate copies of that common ancestor's data, one through each path, which is ambiguous and usually not what anyone wanted.

cpp
struct Animal { int legs = 4; };
struct Dog : virtual Animal {};
struct Cat : virtual Animal {};
struct DogCat : Dog, Cat {}; // only one Animal subobject, thanks to virtual

DogCat dc;
dc.legs; // unambiguous, single shared Animal base

Marking both intermediate bases virtual Animal tells the compiler to share a single instance of that base across the whole hierarchy instead of duplicating it per inheritance path. It solves the ambiguity, but it comes with real cost, virtual base access goes through an extra indirection, and constructor initialization order gets noticeably harder to reason about. Most style guides I've worked under discourage multiple inheritance of state entirely and reserve it for pure interface classes with no data members.

Copy elision is the compiler skipping a copy or move entirely and constructing the object directly in its final destination, instead of building a temporary and then copying or moving from it. As of C++17, one specific form of this, returning a prvalue directly from a function, is mandatory, guaranteed by the standard, not just an optimization the compiler is allowed to apply (cppreference, Copy elision).

cpp
struct Widget {
 Widget() { std::cout << "constructn"; }
 Widget(const Widget&) { std::cout << "copyn"; }
 Widget(Widget&&) { std::cout << "moven"; }
};

Widget make() {
 return Widget(); // C++17: guaranteed elision, only "construct" prints
}

Widget w = make(); // no copy, no move, ever, since C++17

Before C++17, most compilers already performed this optimization under the as-if rule, but it wasn't guaranteed by the standard, so a class with side effects in its copy or move constructor could behave inconsistently across compilers. C++17 made it a language rule for this specific case, so the move constructor here genuinely never runs, not because it's slow, but because there's no temporary object left to move from at all.

std::vector guarantees the strong exception safety guarantee during reallocation: if something throws partway through moving elements to the new buffer, the vector must be left in its original, unchanged state. If a move constructor could throw, the vector can't safely use it for that guarantee, because a partially-completed move that then throws would leave some elements moved-from in the old buffer with no way to roll that back cleanly. So vector checks: if the move constructor is noexcept, it uses moves during reallocation; if it isn't, it falls back to copying instead, even though copying is slower, because copying can be rolled back safely on failure and moving can't.

cpp
class Bad {
public:
 Bad(Bad&& other) { /* not marked noexcept */ }
};
class Good {
public:
 Good(Good&& other) noexcept { }
};

std::vector<Bad> v1; // reallocation copies elements, silently, even though a move ctor exists
std::vector<Good> v2; // reallocation moves elements, as intended

This is one of the more surprising performance footguns in the standard library. Writing a move constructor and forgetting noexcept doesn't cause a compile error, it just silently costs you the performance you wrote the move constructor to get in the first place.

In a template context specifically, T&& is a forwarding reference, not a plain rvalue reference, and it binds to both lvalues and rvalues through a compiler mechanism called reference collapsing. If you pass an lvalue, T deduces to a reference type, and T& plus && collapses down to just T&. If you pass an rvalue, T deduces to the plain type, and you get a real rvalue reference. std::forward exists specifically to preserve that distinction through the rest of the function.

cpp
template<typename T>
void wrapper(T&& arg) {
 target(std::forward<T>(arg)); // preserves lvalue-ness or rvalue-ness of the original call
}

int x = 5;
wrapper(x);  // T deduced as int&, arg is int&
wrapper(10); // T deduced as int, arg is int&&

Outside a template, T&& on a concrete, non-deduced type is always a genuine rvalue reference. The forwarding-reference behavior only kicks in when T itself is being deduced at that call, which is the detail most candidates who've only read a summary of this rule get wrong.

SFINAE stands for Substitution Failure Is Not An Error. When the compiler substitutes a type into a template and that substitution produces an invalid expression, the compiler doesn't emit a hard error, it just quietly removes that overload from consideration and moves on to check whether any other overload works. This lets you write multiple template overloads that are only valid for certain types, and have the compiler pick the correct one automatically, or fail with a real error only if none of them apply.

cpp
template<typename T>
auto has_size(T& t) -> decltype(t.size(), std::true_type{});

std::true_type has_size(...); // fallback, matches anything

std::vector<int> v;
int x;
has_size(v); // true_type, vector has.size()
has_size(x); // false_type would need a second overload, but the point stands

Modern code mostly reaches for if constexpr or concepts (C++20) instead of hand-rolled SFINAE now, because SFINAE-based code produces genuinely brutal compiler error messages when it fails. Knowing SFINAE still matters because a huge amount of existing library code, including parts of the standard library itself, is built on it.

A variadic template accepts an arbitrary number of template arguments of arbitrary types, using a parameter pack, typename... Args. std::make_unique and std::vector::emplace_back both depend on this: they need to forward an unknown number of constructor arguments through to a type they don't otherwise know anything about.

cpp
template<typename T, typename... Args>
std::unique_ptr<T> makePtr(Args&&... args) {
 return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

auto p = makePtr<Widget>(3, "name", 4.5); // forwards all three args to Widget's constructor

Implicit instantiation is what happens by default, the compiler generates a concrete function or class only when it's actually used somewhere in that translation unit. Explicit instantiation forces the compiler to generate the code for a specific type immediately, even if nothing in that translation unit calls it yet, and it's typically used in a source file paired with an explicit-instantiation-declaration in other files, so the compiled code only exists once instead of being regenerated and discarded by every translation unit that includes the template.

cpp
// widget.cpp
template class Stack<int>;  // explicit instantiation, forces generation here

// other.cpp
extern template class Stack<int>; // tells other TUs not to instantiate it again, just link to it

This mostly matters for large template-heavy libraries trying to control compile times and binary size. Most application code never needs it, but it comes up in library-authoring interviews specifically.

An iterator becomes invalid when the operation you performed changes the container's internal structure in a way that makes the iterator's stored position, or the memory it pointed at, no longer correspond to a valid element. Using an invalidated iterator afterward is undefined behavior. The exact rules differ per container: vector::push_back invalidates all iterators if it triggers reallocation, but only the end iterator if capacity was already sufficient. vector::erase invalidates the erased element's iterator and every iterator after it, since everything shifts down. list and map/set only invalidate iterators to the specific element removed, everything else stays valid, which is a big part of why they're chosen when code needs to hold iterators across mutation.

cpp
std::vector<int> v = {1, 2, 3, 4};
auto it = v.begin() + 1;
v.erase(v.begin()); // it is now invalidated, everything shifted
*it; // undefined behavior

std::list<int> l = {1, 2, 3, 4};
auto lit = std::next(l.begin());
l.erase(l.begin()); // lit stays valid, only the erased node's own iterator is invalid
*lit; // fine

A thread spinning in a while loop checking a shared flag, even one properly protected by a mutex, burns CPU continuously while waiting, since it has to keep reacquiring the lock and re-checking. std::condition_variable lets a thread block efficiently, consuming no CPU, until another thread explicitly notifies it that the condition might have changed.

cpp
std::mutex m;
std::condition_variable cv;
bool ready = false;

// waiting thread
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return ready; }); // sleeps until notified, then re-checks the predicate

// notifying thread
{
 std::lock_guard<std::mutex> lock(m);
 ready = true;
}
cv.notify_one();

The predicate passed to wait matters because of spurious wakeups, the standard allows a condition variable to wake a waiting thread even without an actual notification, so wait re-checks the predicate after waking and goes back to sleep if it's still false. Skipping the predicate and just calling the plain overload of wait is a subtle, real bug in production, one that usually only surfaces under specific OS scheduling conditions that don't show up in local testing.

By default, every std::atomic operation uses memory_order_seq_cst, sequential consistency, the strongest and safest ordering, which guarantees a single global order of atomic operations that every thread agrees on. It's also the most expensive, since it can require the compiler and CPU to insert real memory barriers preventing reordering that would otherwise be free.

cpp
std::atomic<int> counter{0};

counter.fetch_add(1, std::memory_order_relaxed); // no ordering guarantee, just atomicity
counter.fetch_add(1); // implicit memory_order_seq_cst, full ordering guarantee

You'd weaken it to memory_order_relaxed only for something like a simple statistics counter where you need the increment itself to be atomic, no lost updates, but you genuinely don't care what order different threads observe it in relative to other memory operations. This is advanced enough that I wouldn't expect most candidates to reach for it correctly on their own, but being able to explain why seq_cst is the safe default, and that weaker orderings exist for a reason, is a fair bar for a senior systems role.

std::async launches a task, possibly on a new thread, possibly deferred to run synchronously when its result is requested, depending on the launch policy, and returns a std::future holding the eventual result. The part that trips people up: if you discard the returned future without storing it anywhere, its destructor runs immediately, and a future obtained from std::async (specifically, not from a promise) blocks in its destructor until the associated task finishes.

cpp
std::async(std::launch::async, longRunningTask); // return value discarded

// the line above BLOCKS here, synchronously, waiting for longRunningTask to finish,
// because the temporary future's destructor runs at the end of the statement
// and that destructor waits. It defeats the entire purpose of using async here.

The fix is to actually store the future somewhere with a lifetime you control, and decide explicitly when to call .get() or .wait() on it. This exact footgun is common enough that plenty of style guides ban bare std::async calls with discarded results outright.

C++20 coroutines let a function suspend its execution partway through, return control to the caller, and resume later exactly where it left off, using the co_await, co_yield, and co_return keywords. That's the mechanism async I/O and generator-style code needs without manually threading callbacks or state machines through the function by hand.

cpp
task<int> fetchData() {
 auto response = co_await httpGet("/api/data"); // suspends here, resumes when response arrives
 co_return response.value;
}

The standard deliberately ships coroutines as a low-level mechanism, not a complete async framework, there's no standard task or generator type included, you have to write or adopt one from a library like cppcoro. That gap is the honest reason adoption has been slower than expected, teams need a supporting library layered on top before coroutines feel usable day to day, and that ecosystem is still consolidating years after C++20 shipped.

Unsigned integer arithmetic in C++ is defined to wrap around using modular arithmetic, UINT_MAX + 1 is guaranteed to be 0. Signed integer overflow is undefined behavior specifically because the standard doesn't mandate two's complement representation for signed types (it does as of C++20, incidentally, but overflow is still undefined even so), and leaving it undefined lets compilers assume signed overflow never happens, which enables real optimizations, like assuming x + 1 > x is always true for a signed x.

cpp
int x = INT_MAX;
x + 1; // undefined behavior, NOT guaranteed to wrap to INT_MIN

unsigned int u = UINT_MAX;
u + 1; // well-defined, guaranteed to wrap to 0

The optimization consequence is the part experienced engineers get wrong too: a compiler is legally allowed to assume signed overflow never happens and eliminate an overflow check written to catch it, if that check can only ever trigger via undefined behavior. A bounds check like if (x + 1 < x) meant to detect overflow can get silently deleted by the optimizer, because from the compiler's point of view, signed overflow "can't happen," so the branch is dead code.

During base class construction, the object's dynamic type is still the base class, even if you're in the middle of constructing a derived object, because the derived part of the object hasn't been initialized yet. Calling a virtual function from the base constructor dispatches to the base class's version, never the derived override, even though it looks like it should call the derived version once construction finishes.

cpp
struct Base {
 Base() { init(); } // calls Base::init, NOT Derived::init, even for a Derived object
 virtual void init() { std::cout << "Base::initn"; }
};
struct Derived : Base {
 void init() override { std::cout << "Derived::initn"; }
};

Derived d; // prints "Base::init", surprising to most people the first time

This isn't a compiler bug or a quirk of one implementation, it's guaranteed by the standard, and it's specifically because calling the derived override on a not-yet-fully-constructed derived object would be genuinely unsafe, its own member variables haven't been initialized yet. The same rule applies in reverse to destructors, in the opposite order.

The standard guarantees that static objects within a single translation unit (source file) are initialized in the order they're defined, but it makes no guarantee at all about the relative order of static objects defined in different translation units. If a static object in one file's constructor depends on a static object defined in another file, and that other file's static object hasn't been constructed yet when the first one runs, you get undefined behavior, using an object before it exists.

cpp
// file_a.cpp
Logger globalLogger; // depends on config being initialized first

// file_b.cpp
Config config; // may or may not be constructed before globalLogger, no guarantee either way

The standard fix is the "construct on first use" idiom, wrapping the static object in a function returning a reference to a local static, since a function-local static is guaranteed to be initialized the first time control passes through that line, no matter which translation unit calls it first.

cpp
Config& getConfig() {
 static Config config; // constructed on first call, guaranteed, thread-safe since C++11
 return config;
}

How to prepare for a C++ interview in 2026

Skip the flashcard approach to syntax, virtual keyword trivia, exact rule of five wording, and build one small thing where ownership actually matters: a resource pool that hands out RAII-wrapped handles, a small thread-safe queue built with a mutex and condition variable, a template class with a real move constructor you have to get right by hand. Compile it with -Wall -Wextra -fsanitize=address,undefined and fix every warning and sanitizer failure it turns up, instead of only fixing what the compiler treats as a hard error. Watching AddressSanitizer catch a use-after-free you didn't know you'd written teaches memory safety faster than any amount of reading about it does.

Across systems and backend mock interviews run through LastRoundAI, the base-constructor-calling-a-virtual-function question catches more candidates off guard than the rule of five does, even candidates who write the rule of five correctly from memory every time. My read is that the rule of five gets studied on purpose because it's a named rule with a clean explanation, while virtual dispatch during construction feels like a detail you'd only run into by accident in real code, so fewer people go looking for it ahead of time. We don't track a precise number on that gap, it shows up often enough in review to be worth flagging here, not often enough that I'd attach a specific percentage to it.

Get the reps in before the real thing

Explaining ownership on a whiteboard is not the same as defending it out loud once an interviewer changes one line and asks what breaks, what happens to that shared_ptr's refcount, whether that vector reallocates, whether that lock actually gets released on the exception path. LastRoundAI's mock interview mode runs live coding rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19/mo if a handful of sessions isn't enough runway.

Once your answers hold up under a follow-up, the slower part of the job hunt is usually just getting in front of enough systems, embedded, and backend roles that actually test C++ instead of treating it as a legacy-maintenance checkbox. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

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

If you already work with C++ day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.

What C++ topics come up most often?

Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.

Do I need hands-on C++ experience to pass?

It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.

Is C++ still worth learning in 2026?

For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.

Leave a Reply

Your email address will not be published. Required fields are marked *