Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up

C++ interviews concentrate on resource management and the costs of each abstraction. Expect questions on RAII and smart pointers, move semantics and why move operations should be noexcept, the Rule of Three, Five and Zero, virtual destructors, STL container trade-offs and iterator invalidation, and the difference between compile-time and runtime polymorphism. Undefined behaviour and the sanitizers used to find it come up regularly. The questions below cover modern C++ practice rather than legacy patterns.

Behavioural Questions

1. Tell me about a C++ project you have worked on. What made it challenging?

Note: C++ is chosen for a reason — performance, hardware access, or an existing codebase. Say which one applied, because it frames everything else about the project.

Cover:

  • The domain and why C++. Game engine, embedded firmware, trading system, image processing, or a native library behind another language. The reason matters: "we needed deterministic latency under 100 microseconds" tells the interviewer what kind of engineer you are.
  • Which C++ you were writing. C++98 with raw pointers is a different job from modern C++17 with smart pointers and move semantics. Be honest — legacy experience is valuable.
  • The hard part. Strong candidates: a memory corruption bug that only appeared in release builds, a data race under concurrency, cutting allocation on a hot path, or long compile times you had to attack.
  • How you verified the fix. Sanitizers, Valgrind, a benchmark — this is where C++ interviewers judge rigour.

2. Describe a difficult bug you tracked down in C++. How did you find it?

C++ bugs are distinctive because the symptom is often far from the cause, so the method matters more than the fix.

  • The symptom. The classic C++ story is a crash somewhere unrelated to the broken code, or behaviour that changes between debug and release builds, or a failure that disappears when you add a print statement. Any of these points at undefined behaviour.
  • The tools. Say which ones and what each found: AddressSanitizer for out-of-bounds access and use-after-free, UndefinedBehaviorSanitizer for signed overflow and bad casts, ThreadSanitizer for data races, Valgrind for leaks, and a debugger with a watchpoint on the corrupted address.
  • The root cause. Good ones: a dangling reference to a destroyed temporary, iterator invalidation after a vector reallocated, a missing virtual destructor, a race on a shared variable, or reading past the end of a buffer.
  • The systemic fix — turning on sanitizers in CI, or replacing a raw pointer with a smart pointer so the class of bug becomes impossible.

Note: Explaining that undefined behaviour lets the compiler assume the bad case cannot happen — and therefore optimise on that assumption — shows genuine depth.

3. How do you decide between optimising code and keeping it simple?

C++ attracts premature optimisation, so interviewers are checking for discipline.

The default is clarity. Write the straightforward version, measure, and optimise only what the profiler identifies. The reason is not that performance does not matter — it is that intuition about C++ performance is unreliable. Cache behaviour, branch prediction, and compiler optimisation regularly make the "obviously faster" code slower.

When to optimise up front:

  • Choice of data structure and algorithm. This is not premature — it is design. Getting the complexity class wrong is expensive to fix later.
  • Memory layout on a hot path. Contiguous storage and struct-of-arrays versus array-of-structs are architectural decisions.
  • Anything in a documented hard real-time budget.

How to keep it honest: benchmark with realistic data, use a proper harness such as Google Benchmark rather than timing one run, and check the generated assembly when a result surprises you.

Note: A strong story is one where you optimised something, measured no improvement, and reverted it. That is a habit interviewers rarely hear about and always respect.

4. How do you approach code review and maintain quality in a C++ codebase?

C++ gives you many ways to be subtly wrong, so lean on tooling and reserve human attention for design.

Automated:

  • clang-format so formatting never appears in review.
  • clang-tidy with the modernize, bugprone, and core guideline checks — it catches whole categories of error mechanically.
  • Sanitizers in CI, at minimum ASan and UBSan on the test suite. This is the highest-value practice in C++ and many teams still skip it.
  • Warnings as errors with -Wall -Wextra.

What humans should review: ownership and lifetime — who owns this pointer and how long does it live; exception safety; whether the interface is easy to misuse; and const-correctness.

Note: The single most useful review question in C++ is "what happens if this throws halfway through?" It surfaces resource leaks and broken invariants that compile perfectly. Mentioning RAII as the structural answer — that resources should be owned by objects so cleanup is automatic — shows you think in terms of preventing bugs rather than catching them.

5. C++ has evolved rapidly since C++11. How do you keep current and decide what to adopt?

How you keep current: the standards committee papers show what is coming, cppreference is the practical daily reference, and conference talks from CppCon give you the reasoning behind features rather than just the syntax. Most usefully, rewriting something you already wrote using a newer feature teaches you where it does and does not help.

How you decide what to adopt:

  • Which standard can you actually use? The compiler your product ships with is the real constraint, particularly in embedded work where the toolchain may be years old.
  • Does it remove a class of bug? Smart pointers, std::optional, std::string_view, range-based for, and structured bindings all do. These are worth adopting immediately.
  • Does it complicate the code for a marginal gain? Heavy template metaprogramming and clever constexpr can make a codebase unreadable to everyone but its author.

Note: A good concrete answer is that you would enforce a rule such as no owning raw pointers, backed by clang-tidy, rather than relying on people to remember. And the C++ Core Guidelines are worth naming — they are the closest thing the community has to agreed practice.

6. Tell me about a time you modernised a legacy C++ codebase, for example replacing raw new and delete with smart pointers. How did you manage the risk?

This question tests whether you can improve old code without breaking it. Interviewers want to hear about incremental change, safety nets and measurable outcomes — not a heroic rewrite.

Structure your answer with STAR:

  • Situation — describe the codebase briefly: its age, the standard it targeted (C++98 or C++03), its size, and the pain it caused. For example, “a 400,000-line trading gateway with manual memory management and recurring leaks reported by Valgrind”.
  • Task — what you were asked to achieve and the constraints: no downtime, no behaviour change, a fixed compiler-upgrade window.
  • Action — the heart of the answer. Strong points to mention:
    • Adding characterisation tests and running AddressSanitizer in CI before touching anything.
    • Upgrading the compiler and enabling warnings first, fixing them as a separate change.
    • Converting ownership one module at a time: std::unique_ptr for owned members, raw pointers or references kept only as non-owning observers.
    • Using clang-tidy modernize checks to automate mechanical changes such as override and nullptr.
    • Keeping each pull request small and reviewable, and never mixing refactoring with feature work.
  • Result — quantify it: leaks eliminated, crash reports down, build warnings from 2,000 to zero, faster onboarding.

What to avoid: claiming you replaced every pointer with std::shared_ptr. That signals you do not understand ownership — shared ownership should be the exception, and interviewers often probe exactly that point.

Note: Close with a lesson learned, such as a change that caused a regression and how your test net caught it. Honest detail about a setback makes the story far more credible than a flawless one.

7. Describe a time you found and fixed a performance bottleneck in C++ code. How did you measure it before and after?

The interviewer is checking that you optimise with evidence rather than intuition. The strongest answers are built around measurement, a clear root cause and a verified result.

  • Set the scene with a number. “Our order-matching service had a p99 latency of 12 ms against a 5 ms target” is far better than “it was slow”.
  • Explain how you measured. Mention a profiler on an optimised build with debug symbols — perf, VTune or Instruments — and a repeatable micro-benchmark using Google Benchmark. Say you profiled a realistic workload, not a toy input.
  • Name the real root cause. Typical, credible findings in C++:
    • Hidden copies of large objects passed by value or returned from loops.
    • Heap allocation inside a hot loop, fixed with reserve() or an object pool.
    • Cache misses from pointer-chasing structures such as std::list or node-based maps, fixed with a contiguous std::vector.
    • Lock contention, fixed by shortening critical sections or sharding data.
  • Show the result and the guard-rail. Give before and after figures, and say you added the benchmark to CI so a future change cannot silently regress it.

It also helps to mention what you did not do: you left clear code alone where the profiler showed no cost, and you documented any non-obvious optimisation with a comment explaining why it exists.

Note: If you do not have a big production story, a well-measured smaller one is fine. Interviewers value the method — hypothesis, measurement, change, re-measurement — much more than the size of the speed-up.

8. A junior developer on your team keeps introducing memory-safety bugs in C++. How would you help them improve?

This is a mentoring and team-quality question. The interviewer wants to see empathy, practical teaching and a preference for systems that prevent bugs over blaming individuals.

A strong answer covers three layers:

  • Understand first. Look at the actual bugs together. Are they dangling references, leaks, double frees or out-of-bounds access? Each points to a different gap — ownership, lifetimes or bounds checking. Ask how they reason about who owns an object.
  • Teach the model, not just the fix.
    • Pair on one bug end to end, reproducing it under AddressSanitizer so they see exactly where memory was freed and later used.
    • Explain the modern ownership rules: std::unique_ptr for owners, references for non-owning access, containers instead of manual arrays, and almost never a bare delete.
    • Point them to the C++ Core Guidelines sections on resource management and lifetimes.
  • Make the tooling catch it. Enable -Wall -Wextra, run sanitizers and clang-tidy in CI, and add a review checklist item for ownership. This protects the whole team, not just one person.

In code review, explain why a pattern is dangerous rather than simply rewriting it, and praise improvements publicly. Set a check-in after a few weeks to see whether the bug rate has actually dropped.

If you have a real example, use STAR and finish with a result, such as the developer later catching a lifetime bug in someone else's review.

Note: Avoid framing the junior as the problem. Interviewers are listening for whether you would build a safer environment — tools, guidelines, pairing — because in C++ even experienced engineers make these mistakes.

9. Tell me about a time you had to choose between writing a component yourself and adopting a third-party C++ library.

This question probes engineering judgement. C++ has no single package ecosystem, so choosing dependencies carries real cost, and interviewers want to hear a structured evaluation.

Walk through your decision criteria:

  • Licence. MIT, BSD, Boost and Apache licences are easy for commercial products; GPL may be unacceptable for proprietary code. Mention checking with legal where relevant.
  • Maintenance health. Recent releases, responsive issue tracker, number of maintainers, and security track record.
  • Build and platform fit. Does it support your compilers and C++ standard, integrate with CMake, and install cleanly through vcpkg or Conan? Header-only libraries are simple to adopt but can slow compilation.
  • ABI and exceptions. Does it throw when your codebase compiles with exceptions disabled? Does it expose STL types across a shared-library boundary?
  • Performance and footprint. Benchmark it on your own workload rather than trusting the README.
  • Total cost of writing it yourself. Not just the first version — testing, edge cases, fuzzing and years of maintenance.

Example shape: “We needed JSON parsing in a latency-sensitive service. I benchmarked nlohmann/json against simdjson, found simdjson around six times faster on our payloads, confirmed its licence and CMake support, and wrapped it behind a small interface so we could swap it later.”

Finish with the outcome and any trade-off you accepted, such as a steeper API or a pinned version you now have to track for security fixes.

Note: Wrapping a third-party library behind your own thin interface is a point worth stating explicitly — it limits the blast radius if the library is abandoned or needs replacing.

10. How have you handled a disagreement with a colleague over a C++ design choice, such as exceptions versus error codes?

Interviewers use this to judge collaboration and technical maturity. Exceptions versus error codes is a good vehicle because both sides have legitimate arguments, so a strong answer shows you understood the other view.

Structure the story:

  • The disagreement. State both positions fairly. For example: “I wanted exceptions for a new parsing module; a senior colleague preferred error codes because part of our product runs on an embedded target built with exceptions disabled.”
  • How you explored it.
    • Listening to the underlying constraint rather than the stated preference — here, binary size and deterministic latency on the embedded side.
    • Gathering evidence: a small prototype, a measurement of code size, or the C++ Core Guidelines position.
    • Looking for a middle path. std::expected in C++23, or a similar result type in older code, gives explicit error values without silent ignoring, and [[nodiscard]] stops callers dropping them.
  • The decision. Explain how it was made — team discussion, a tech lead, or an architecture decision record — and that you committed fully once it was decided, even if it was not your original choice.
  • The result. A consistent convention written into the team style guide, and fewer arguments in later reviews.

Avoid stories where you simply “won”. Interviewers are more impressed by a candidate who changed their mind when shown a real constraint, or who found a solution both people were happy with.

Note: Mention writing the decision down. Recording the reasoning in a short design note means the same debate does not restart every time a new engineer joins the team.

Technical Questions

11. What is RAII, and how do smart pointers implement it?

RAII — Resource Acquisition Is Initialisation — ties a resource's lifetime to an object's lifetime. The constructor acquires it, the destructor releases it. Because C++ guarantees destructors run when an object goes out of scope, including during stack unwinding from an exception, cleanup is automatic and cannot be forgotten.

It applies to any resource, not just memory: file handles, mutex locks, sockets, database connections.

Smart pointers are RAII for heap memory:

  • std::unique_ptr — exclusive ownership. Cannot be copied, only moved. Zero overhead compared to a raw pointer. This should be your default.
  • std::shared_ptr — shared ownership with an atomic reference count; the object is destroyed when the last one goes away. It costs a control block and atomic increments, so use it only when ownership genuinely is shared.
  • std::weak_ptr — observes a shared_ptr without owning it. Its purpose is breaking reference cycles, which shared_ptr alone cannot handle.

Note: Prefer make_unique and make_shared over new. They are exception-safe in argument evaluation, and make_shared allocates the object and control block in one go. The broader point is that in modern C++ you should essentially never write delete.

12. What is the difference between a pointer and a reference in C++?

Both give indirect access, but with different rules.

  • A reference must be initialised when declared and can never be reseated to refer to something else. A pointer can be null and can be reassigned.
  • A reference cannot be null — there is no valid way to create one from nothing — so a function taking a reference need not check.
  • Syntax. A reference is used like the object itself; a pointer must be dereferenced with * or ->.
  • Pointer arithmetic exists; reference arithmetic does not.
  • You can have a pointer to a pointer, but not a reference to a reference.

How to choose: use a reference when the parameter is mandatory and use const& to pass large objects without copying. Use a pointer when the argument is genuinely optional, when you need to reseat it, or when interfacing with a C API. std::optional is often clearer than a nullable pointer for an optional value.

Note: The important caveat is that a reference is not inherently safe — it can dangle. Returning a reference to a local variable, or holding a reference to a vector element after the vector reallocates, are both undefined behaviour despite the reference never being null.

Free workshop by Jobaaj Learnings

13. Explain move semantics, rvalue references and std::move.

Before C++11, returning a large object or inserting into a container meant copying. Move semantics lets you transfer ownership of internal resources instead.

An lvalue has an identity you can take the address of; an rvalue is a temporary about to expire. An rvalue reference, written T&&, binds to those temporaries and says "this object is about to die, so you may steal from it".

class Buffer {
    char* data_; size_t size_;
public:
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;   // leave source valid and destructible
        other.size_ = 0;
    }
};

A move constructor copies the pointer rather than the megabytes it points at — O(1) instead of O(n).

std::move does not move anything. It is a cast to an rvalue reference, telling the compiler it may select the move overload. After moving from an object, it is in a valid but unspecified state: you may destroy it or assign to it, but not rely on its contents.

Note: Mark move operations noexcept. std::vector will only use a move constructor during reallocation if it is noexcept — otherwise it copies to preserve the strong exception guarantee, and you silently lose the benefit.

14. What is the Rule of Three, Five and Zero?

These rules describe which special member functions must be defined together.

The Rule of Three (C++98): if you need to write any one of the destructor, copy constructor, or copy assignment operator, you almost certainly need all three. The reason is that needing a custom destructor implies the class manages a resource, and the compiler-generated copy operations will copy the pointer rather than the resource — producing a double free.

The Rule of Five (C++11) adds the move constructor and move assignment operator. Declaring a destructor or copy operation suppresses the implicit move operations, so a class following only the Rule of Three silently loses move optimisation.

The Rule of Zero is the modern guidance and the one to lead with: design classes so you need none of them. If every member is a type that manages itself — std::string, std::vector, std::unique_ptr — the compiler-generated versions are all correct, and there is no code to get wrong.

Note: The practical consequence is a separation of duties. Write resource-managing wrapper classes rarely and carefully, and let everything else follow the Rule of Zero. A class that both manages a raw resource and holds business logic is where these bugs live.

15. How does virtual inheritance and polymorphism work in C++, and why do base classes need virtual destructors?

Runtime polymorphism works through the virtual table. A class with virtual functions gets a hidden pointer to a table of function addresses; calling a virtual function through a base pointer looks up the derived implementation at runtime. The cost is one pointer per object, one indirection per call, and the loss of inlining.

Why the destructor must be virtual:

Base* p = new Derived();
delete p;   // undefined behaviour if ~Base is not virtual

Without virtual, only ~Base runs. ~Derived never executes, so any resource the derived class owns leaks. The rule: any class intended to be inherited from and deleted polymorphically needs a virtual destructor. If a class is not meant to be a base, it does not need one — and adding it unnecessarily costs a vtable pointer.

Virtual inheritance solves the diamond problem. If B and C both inherit from A, and D inherits from both, D normally contains two copies of A — ambiguous and wasteful. Declaring class B : virtual public A makes them share one subobject.

Note: Use override on every overriding function. It makes the compiler verify that you are actually overriding something, catching the common bug where a signature differs slightly and you silently create a new function instead.

16. What is the STL, and what are the main container types and their complexity?

The Standard Template Library is built from containers, iterators, and algorithms, connected so that algorithms work on any container through iterators.

Sequence containers:

  • vector — contiguous, dynamically resized. Random access O(1), push_back amortised O(1), insert or erase in the middle O(n). This should be your default container — contiguity makes it cache-friendly, which usually beats better asymptotic complexity at real sizes.
  • deque — O(1) insertion at both ends, random access O(1), but not contiguous.
  • list — doubly linked. O(1) insert or erase given an iterator, but O(n) to find anything and poor cache behaviour. Rarely the right choice in practice.

Associative containers:

  • map / set — balanced binary trees. O(log n) lookup, and iteration in sorted order.
  • unordered_map / unordered_set — hash tables. O(1) average lookup, O(n) worst case, no ordering.

Note: Iterator invalidation is the follow-up to be ready for. Inserting into a vector may reallocate and invalidate every iterator, pointer, and reference into it; list and map invalidate only what you erase. Modifying a container while iterating over it is one of the most common C++ bugs.

17. What is the difference between the stack and the heap in C++?

The stack holds local variables and function call frames. Allocation is a single register adjustment, so it is extremely fast; deallocation is automatic when scope ends. It is contiguous and cache-friendly. The limits are that its size is fixed at thread creation — typically one to eight megabytes — and that the size must be known at compile time.

The heap is memory obtained with new or malloc. It is large, sized at runtime, and outlives the scope that created it. The costs are a much slower allocator that must search for a free block, potential fragmentation, worse locality, and the obligation to free it.

How to choose: prefer the stack. Use the heap when the object is too large for the stack, when its size is only known at runtime, or when it must outlive the current scope. Even then, own it with unique_ptr or a container rather than a raw pointer.

The failure modes differ: exhausting the stack — usually via unbounded recursion or a huge local array — gives you a stack overflow and immediate crash. Exhausting the heap throws std::bad_alloc. Forgetting to free heap memory leaks; freeing twice or using after free is undefined behaviour and often exploitable.

Note: std::vector is a useful illustration — the vector object itself sits on the stack while its elements live on the heap, and its destructor frees them automatically.

18. What are templates in C++, and what is the difference between compile-time and runtime polymorphism?

A template is a blueprint the compiler uses to generate code for each type you instantiate it with. std::vector<int> and std::vector<string> are entirely separate generated classes.

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

Compile-time polymorphism (templates):

  • Type resolution happens during compilation, so calls can be inlined and there is no runtime overhead.
  • Costs: longer compile times, larger binaries from code generated per type, and historically dreadful error messages — which is what C++20 concepts were introduced to fix.

Runtime polymorphism (virtual functions):

  • The type is resolved through the vtable while the program runs.
  • Costs an indirection and prevents inlining, but lets you store heterogeneous types behind one base pointer and choose behaviour based on runtime data.

How to choose: if the set of types is known at compile time and performance matters, use templates. If you need a collection of different types handled uniformly, or the type depends on runtime input such as a configuration file, use virtual functions.

Note: Templates must generally be defined in headers, because the compiler needs the definition at each instantiation point. That is why STL headers are so large.

19. How do you handle concurrency in modern C++, and what causes a data race?

Since C++11 the standard library provides threading directly.

  • std::thread — a thread of execution. It must be joined or detached before destruction, or the program terminates. std::jthread in C++20 joins automatically and supports cooperative cancellation.
  • std::mutex with std::lock_guard or std::unique_lock — RAII locking, so the mutex is released even if an exception propagates. Never call lock() and unlock() by hand.
  • std::atomic — lock-free operations on single values, for counters and flags.
  • std::condition_variable — for waiting until a condition holds. Always wait with a predicate, because spurious wakeups are permitted.
  • std::async and std::future — a higher-level way to get a result back from concurrent work.

A data race occurs when two threads access the same memory location concurrently, at least one writes, and there is no synchronisation between them. It is undefined behaviour, not merely a wrong value — the compiler is entitled to optimise on the assumption it cannot happen, so symptoms can be bizarre and unrelated.

How to avoid them: prefer immutable shared data or message passing over shared mutable state; protect what must be shared with a mutex; take locks in a consistent order to avoid deadlock; and run ThreadSanitizer in CI, because races are almost impossible to find by inspection.

20. What is const-correctness, and where can const be applied?

Const-correctness means marking everything that does not modify state as const, so the compiler enforces your intent. It documents the interface, prevents accidental modification, and enables optimisation.

Where it applies:

  • Variablesconst int max = 100;
  • Function parametersvoid print(const std::string& s). Passing by const& avoids a copy while promising not to modify.
  • Member functionsint size() const; promises not to modify the object, and is required for the method to be callable on a const object. Omitting it is the most common const-correctness failure.
  • Return values — returning const& to expose a member without allowing modification.
  • Pointers, in two distinct waysconst char* p is a pointer to constant data (the data cannot change), while char* const p is a constant pointer (the pointer cannot be reseated). const char* const p is both. Read these right to left.

Note: Const-correctness is viral. If a low-level function is not const-correct, every caller is forced to give up const too, which is why it must be applied from the start — retrofitting it across a large codebase is genuinely painful. Also worth knowing: mutable exempts a member from const, which is legitimate for a cache or a mutex inside a const method.

21. How should you pass smart pointers to functions, and what does each parameter style say about ownership?

A function signature should state what it does with ownership. The C++ Core Guidelines give a clear convention:

  • Only using the object? Take T& or const T&, or T* if it may be null. The function does not care how the caller manages lifetime, so it should not demand a smart pointer at all.
  • std::unique_ptr<T> by value — the function takes ownership. The caller must write std::move(p), which makes the transfer visible at the call site.
  • std::unique_ptr<T>& — the function may reseat the caller's pointer, resetting or replacing it. This is rare.
  • std::shared_ptr<T> by value — the function will share ownership, for example by storing a copy in a member. Callers can move in to avoid an atomic increment.
  • const std::shared_ptr<T>& — the function might take a copy conditionally. Otherwise it is a code smell.
void render(const Widget& w);             // uses only
void adopt(std::unique_ptr<Widget> w); // sink: takes ownership
void keep(std::shared_ptr<Widget> w) { // shares ownership
cache_.push_back(std::move(w));
}

auto p = std::make_unique<Widget>();
render(*p);
adopt(std::move(p)); // p is now null

Returning is simpler: factories should return std::unique_ptr by value. It converts implicitly to std::shared_ptr if the caller needs shared ownership, so one factory serves both cases.

Note: Passing shared_ptr by value everywhere “to be safe” costs an atomic increment and decrement per call and hides who actually owns the object. Interviewers look for candidates who default to plain references.

22. How does std::shared_ptr work internally, and why is make_shared usually preferred over calling new?

A std::shared_ptr is two pointers: one to the managed object and one to a control block. The control block holds the strong count (owners), the weak count (observers), and the deleter and allocator.

  • Copying increments the strong count atomically; destroying decrements it. When the strong count reaches zero the object is destroyed; when the weak count also reaches zero the control block is freed.
  • Thread safety applies to the counts only. Different threads may copy and destroy their own shared_ptr instances safely, but access to the pointed-to object still needs synchronisation.

Why std::make_shared is preferred:

  • One allocation for object and control block instead of two — faster, and better cache locality.
  • Exception safety — there is never a moment when a raw new result is unowned.
  • No repeated type name and no visible new.

Its trade-offs: you cannot supply a custom deleter, and because the object and control block share one allocation, the object's memory is not released until the last weak_ptr disappears, even though its destructor has run.

The classic bug is creating two control blocks for one object:

Widget* raw = new Widget;
std::shared_ptr<Widget> a(raw);
std::shared_ptr<Widget> b(raw); // second control block: double delete

class Session : public std::enable_shared_from_this<Session> {
public:
void start() {
auto self = shared_from_this(); // shares the existing control block
}
};

Note: shared_from_this only works if the object is already owned by a shared_ptr. Calling it on a stack object, or inside the constructor, throws std::bad_weak_ptr since C++17.

23. What is a reference cycle with shared_ptr, and how does weak_ptr break it?

std::shared_ptr frees an object when its strong count reaches zero. If two objects own each other through shared_ptr, each keeps the other's count at one or more, so neither is ever destroyed — a leak that no tool in the language will clean up, because C++ has no garbage collector to detect cycles.

The typical case is a tree or graph with back-pointers: a parent owns its children, and each child points back to its parent.

The fix is to make one direction non-owning with std::weak_ptr. A weak_ptr observes an object managed by shared_ptr without contributing to the strong count.

struct Node {
std::vector<std::shared_ptr<Node>> children; // owning
std::weak_ptr<Node> parent; // non-owning back-reference
};

void notify_parent(const Node& n) {
if (auto p = n.parent.lock()) { // shared_ptr, or empty if gone
p->on_child_changed(); // p keeps the parent alive here
}
}
  • lock() atomically produces a shared_ptr if the object still exists, or an empty one if it has been destroyed. Always use it rather than checking expired() and then locking, which is a race.
  • Ownership should form a tree. Decide which side is the owner — usually the one that controls lifetime — and make every back-reference weak.

Other good uses of weak_ptr: observer lists where subscribers may disappear, and caches that should not keep entries alive once nobody else is using them.

Note: A weak_ptr keeps the control block alive, not the object. Combined with make_shared, that means the object's memory lingers until the last weak_ptr is gone — worth knowing for large objects in long-lived caches.

24. How do you use unique_ptr with a custom deleter to manage non-memory resources such as C FILE handles?

std::unique_ptr takes an optional second template argument: the deleter it calls instead of delete. That turns it into a general RAII wrapper for any C-style handle — files, sockets, SDK objects, OpenSSL contexts.

struct FileCloser {
void operator()(std::FILE* f) const noexcept { std::fclose(f); }
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;

FilePtr open_file(const char* path) {
return FilePtr(std::fopen(path, "rb"));
}

void load(const char* path) {
FilePtr f = open_file(path);
if (!f) throw std::runtime_error("cannot open file");
// use f.get() with fread; fclose runs on every exit path
}
  • The deleter is only called for a non-null pointer, so it does not need its own null check.
  • Prefer a stateless function object. An empty deleter type adds no size, so FilePtr is still one pointer wide. A function-pointer deleter doubles the size because the pointer must be stored in every instance.
  • A lambda works too in C++20 with std::unique_ptr<T, decltype(lambda)>, since captureless lambdas became default-constructible.
  • Avoid decltype(&std::fclose). Taking the address of most standard library functions is not guaranteed to be valid, and it costs the extra pointer as well.

With std::shared_ptr the deleter is passed to the constructor and is not part of the type — it is stored in the control block. That makes shared_ptr more flexible when handles with different cleanup must share a type, at the cost of a heap-allocated control block.

Note: For handles that are not pointers, such as POSIX file descriptors, write a small dedicated RAII class instead. Forcing an int into unique_ptr is possible with a custom pointer typedef, but a clear handle class is easier to read and review.

25. What is perfect forwarding, and how do forwarding references and std::forward work together?

Perfect forwarding means a function template passes its arguments on to another function exactly as they were received — lvalues stay lvalues and rvalues stay rvalues — so the callee can copy or move appropriately. It is how std::make_unique, emplace_back and std::thread construct objects in place.

Two ingredients:

  • A forwarding referenceT&& where T is a deduced template parameter, or auto&&. Passing an lvalue deduces T as X&; passing an rvalue deduces T as X.
  • Reference collapsing — any combination involving an lvalue reference collapses to &; only && with && stays &&. So the parameter ends up as X& or X&&.

Inside the function, a named parameter is always an lvalue. std::forward<T>(arg) is a conditional cast that restores the original category: it casts to an rvalue only when T was deduced from an rvalue.

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

std::string name = get_name();
auto a = make<User>(name); // copied into User
auto b = make<User>(std::move(name)); // moved into User

Common pitfalls:

  • std::vector<T>&& or const T&& is not a forwarding reference — it only binds rvalues.
  • Forwarding the same argument twice can move from it twice.
  • A constructor taking a forwarding reference is a greedier match than the copy constructor for non-const lvalues, and can hijack copying unless constrained.

Note: Use std::move when you know you have an rvalue reference, and std::forward only with forwarding references. Mixing them up is a frequent interview trap.

26. What are value categories in C++, and how does guaranteed copy elision in C++17 change returning objects by value?

Every expression has a type and a value category. Since C++11 there are three primary categories:

  • lvalue — has identity and cannot be implicitly moved from: a named variable, *p, a function returning T&.
  • prvalue (pure rvalue) — a value with no identity yet: 42, a + b, Widget{}, a function returning T by value.
  • xvalue (expiring value) — has identity but may be moved from: std::move(x), a function returning T&&.

lvalues and xvalues together are glvalues; prvalues and xvalues together are rvalues, which is why rvalue references bind to both.

C++17 changed what a prvalue is. It is no longer a temporary object but an initialiser that is materialised only when needed. Returning a prvalue of the same type therefore constructs the result directly in the caller's storage — no copy, no move, guaranteed.

Widget make() { return Widget{42}; }       // guaranteed elision

Widget make_named() {
Widget w;
w.init();
return w; // NRVO likely; otherwise an implicit move
}

Widget make_bad() {
Widget w;
return std::move(w); // blocks NRVO and forces a move
}
  • Guaranteed elision works even for types with deleted copy and move constructors, so a factory can return a std::mutex or std::atomic prvalue.
  • Named return value optimisation is still optional, but every major compiler does it; if it does not happen, a returned local is implicitly moved.
  • Writing return std::move(local); is a pessimisation — it turns the expression into an xvalue, which disables NRVO. Compilers warn about it with -Wpessimizing-move.

Note: The practical rule is to return by value freely and let the compiler elide. Only reach for std::move on return when returning a member or a parameter, which are not eligible for elision.

27. What is SFINAE, and how is std::enable_if used to constrain function templates?

SFINAE — Substitution Failure Is Not An Error — is the rule that when the compiler substitutes template arguments into a function template's declaration during overload resolution, and the result is an invalid type or expression, that candidate is simply removed from the overload set instead of causing a compile error.

It was the main pre-C++20 technique for enabling a template only for certain types.

// Only participates for integral types
template <typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
T half(T x) { return x / 2; }

// Only participates for floating-point types
template <typename T,
std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T half(T x) { return x * 0.5; }

// Expression SFINAE: only for types with a size() member
template <typename C>
auto count(const C& c) -> decltype(c.size()) { return c.size(); }

std::enable_if_t<Cond, T> is T when the condition is true and has no member type otherwise, so substitution fails and the overload disappears.

  • Only the immediate context counts. An error inside the function body, or deep inside another template being instantiated, is a hard error, not SFINAE.
  • Put the condition in the type of a non-type template parameter, as above. With the older typename = std::enable_if_t<...> form, two overloads that differ only in their default template argument are treated as redeclarations of the same template.
  • The detection idiom with std::void_t builds traits such as “has a serialize member”.

Drawbacks: the syntax is hard to read, and when no overload matches, the error messages are long lists of rejected candidates.

Note: In C++20, concepts and requires clauses replace almost all of these uses with readable constraints and far better error messages. Knowing SFINAE still matters, because most existing library code and pre-C++20 codebases rely on it.

28. What are C++20 concepts, and how do requires clauses improve on SFINAE for constraining templates?

A concept is a named compile-time predicate on template arguments. Constraining a template with one states its requirements in the interface, where both readers and the compiler can see them.

template <typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

template <Hashable Key, typename Value>
class Cache { /* ... */ };

template <typename T>
requires std::integral<T> && (sizeof(T) == 4)
T rotate(T x, int n);

void print(std::floating_point auto x); // abbreviated template

Improvements over SFINAE:

  • Readable intent. template <std::integral T> says in one line what enable_if buried in a default template argument.
  • Better errors. The compiler reports which constraint was not satisfied at the call site, instead of a wall of failed substitutions from deep inside the implementation.
  • Overloading by subsumption. When two constrained overloads both match, the more constrained one wins. A std::random_access_iterator overload is preferred over a std::forward_iterator one without tag-dispatch tricks.
  • Constraining non-templates. A member function of a class template can carry its own requires clause.

The standard library ships many concepts in <concepts> and <iterator>: std::same_as, std::derived_from, std::invocable, std::ranges::range, and more. Use them before writing your own.

Limits: concepts check syntax, not semantics — a type can satisfy std::totally_ordered syntactically while its comparison is broken. Over-constraining also makes templates harder to reuse, so require only what the implementation actually uses.

Note: A good interview point is that concepts change error messages at the call site, not the generated code. Constrained and SFINAE-based templates compile to the same machine code; the gain is entirely in correctness checking and readability.

29. What are variadic templates and fold expressions, and where are they useful in real code?

A variadic template accepts any number of template arguments through a parameter pack. typename... Ts declares a pack of types, Ts... args a pack of function parameters, and args... expands it. sizeof...(Ts) gives the number of elements.

Before C++17, processing a pack meant recursion: handle the first element, then call yourself with the rest, plus a base-case overload. Fold expressions in C++17 apply a binary operator across the whole pack in one expression.

template <typename... Ts>
auto sum(Ts... xs) {
return (xs + ... + 0); // binary right fold, works for empty pack
}

template <typename... Ts>
bool all_positive(Ts... xs) {
return ((xs > 0) && ...); // unary fold; empty pack gives true
}

template <typename... Ts>
void log_all(std::ostream& os, const Ts&... xs) {
((os << xs << ' '), ...); // comma fold, strictly left to right
}
  • Four forms: unary right (pack op ...), unary left (... op pack), and binary versions with an initial value.
  • Empty packs are only allowed in a unary fold for && (true), || (false) and the comma operator (void). For +, supply an initial value.

Where you meet them:

  • Factory and emplace functions that perfectly forward constructor arguments.
  • std::tuple, std::variant and std::apply.
  • Type-safe logging and formatting functions, and C++20 std::format.
  • Compile-time checks such as “all types are trivially copyable”, written as a fold over type traits.

Note: Each distinct combination of argument types instantiates a new function, so heavy variadic code can increase binary size and compile times. It is a tool for library-style building blocks, not for everyday application logic.

30. What is the difference between full and partial template specialisation, and why can function templates not be partially specialised?

Specialisation provides a different implementation of a template for particular arguments.

  • Full (explicit) specialisation fixes every template parameter: template <> struct Hash<bool>.
  • Partial specialisation fixes only some, or constrains their shape: template <typename T> struct Hash<T*> covers every pointer type.
template <typename T>
struct IsPointer { static constexpr bool value = false; };

template <typename T>
struct IsPointer<T*> { static constexpr bool value = true; }; // partial

template <>
struct Printer<bool> { // full
static void print(bool b) { std::cout << (b ? "true" : "false"); }
};

This is exactly how type traits such as std::is_pointer are implemented, and std::vector<bool> is a famous (and much criticised) specialisation that packs bits.

Function templates allow only full specialisation. The language instead gives functions overloading, which does the same job. And full specialisations of function templates behave surprisingly: they do not take part in overload resolution. The compiler first picks the best primary template among overloads, and only then looks for a specialisation of that template — so a specialisation you wrote may be silently ignored.

Practical guidance:

  • For functions, write an ordinary overload rather than a specialisation.
  • If you need partial-specialisation behaviour for a function, forward to a class template's static member, which can be partially specialised.
  • Specialisations must be declared before first use that would instantiate them, or the program is ill-formed.
  • You may specialise standard templates such as std::hash for your own types, but not add overloads to namespace std.

Note: With C++17 if constexpr and C++20 concepts, many specialisations can be replaced by a single constrained template, which is often easier to follow than a scattered family of specialisations.

31. How do vtables and vptrs implement virtual function calls, and what is the runtime cost of a virtual call?

The standard does not mandate an implementation, but every mainstream compiler uses the same scheme:

  • The vtable — each polymorphic class has one static table of function pointers, one slot per virtual function, plus type information used by dynamic_cast and typeid.
  • The vptr — each object of a polymorphic class carries a hidden pointer to its class's vtable, set by the constructor.

A virtual call shape->area() compiles to roughly: load the vptr from the object, load the function pointer from the right slot, make an indirect call.

struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0; // slot in Shape's vtable
};
struct Circle final : Shape {
double r;
double area() const override { return 3.14159 * r * r; }
};

The costs:

  • Memory — one pointer per object (more with multiple inheritance, which needs a vptr per polymorphic base and “thunks” that adjust this).
  • An indirect call — usually cheap when the branch predictor has seen the same target, costly when targets vary unpredictably.
  • Lost inlining — the biggest real cost. The compiler cannot inline a call it cannot resolve, which blocks further optimisation in tight loops.

Devirtualisation recovers this when the dynamic type is known: marking a class or function final, calling through a concrete object rather than a pointer, or link-time optimisation.

Constructors and destructors reset the vptr as each layer is built or torn down, so a virtual call from a base constructor dispatches to the base version — the derived part does not exist yet.

Note: In most code the overhead is negligible; worry about it only in hot loops over millions of objects. There, sorting objects by type, or switching to templates or std::variant, is the usual fix after profiling confirms the problem.

32. What is object slicing in C++, and how do you prevent it?

Object slicing happens when a derived-class object is copied into a base-class object — by value, not by reference or pointer. Only the base part is copied; the derived members and the derived dynamic type are lost, so virtual calls on the copy run the base implementation.

struct Shape {
virtual ~Shape() = default;
virtual double area() const { return 0; }
};
struct Circle : Shape {
double r = 1;
double area() const override { return 3.14159 * r * r; }
};

void print(Shape s) { std::cout << s.area(); } // parameter by value

Circle c;
print(c); // prints 0: sliced to a Shape
std::vector<Shape> v{c}; // also sliced
Shape& ref = c;
ref = Circle{}; // assigns only the Shape part

Slicing compiles silently, which is what makes it dangerous. It can also corrupt objects: assigning through a base reference copies only base members, leaving a mixture of old derived state and new base state.

How to prevent it:

  • Pass polymorphic objects by reference or pointer: const Shape&, Shape*.
  • Store them in containers through owning pointers: std::vector<std::unique_ptr<Shape>>.
  • Make polymorphic bases non-copyable, or give them protected copy operations, so accidental slicing becomes a compile error. Making the base abstract also prevents creating a standalone Shape.
  • Provide a virtual clone() returning std::unique_ptr<Shape> when you genuinely need to copy a polymorphic object.

Note: The C++ Core Guidelines rule C.67 says a polymorphic class should suppress public copy and move. Following it turns slicing from a silent runtime bug into a compile-time error.

33. What are pure virtual functions and abstract classes, and what happens if you call a virtual function from a constructor?

A pure virtual function is declared with = 0. It says “derived classes must provide this”. A class with at least one pure virtual function is abstract: you cannot create objects of it, only of derived classes that override every pure virtual function.

class Storage {
public:
virtual ~Storage() = 0; // pure, but needs a body
virtual void write(std::span<const std::byte> data) = 0;
virtual std::size_t size() const = 0;
};
Storage::~Storage() = default;

class FileStorage : public Storage {
public:
void write(std::span<const std::byte> data) override;
std::size_t size() const override;
};
  • Abstract classes define interfaces. C++ has no interface keyword; a class with only pure virtual functions and a virtual destructor plays that role.
  • A pure virtual function can still have a definition. Derived classes can call it explicitly as a default behaviour. A pure virtual destructor must have one, because every derived destructor calls it.
  • Forgetting to override one pure virtual function leaves the derived class abstract, and the error appears only where you try to instantiate it.

Virtual calls during construction and destruction do not dispatch to derived overrides. While the base constructor runs, the object is a base object — the derived members are not yet initialised — so the base version is called. The same applies in reverse in destructors.

If the base version is pure and is reached indirectly from a constructor, the behaviour is undefined; in practice the program aborts with “pure virtual function called”.

The fix is to pass what the base needs as constructor arguments, or to use a factory function that fully constructs the object and then calls an initialisation step.

Note: Compilers warn about direct pure virtual calls in constructors, but not about ones made through a helper function. Static analysers such as clang-tidy flag this pattern, which is worth enabling in any codebase with deep class hierarchies.

34. What is the Curiously Recurring Template Pattern, and when would you use it instead of virtual functions?

In the Curiously Recurring Template Pattern (CRTP), a class derives from a base template instantiated with itself. The base knows the derived type at compile time and can call into it with a static_cast, giving static polymorphism — no vtable, no vptr, and calls that can be fully inlined.

template <typename Derived>
class Shape {
public:
double area() const {
return static_cast<const Derived&>(*this).area_impl();
}
double scaled_area(double k) const { return k * k * area(); }
};

class Square : public Shape<Square> {
public:
explicit Square(double s) : side_(s) {}
double area_impl() const { return side_ * side_; }
private:
double side_;
};

Where it is used:

  • Performance-critical dispatch — numeric libraries such as Eigen use CRTP with expression templates so that operations inline into tight loops.
  • Mixins — adding behaviour to many classes: an instance counter, comparison operators derived from one method, or std::enable_shared_from_this, which is itself CRTP.
  • Compile-time interfaces in code where the set of types is known and runtime polymorphism is not needed.

Limitations compared with virtual functions:

  • There is no common base type — Shape<Square> and Shape<Circle> are unrelated — so you cannot store mixed shapes in one container.
  • Every derived type instantiates the base again, which can grow code size.
  • Mistakes such as deriving from the wrong instantiation compile silently unless you guard against them, for example with a private base constructor and a friend declaration.

Note: C++23 “deducing this” lets a member function take an explicit object parameter and deduce the derived type directly, which removes most of the boilerplate CRTP needed. Mentioning it shows you follow the standard's evolution.

35. What are the most common sources of undefined behaviour in C++, and why can it break code in surprising ways under optimisation?

Undefined behaviour (UB) means the standard places no requirements on what the program does. It is not just “an unpredictable value” — the whole execution loses its meaning, and the optimiser is allowed to assume UB never happens.

The usual suspects:

  • Signed integer overflow (unsigned overflow is well defined and wraps).
  • Dereferencing null, dangling or out-of-bounds pointers; use after free; double delete.
  • Reading an uninitialised variable.
  • Data races on non-atomic variables.
  • Violating strict aliasing — accessing an object through a pointer of an unrelated type.
  • Shifting by a negative amount or by at least the width of the type.
  • Falling off the end of a non-void function, or modifying a string literal.

Why the effects are so strange: compilers reason “this cannot happen in a valid program” and delete code accordingly.

bool will_overflow(int x) {
return x + 1 < x; // UB on overflow, so this may compile to false
}

void process(int* p) {
int v = *p; // compiler now assumes p is not null
if (p == nullptr) // this check may be removed entirely
return;
use(v);
}

The bug may therefore appear only in release builds, only with one compiler version, or far away from its cause — and it can turn into a security hole when a safety check is optimised out.

How to defend against it:

  • Build and test with -fsanitize=address,undefined, and ThreadSanitizer for concurrent code.
  • Enable -Wall -Wextra and static analysis.
  • Use std::memcpy or C++20 std::bit_cast for type punning, and checked arithmetic helpers where overflow is possible.
  • Constant evaluation is strict: UB inside a constexpr evaluation is a compile error, which makes it a handy test tool.

Note: Distinguish UB from unspecified and implementation-defined behaviour. Argument evaluation order is unspecified but still valid; the size of int is implementation-defined and documented. Only UB licenses the compiler to do anything at all.

36. What are the iterator invalidation rules for the main STL containers, and how do you erase elements safely while iterating?

An iterator, pointer or reference is invalidated when the element it refers to moves or disappears. Using an invalidated iterator is undefined behaviour, and it is one of the most common real-world C++ bugs.

ContainerInsertionErasure
vector, stringIf it reallocates, everything is invalidated; otherwise only elements at or after the insertion pointElements at or after the erased position, plus end()
dequeAt either end: all iterators, but references stay valid; in the middle: everythingAt either end: only the erased elements (and end() if the last one); in the middle: everything
list, forward_listNothingOnly the erased elements
map, setNothingOnly the erased elements
unordered_map, unordered_setIf it rehashes, all iterators — but references and pointers remain validOnly the erased elements

Erasing while iterating: erase returns the iterator to the next element, so use it instead of incrementing.

for (auto it = orders.begin(); it != orders.end(); ) {
if (it->cancelled)
it = orders.erase(it); // continue from the returned iterator
else
++it;
}

// C++20: one call, works for vector, deque, list, maps and sets
std::erase_if(orders, [](const Order& o) { return o.cancelled; });

For vector, the loop above is quadratic because each erase shifts the tail. std::erase_if, or the older erase-remove idiom, does it in a single linear pass.

  • Push inside a range-for over a vector is a classic bug: a reallocation invalidates the loop's hidden iterators.
  • Calling reserve() in advance guarantees no reallocation until the size exceeds the capacity, which keeps iterators stable during insertion.

Note: Debug modes of the standard library, such as _GLIBCXX_DEBUG in libstdc++ or the checked iterators in MSVC, abort on use of an invalidated iterator. Running tests with them is a cheap way to catch these bugs early.

37. How do lambdas work under the hood in C++, and what are the pitfalls of capturing by reference?

A lambda expression creates an object of a unique, unnamed closure type — effectively a compiler-generated class whose captured variables are data members and whose body is a const operator().

int threshold = 10;
auto big = [threshold](int x) { return x > threshold; };

// roughly equivalent to:
struct Closure {
int threshold;
bool operator()(int x) const { return x > threshold; }
};

Capture options:

  • [x] copy, [&x] reference, [=] and [&] default captures.
  • [p = std::move(ptr)] — init capture (C++14), the way to move a unique_ptr into a lambda.
  • [this] captures the pointer; [*this] (C++17) copies the whole object.
  • mutable makes operator() non-const so by-copy captures can be modified.
  • auto parameters make a generic lambda (C++14); C++20 adds explicit template parameter lists.

The big pitfall is lifetime. A reference capture does not extend the lifetime of what it refers to. If the lambda outlives the scope — stored in a std::function, handed to a thread, or queued as a callback — it dangles.

std::function<int()> make_counter() {
int count = 0;
return [&count] { return ++count; }; // dangling: count dies here
}

void Widget::start() {
pool.submit([=] { refresh(); }); // [=] captured this, not a copy
}

The second case is subtle: [=] captures this implicitly, so the “copy” lambda still dereferences the object, which may be destroyed before the task runs. C++20 deprecates that implicit capture for exactly this reason.

Guidelines: use [&] only for lambdas used immediately, such as algorithm predicates; capture explicitly by value for anything stored or asynchronous; and capture a shared_ptr or weak_ptr to the object for async callbacks.

Note: Captureless lambdas convert implicitly to plain function pointers, which is how they are passed to C APIs such as qsort or thread-creation callbacks.

38. What are the basic, strong and no-throw exception safety guarantees, and how does copy-and-swap provide the strong guarantee?

Exception safety describes what state an operation leaves behind if it throws. There are three standard levels:

  • Basic guarantee — no resources leak and all objects remain in a valid (destructible, usable) state, though that state may have changed.
  • Strong guarantee — commit-or-rollback: if the operation throws, the program state is exactly as it was before the call.
  • No-throw guarantee — the operation never throws. Destructors, swap, move operations and memory deallocation should offer this, because the other guarantees are built on them.

RAII gives you the basic guarantee almost automatically. The strong guarantee usually needs the pattern “do all the work that can fail on the side, then commit with operations that cannot fail”.

Copy-and-swap applies that pattern to assignment:

class Buffer {
public:
Buffer(const Buffer& other); // deep copy, may throw
Buffer(Buffer&& other) noexcept; // cheap, cannot throw
Buffer& operator=(Buffer other) noexcept { // copy made in the parameter
swap(*this, other); // cannot throw
return *this; // old state dies with other
}
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
private:
std::unique_ptr<char[]> data_;
std::size_t size_ = 0;
};

If the copy throws, it throws before *this is touched. The by-value parameter also handles self-assignment and serves as the move assignment when called with an rvalue.

Trade-off: it always allocates a new buffer, even when the existing one is large enough to reuse. For performance-critical types like std::vector, the standard library gives only the basic guarantee for assignment for this reason.

Note: The standard containers document their guarantees. vector push_back gives the strong guarantee, but only if the element's move constructor is noexcept or the type is copyable — which is one more reason to mark move operations noexcept.

39. What happens when an exception escapes a noexcept function, and how do you decide which functions to mark noexcept?

noexcept is a promise that a function will not throw. If an exception tries to leave it anyway, the runtime calls std::terminate. Whether the stack is unwound first is implementation-defined, so destructors of local objects may never run. There is no way to catch the exception outside the function.

Since C++17, noexcept is part of the function type, and the noexcept(expr) operator lets code ask at compile time whether an expression can throw.

Why it matters for performance: generic code picks faster algorithms when operations cannot throw. The best-known example is std::vector reallocation, which moves elements only if the move constructor is noexcept (via std::move_if_noexcept); otherwise it copies them to preserve the strong guarantee.

class Buffer {
public:
Buffer(Buffer&& other) noexcept; // vector can move on growth
Buffer& operator=(Buffer&& other) noexcept;
~Buffer(); // implicitly noexcept
};

template <typename T>
void my_swap(T& a, T& b) noexcept(
std::is_nothrow_move_constructible_v<T> &&
std::is_nothrow_move_assignable_v<T>); // conditional noexcept

Mark as noexcept:

  • Move constructors and move assignment operators.
  • swap functions.
  • Destructors (already implicit unless a member's destructor can throw).
  • Simple functions that genuinely cannot fail, such as getters.

Do not mark a function that allocates memory, calls code that can throw, or might need to report errors by exception in future. Removing noexcept later is a breaking change for callers who relied on it, and marking it wrongly converts a recoverable error into program termination.

Note: Defaulted special member functions are automatically noexcept when all members' corresponding operations are, so following the Rule of Zero often gives you correct noexcept specifications for free.

40. What is std::atomic, and what do the sequentially consistent, acquire-release and relaxed memory orders mean?

std::atomic<T> makes operations on a variable indivisible and free of data races, so several threads can read and modify it without a mutex. Operations include load, store, exchange, fetch_add and compare_exchange_weak or compare_exchange_strong.

Atomicity alone is not enough: compilers and CPUs reorder ordinary memory accesses. The memory order argument says how an atomic operation orders the surrounding non-atomic reads and writes.

  • memory_order_seq_cst (the default) — all threads agree on a single total order of these operations. The easiest to reason about, and slightly more expensive on some architectures.
  • Acquire-release — a store with release publishes every write made before it; a load with acquire that reads that value sees all of them. This is the standard way to hand data from one thread to another.
  • memory_order_relaxed — atomicity only, with no ordering of other accesses. Fine for statistics counters, wrong for publishing data.
std::atomic<bool> ready{false};
int payload = 0; // ordinary variable

void producer() {
payload = 42;
ready.store(true, std::memory_order_release); // publish
}

void consumer() {
while (!ready.load(std::memory_order_acquire)) { }
assert(payload == 42); // guaranteed
}

With relaxed on both sides, the consumer could see ready as true and still read a stale payload on a weakly ordered CPU such as ARM.

  • volatile is not a threading tool in C++; it neither prevents data races nor orders memory.
  • is_lock_free() tells you whether an atomic type uses hardware instructions or an internal lock.

Note: Start with the default sequential consistency, or better, a mutex. Weaker orders are for measured hot spots and experts, and bugs in them often show up only on ARM hardware, never on the x86 development machine.

41. Why can a condition variable wake up without being notified, and how do you use std::condition_variable correctly?

A std::condition_variable lets a thread sleep until another thread signals that some shared state has changed. Two facts make it easy to misuse:

  • Spurious wakeups — the standard allows wait to return without any notification, because some operating systems implement it that way.
  • Stolen or lost state — between the notification and the woken thread reacquiring the mutex, another thread may consume the item. And a notification sent before anyone is waiting is simply lost.

The rule therefore is: always wait on a condition over shared state, protected by the mutex, and re-check it after waking. The predicate overload of wait does the loop for you.

std::mutex m;
std::condition_variable cv;
std::queue<Job> jobs;
bool stopping = false;

void push(Job j) {
{
std::lock_guard lk(m);
jobs.push(std::move(j));
}
cv.notify_one();
}

std::optional<Job> pop() {
std::unique_lock lk(m);
cv.wait(lk, [&] { return stopping || !jobs.empty(); });
if (jobs.empty()) return std::nullopt; // woken for shutdown
Job j = std::move(jobs.front());
jobs.pop();
return j;
}
  • std::unique_lock is required, because wait must unlock the mutex while sleeping and relock it before returning.
  • Modify the shared state while holding the mutex, even if it is a single flag. Otherwise the change can slip between the waiter's check and its sleep, and the wake-up is lost.
  • Notifying after releasing the lock is correct and avoids waking a thread that immediately blocks on the mutex.
  • Plan shutdown: set a stop flag under the lock and call notify_all.

Note: C++20 adds std::condition_variable_any with stop_token support and std::jthread, so a waiting worker can be cancelled cleanly. For simple one-shot signals, std::latch or a std::promise and future pair is often clearer than a condition variable.

42. What causes deadlock in multithreaded C++, and how do std::scoped_lock and a consistent lock order prevent it?

A deadlock occurs when threads wait on each other in a cycle and none can proceed. The textbook conditions are mutual exclusion, holding one lock while waiting for another, no forced release, and a circular wait. Breaking any one prevents deadlock; in practice you break the circular wait.

The classic case: thread A locks account X then Y, while thread B locks Y then X. Each holds the lock the other needs.

struct Account {
std::mutex m;
long balance = 0;
};

void transfer(Account& from, Account& to, long amount) {
if (&from == &to) return; // same mutex twice would be UB
std::scoped_lock lock(from.m, to.m); // locks both, deadlock-free
from.balance -= amount;
to.balance += amount;
}

std::scoped_lock (C++17) locks several mutexes using a deadlock-avoidance algorithm, the same one as std::lock, so the argument order does not matter. It is RAII, so all locks are released on every exit path.

Other techniques:

  • A global lock hierarchy — give every mutex a level and always acquire in the same order. Some teams enforce it with a debug-mode wrapper that asserts on violations.
  • Never call unknown code while holding a lock — callbacks, virtual functions supplied by users, or signals may try to take the same lock.
  • Keep critical sections short and do slow work, such as I/O, outside the lock.
  • Avoid locking the same non-recursive mutex twice in one thread; that is undefined behaviour and usually a self-deadlock. Needing a recursive mutex is often a design smell.
  • Use try_lock_for with a timeout where a stuck operation should fail rather than hang.

Note: ThreadSanitizer reports lock-order inversions even when the deadlock did not actually happen during the test run, which makes it far more effective than waiting for a hang in production.

43. What does the inline keyword really mean in modern C++, and what is the One Definition Rule?

The One Definition Rule (ODR) says that every non-inline function and variable used in a program must be defined exactly once across all translation units. Classes, templates and inline functions and variables may be defined in several translation units, but every definition must be identical.

inline is about the ODR, not about inlining. Its guaranteed meaning is “this definition may appear in multiple translation units; the linker keeps one”. Whether a call is actually inlined is decided by the optimiser, which inlines non-inline functions freely and ignores the hint when it wants to.

// limits.h — included by many .cpp files
inline int twice(int x) { return 2 * x; } // OK in every TU

inline constexpr int kMaxUsers = 100; // one shared entity (C++17)

struct Registry {
static inline int count = 0; // no separate .cpp definition
int size() const { return n_; } // implicitly inline
int n_ = 0;
};
  • Implicitly inline: member functions defined inside the class body, constexpr functions, and function templates behave similarly.
  • Inline variables (C++17) let header-only libraries define global constants and static data members without a separate source file.
  • A namespace-scope const or constexpr variable has internal linkage, so each translation unit gets its own copy with its own address. inline constexpr makes it a single entity.
  • A non-inline function defined in a header and included twice gives a “multiple definition” link error.

The dangerous violations are silent. If two translation units define a class or inline function with the same name but different contents — for example because of different macros or compiler flags — the program is ill-formed with no diagnostic required. The linker picks one definition, and code built against the other misbehaves at run time.

Note: Put private helpers in an unnamed namespace so they get internal linkage. That prevents accidental ODR clashes between two .cpp files that happen to use the same helper name.

44. What is the static initialisation order fiasco, and how do you avoid it in C++?

Objects with static storage duration — globals, namespace-scope variables and static data members — are initialised before main. Within one translation unit they are initialised in definition order, but the order across translation units is unspecified.

The fiasco happens when the dynamic initialisation of one global uses another global from a different file that has not been initialised yet:

// config.cpp
std::string g_app_name = load_name();

// logger.cpp
Logger g_logger(g_app_name); // may read g_app_name before it is constructed

It may work on one build and crash on the next, depending purely on link order.

Ways to avoid it:

  • Function-local statics (the “Meyers singleton”). The object is constructed the first time control passes through its declaration, and since C++11 that initialisation is thread-safe.
  • Constant initialisation. Variables initialised with constant expressions are set at compile time, before any dynamic initialisation, so order does not matter. C++20 constinit makes the compiler enforce it.
  • Avoid mutable globals altogether: pass dependencies explicitly or create them in main.
Logger& logger() {
static Logger instance(app_name()); // built on first use, thread-safe
return instance;
}

constexpr int compute_limit() { return 64 * 1024; }
constinit int g_limit = compute_limit(); // error unless constant-initialised

The mirror problem exists at shutdown: statics are destroyed in reverse order of construction, so a destructor that uses another static may touch a destroyed object. A common fix for long-lived services such as loggers is to allocate them with new inside the function and deliberately never delete them.

Note: The fiasco often appears when plugins or self-registering factories use global objects to register themselves. A registry obtained through a function-local static is the standard, safe way to build those.

45. What are the four C++ cast operators, and when is each one appropriate?

C++ splits the single C-style cast into four named casts, each with a narrow purpose. They are easy to search for and the compiler rejects conversions outside their remit.

  • static_cast — well-defined conversions checked at compile time: numeric conversions, void* back to a typed pointer, explicit constructor or conversion operator calls, and downcasts in a class hierarchy when you know the dynamic type. A wrong downcast is undefined behaviour — nothing is checked at run time.
  • dynamic_cast — checked downcasts and cross-casts on polymorphic types, using RTTI. For pointers it returns nullptr on failure; for references it throws std::bad_cast. It has a runtime cost and needing it often is a design smell — a virtual function is usually better.
  • const_cast — adds or removes const or volatile. Mainly for calling legacy APIs that are not const-correct. Modifying an object that was originally declared const through it is undefined behaviour.
  • reinterpret_cast — reinterprets bits: pointer to integer, unrelated pointer types, function pointer conversions. Highly implementation-specific, and accessing the result as a different type usually violates strict aliasing.
double ratio = static_cast<double>(hits) / total;

if (auto* c = dynamic_cast<Circle*>(shape)) {
c->set_radius(2.0);
}

legacy_print(const_cast<char*>(name.c_str())); // API never writes

auto addr = reinterpret_cast<std::uintptr_t>(ptr);
float f = std::bit_cast<float>(bits); // C++20 safe type punning

Why avoid the C-style cast (T)x: it silently tries const_cast, static_cast and reinterpret_cast in combination, so it can quietly cast away const or reinterpret memory when you meant a numeric conversion. It is also impossible to grep for.

Note: For type punning, use std::memcpy or C++20 std::bit_cast rather than reinterpret_cast or a union. Both are well defined, and compilers optimise them into the same single instruction.

46. What is std::variant, and how does std::visit compare with using a virtual class hierarchy?

std::variant (C++17) is a type-safe union: it holds exactly one value from a fixed list of types, stored inline without heap allocation, and it knows which one it holds.

  • std::get<T>(v) returns the value or throws std::bad_variant_access; std::get_if<T>(&v) returns a pointer or null; v.index() and std::holds_alternative<T>(v) query the active type.
  • std::visit calls a callable with whichever alternative is active. Combined with the “overloaded” helper, it reads like pattern matching, and the compiler errors if an alternative is not handled.
template <class... Fs> struct overloaded : Fs... { using Fs::operator()...; };

struct Circle { double r; };
struct Square { double side; };
using Shape = std::variant<Circle, Square>;

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

(In C++17 the overloaded helper also needs a one-line deduction guide; C++20 deduces it automatically.)

Variant versus virtual functions is a trade-off along two axes:

std::variantVirtual hierarchy
Adding a new typeEdit the variant and every visitorJust add a derived class
Adding a new operationJust write a new visitorEdit the base and every class
StorageValue semantics, inline, contiguous in a vectorHeap objects behind pointers

Choose variant for a closed set of types known in one place — message types, parser tokens, state machine states. Choose inheritance for open extension, such as plugins written by other teams.

Note: A variant can become valueless_by_exception if a type's constructor throws during assignment. It is rare, but visiting a valueless variant throws, so strong answers mention it.

47. What does if constexpr do, and how does it simplify template code compared with tag dispatch or SFINAE?

if constexpr (C++17) is an if whose condition is evaluated at compile time. Inside a template, the branch not taken is discarded: it is not instantiated, so it may contain code that would not compile for the current type.

template <typename T>
std::string to_text(const T& v) {
if constexpr (std::is_same_v<T, std::string>)
return v;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(v);
else
return v.to_string(); // only instantiated for other types
}

With an ordinary if, every branch is compiled for every T, so std::to_string(v) on a std::string or v.to_string() on an int would be errors even though they never run.

What it replaces:

  • Tag dispatch — separate overloads selected by a std::true_type or std::false_type argument.
  • SFINAE overload sets with mutually exclusive enable_if conditions.
  • Recursive variadic templates needing a separate base case, which can now be written as a single function with if constexpr (sizeof...(rest) > 0).

The logic reads top to bottom in one function, which is much easier to review.

Rules and gotchas:

  • The condition must be a constant expression.
  • Discarding only happens inside templates and depends on the template parameters. In a non-template function both branches must still be valid code.
  • Before C++23, writing static_assert(false) in the final else branch made the program ill-formed; the workaround was a condition dependent on T. C++23 allows it in discarded branches.
  • It chooses code paths inside one function; it does not remove the function from overload resolution. For that, use a concept or requires clause.

Note: Do not confuse it with C++23 if consteval, which asks whether the current evaluation is happening at compile time rather than testing a constant condition.

48. What does the C++20 three-way comparison operator do, and what do you get by defaulting it?

C++20 adds operator<=>, nicknamed the “spaceship” operator. a <=> b returns an ordering object that says whether a is less than, equal to (or equivalent to), or greater than b in a single call.

The compiler rewrites the relational operators in terms of it: a < b becomes (a <=> b) < 0, and reversed arguments are tried automatically. One function replaces the four relational operators you used to write by hand.

struct Version {
int major = 0, minor = 0, patch = 0;
auto operator<=>(const Version&) const = default;
};

Version a{1, 2, 0}, b{1, 10, 0};
bool older = a < b; // true: compared member by member
bool same = a == b; // false: == is also defaulted implicitly
std::set<Version> releases{a, b}; // works as a key out of the box

What defaulting gives you:

  • A lexicographic comparison of base classes and then members, in declaration order.
  • An implicitly declared, defaulted operator== as well, so all six comparison operators work.
  • The correct return category, deduced from the members.

The three ordering categories:

  • std::strong_ordering — equal values are indistinguishable, as with integers.
  • std::weak_ordering — equivalent values may differ, such as case-insensitive strings.
  • std::partial_ordering — some values are unordered, such as floating-point NaN. A struct containing a double gets this category.

A subtle rule: if you write operator<=> yourself rather than defaulting it, you do not get operator== for free. That is deliberate — equality can often be checked more cheaply, for example by comparing string lengths first, so the language keeps == separate.

Note: Defaulted comparisons follow declaration order, so reordering members silently changes the sort order of your type. Keep the member order meaningful, or write the comparison explicitly when ordering is part of the type's contract.

49. What are C++20 ranges and views, and how do they differ from classic iterator-pair algorithms?

The Ranges library, in <ranges> and <algorithm>, modernises the STL in three ways.

  • Algorithms take whole ranges. std::ranges::sort(v) instead of std::sort(v.begin(), v.end()), which removes the mismatched-iterator bug entirely. They are constrained with concepts, so errors are clearer.
  • Projections. Algorithms accept a callable applied to each element before comparison, so sorting people by age needs no custom comparator.
  • Views. Lightweight, lazy adaptors that transform or filter a range without copying it, and compose with the pipe operator.
std::vector<int> v{5, 3, 8, 1, 9, 2};

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

for (int x : evens_squared) { /* 64, then 4 */ }

std::ranges::sort(v); // no begin/end pair
std::ranges::sort(people, {}, &Person::age); // projection
auto it = std::ranges::find(people, 42, &Person::id);

Laziness means nothing is computed until you iterate, and each element flows through the whole pipeline one at a time. That allows infinite ranges such as std::views::iota(1) combined with std::views::take(10).

Pitfalls worth knowing:

  • Views do not own data. A view over a temporary or a destroyed container dangles. Algorithms return std::ranges::dangling instead of an iterator when passed a temporary, which catches some of these at compile time.
  • Some views cache state, such as filter caching its first element, so they cannot be iterated through a const reference and should not be reused after the underlying container changes.
  • Materialising a result into a container needs a loop in C++20; C++23 adds std::ranges::to.
  • Compile times and debug performance can suffer with long pipelines.

Note: Ranges also introduced sentinels — an end marker of a different type from the iterator — which lets algorithms work directly on things like null-terminated strings without first computing their length.

50. What are struct padding and alignment in C++, and how can false sharing hurt multithreaded performance?

Every type has an alignment requirement: its address must be a multiple of some power of two, typically its size for built-in types. To satisfy it, the compiler inserts padding bytes between members and at the end of a struct, so that arrays of the struct stay aligned too.

struct Loose {        // typical 64-bit layout
char flag; // 1 byte + 7 padding
double value; // 8 bytes
int id; // 4 bytes + 4 padding
}; // sizeof == 24

struct Tight {
double value; // 8
int id; // 4
char flag; // 1 + 3 padding
}; // sizeof == 16

Ordering members from largest to smallest alignment often shrinks a struct noticeably, which matters when you hold millions of them — more objects per cache line means fewer cache misses. alignof(T) reports the requirement and alignas(N) raises it.

Avoid #pragma pack for ordinary code: misaligned access is slow on some CPUs, can fault on others, and taking a reference to a packed member is dangerous.

False sharing is the multithreaded cousin of this problem. CPUs move memory between cores in cache lines, commonly 64 bytes. If two threads write to different variables that sit on the same line, each write invalidates the other core's copy, and the line ping-pongs between cores. The code is correct but can run many times slower than expected.

struct alignas(64) PaddedCounter {      // one cache line each
std::atomic<long> value{0};
};

PaddedCounter per_thread[8]; // no two counters share a line

C++17 provides std::hardware_destructive_interference_size as a portable constant for this, although not every standard library implements it.

Note: False sharing is invisible in the source, so find it with tools: perf c2c on Linux or VTune's memory-access analysis. A classic symptom is a parallel loop that gets slower as you add threads.

Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up as