Skip to content

C++ Interview Questions for 10 Years Experience

40 questions with answers for candidates with 10+ years of experience — 12 medium, 28 hard.

What interviewers expect: 10 years experience

At ten years and beyond, candidates are evaluated as principal engineers, architects or engineering managers. Discussions cover technology strategy, long-term evolution of large C++ codebases, hiring and growing teams, and aligning engineering with business goals. Expect deep architecture discussions, stakeholder management scenarios and questions on how you influence standards across multiple teams.

This set is weighted towards Templates and Generic Programming (8), Standard Template Library (STL) (6), Object-Oriented Programming in C++ (6), Multithreading and Concurrency (5).

Technical strategy Large-scale architecture Organisational influence Hiring and team growth
Solved at this levelStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

When do you need the typename keyword inside a template?

Save to ▾
Show answer
Inside a template, a name that depends on a template parameter, such as T::value_type, could be a type or a static member, so the compiler assumes it is a value. You must write typename T::value_type x; to say it is a type. Similarly, a dependent member template needs the template keyword, as in obj.template get<0>(). C++20 relaxes this in some contexts, but the rule still matters.
02

When would you use std::set versus std::priority_queue?

Save to ▾
Show answer
priority_queue is a binary heap that gives O(1) access to the largest element and O(log n) push and pop, but you cannot search or erase arbitrary elements. std::set keeps all elements sorted with O(log n) insert, erase and find, and gives both minimum and maximum. Use priority_queue for schedulers and Dijkstra, and set or multiset when you must update or remove arbitrary items.
03

A generic average function must accept only numeric types. How would you enforce that?

Save to ▾
Show answer
In C++20 I would constrain it: template <typename T> requires std::is_arithmetic_v<T> double average(const std::vector<T>& v). Passing a vector of strings then fails with a clear constraint error. On C++17 the same check can go in a static_assert or std::enable_if. I would also decide whether bool and char should be allowed, since they are technically arithmetic types.
04

Write a micro-benchmark with Google Benchmark comparing two approaches.

Save to ▾
Show answer
#include <benchmark/benchmark.h> #include <list> #include <numeric> #include <vector> static void BM_VectorSum(benchmark::State& state) { std::vector<int> v(state.range(0), 1); for (auto _ : state) benchmark::DoNotOptimize(std::accumulate(v.begin(), v.end(), 0)); } BENCHMARK(BM_VectorSum)->Range(1 << 10, 1 << 20); static void BM_ListSum(benchmark::State& state) { std::list<int> l(state.range(0), 1); for (auto _ : state) benchmark::DoNotOptimize(std::accumulate(l.begin(), l.end(), 0)); } BENCHMARK(BM_ListSum)->Range(1 << 10, 1 << 20); BENCHMARK_MAIN();
05

Implement an LRU cache with get and put in O(1).

Save to ▾
Show answer
#include <list> #include <unordered_map> class LRUCache { int cap_; std::list<std::pair<int, int>> items_; // front = most recent std::unordered_map<int, std::list<std::pair<int, int>>::iterator> pos_; public: explicit LRUCache(int capacity) : cap_(capacity) {} int get(int key) { auto it = pos_.find(key); if (it == pos_.end()) return -1; items_.splice(items_.begin(), items_, it->second); return it->second->second; } void put(int key, int value) { if (auto it = pos_.find(key); it != pos_.end()) { it->second->second = value; items_.splice(items_.begin(), items_, it->second); return; } if (static_cast<int>(items_.size()) == cap_) { pos_.erase(items_.back().first); items_.pop_back(); } items_.emplace_front(key, value); pos_[key] = items_.begin(); } };
06

What are placement new and memory pools?

Save to ▾
Show answer
Placement new constructs an object in memory you already own, written new (buffer) T(args). It does not allocate, so you must call the destructor yourself and release the buffer separately. A memory pool pre-allocates large blocks and hands out fixed-size slots, avoiding slow general-purpose allocation and fragmentation. Game engines, low-latency trading systems and std::pmr allocators use these for predictable performance.
07

What is perfect forwarding?

Save to ▾
Show answer
Perfect forwarding passes arguments through a function template while preserving whether they were lvalues or rvalues. It uses a forwarding reference, T&& where T is deduced, together with std::forward<T>(arg). Example: template <typename... A> void make(A&&... a) { build(std::forward<A>(a)...); }. Without forward, every named parameter is an lvalue and would be copied. make_unique and emplace_back rely on it.
08

Many threads read a shared configuration object constantly, but it changes only once an hour. How do you synchronise it efficiently?

Save to ▾
Show answer
A plain mutex would serialise all readers. Option one is std::shared_mutex: readers take shared_lock and run in parallel, the updater takes unique_lock. Option two, often faster, is to keep the config in a std::shared_ptr<const Config> and swap in a new immutable object atomically with std::atomic<std::shared_ptr> in C++20; readers grab a copy of the pointer and never block the writer.
09

You need to build an LRU cache with O(1) get and put. Which containers would you use?

Save to ▾
Show answer
Combine a doubly linked list with a hash map. std::list stores key-value pairs in usage order, with the most recent at the front. std::unordered_map maps each key to its list iterator. On get, splice the node to the front in O(1). On put, update or insert at the front and, if over capacity, remove the back node and its map entry. list iterators stay valid after splice, which makes this work.
10

You inherit a 3000-line class that handles parsing, validation, database and logging. How would you refactor it?

Save to ▾
Show answer
It breaks the single responsibility principle. First I add characterisation tests around current behaviour. Then I extract cohesive pieces into separate classes such as Parser, Validator and Repository, injected through interfaces so they can be mocked. I move step by step, keeping the old public API as a facade, and merge small refactors frequently instead of one big rewrite, so risk stays low and reviews stay easy.
11

A hot loop calls a virtual function millions of times and profiling shows the indirect call cost. What can you do?

Save to ▾
Show answer
Options include static polymorphism with CRTP or templates so calls can be inlined, marking classes or functions final so the compiler can devirtualise, and batching work by type so each loop handles one concrete class. Another option is replacing the hierarchy with std::variant and std::visit. I would confirm with a benchmark first, because often memory layout matters more than the virtual call itself.
12

Build times have become very slow because heavy templates are included everywhere. How do you speed them up?

Save to ▾
Show answer
I would measure first with -ftime-trace or build analyzers to find the costly headers. Then reduce includes with forward declarations, move template-heavy implementation details behind non-template interfaces, use extern template with explicit instantiation for common types, enable precompiled headers and a compiler cache such as ccache, and evaluate C++20 modules where the toolchain supports them well.
13

Tell me about a concurrency bug you investigated and how you solved it.

Save to ▾
Show answer
Situation: a market data service dropped a few updates per day only under heavy load. Task: find the cause without disrupting trading hours. Action: I wrote a stress harness replaying recorded traffic, ran it under ThreadSanitizer and found two threads updating a shared index without synchronisation. I redesigned it to a single-writer model with an SPSC queue. Result: zero dropped updates over a month and latency improved slightly because locks were removed.
14

The release binary has grown very large and analysis shows many template instantiations. What can you do?

Save to ▾
Show answer
Each distinct set of template arguments generates separate code, so heavy generic code bloats binaries. I would find the biggest symbols with tools like bloaty or nm, move type-independent logic into a non-template base or helper, use type erasure for rarely hot paths, limit instantiations with extern template, and enable link-time optimisation plus identical code folding to merge duplicate functions.
15

Implement a basic Observer pattern for an event publisher.

Save to ▾
Show answer
#include <functional> #include <string> #include <vector> class EventBus { public: using Handler = std::function<void(const std::string&)>; int subscribe(Handler h) { handlers_.push_back({nextId_, std::move(h)}); return nextId_++; } void unsubscribe(int id) { std::erase_if(handlers_, [id](const auto& p) { return p.first == id; }); } void publish(const std::string& msg) const { for (const auto& [id, h] : handlers_) h(msg); } private: std::vector<std::pair<int, Handler>> handlers_; int nextId_ = 0; };
16

Implement a thread-safe blocking queue.

Save to ▾
Show answer
#include <condition_variable> #include <mutex> #include <optional> #include <queue> template <typename T> class BlockingQueue { std::queue<T> q_; std::mutex m_; std::condition_variable cv_; bool closed_ = false; public: void push(T v) { { std::lock_guard<std::mutex> lk(m_); q_.push(std::move(v)); } cv_.notify_one(); } std::optional<T> pop() { std::unique_lock<std::mutex> lk(m_); cv_.wait(lk, [&] { return closed_ || !q_.empty(); }); if (q_.empty()) return std::nullopt; // closed and drained T v = std::move(q_.front()); q_.pop(); return v; } void close() { { std::lock_guard<std::mutex> lk(m_); closed_ = true; } cv_.notify_all(); } };
17

Tell me about a time you convinced your team to adopt a new C++ feature or practice.

Save to ▾
Show answer
Situation: our team avoided smart pointers, and leaks kept reaching QA. Task: get agreement to adopt unique_ptr in new code. Action: I prepared a short benchmark showing no measurable overhead, converted one leaky module as a pilot, and presented before and after defect counts at our tech sync. I also wrote clang-tidy rules to help. Result: the team agreed to make it a coding standard and leak bugs dropped sharply in the next quarter.
18

Tell me about a time you wrote generic, reusable code that saved effort for your team.

Save to ▾
Show answer
Situation: three services each had their own copy of retry logic for network calls. Task: remove duplication without slowing them down. Action: I wrote a small header-only template, retry with a callable and a policy for attempts and backoff, added unit tests and documented examples. Result: all three services adopted it, around 600 lines of duplicate code were deleted and a bug fix now happens in one place.
19

Tell me about a mistake you made in production and what you learned from it.

Save to ▾
Show answer
Situation: I released a change that used string_view in a cache, and a week later we saw corrupted keys. Task: fix it fast and prevent repeats. Action: I rolled back, reproduced the issue with AddressSanitizer, found the views referenced temporary strings, switched to owning std::string keys and wrote a post-mortem. Result: no further incidents, and I added a lifetime checklist and ASan stage to our pipeline that caught two similar bugs later.
20

Using a custom struct as the key of an unordered_map gives a compile error. Why, and how do you fix it?

Save to ▾
Show answer
unordered_map needs a hash function and equality comparison for the key type, and std::hash is not defined for user structs. Fix it by defining operator== (or defaulting it in C++20) and providing a hash functor that combines member hashes, passed as the third template argument, or by specialising std::hash for the type. std::map instead needs operator< or a comparator.
21

You are building a particle simulation with millions of particles. Would you store them as an array of structs or a struct of arrays?

Save to ▾
Show answer
If each update loop touches only a few fields, such as position and velocity, a struct of arrays keeps those fields contiguous, so every cache line is fully used and the compiler can vectorise with SIMD. An array of structs is simpler and better when most fields of one particle are used together. I would benchmark both, and often a hybrid that groups hot fields together works best.
22

What are variadic templates and fold expressions?

Save to ▾
Show answer
A variadic template accepts any number of template arguments using a parameter pack, written template <typename... Args>. Before C++17 packs were processed recursively. Fold expressions from C++17 apply an operator across the whole pack in one line, for example return (args + ...); sums all arguments. They power std::make_unique, std::tuple, std::format and logging functions that take arbitrary arguments.
23

What are lvalues and rvalues?

Save to ▾
Show answer
An lvalue has identity and a persistent address, such as a named variable x. An rvalue is a temporary without lasting identity, such as 5 or the result of a + b. C++11 splits these further into lvalue, xvalue and prvalue. The distinction matters because rvalue references, written T&&, bind to temporaries and enable move semantics, letting the program steal resources instead of making expensive copies.
24

Implement a minimal unique_ptr-like class.

Save to ▾
Show answer
template <typename T> class UniquePtr { T* ptr_ = nullptr; public: explicit UniquePtr(T* p = nullptr) : ptr_(p) {} ~UniquePtr() { delete ptr_; } UniquePtr(const UniquePtr&) = delete; UniquePtr& operator=(const UniquePtr&) = delete; UniquePtr(UniquePtr&& o) noexcept : ptr_(o.ptr_) { o.ptr_ = nullptr; } UniquePtr& operator=(UniquePtr&& o) noexcept { if (this != &o) { delete ptr_; ptr_ = o.ptr_; o.ptr_ = nullptr; } return *this; } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } T* get() const { return ptr_; } };
25

Use perf to find the hotspots in a C++ program.

Save to ▾
Show answer
# Build optimised but with symbols and frame pointers g++ -O2 -g -fno-omit-frame-pointer app.cpp -o app perf stat ./app # cycles, instructions, cache misses perf record -g ./app # sample with call graphs perf report # browse hottest functions # Flame graph (with FlameGraph scripts on PATH) perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg perf stat -e cache-misses,branch-misses ./app # specific counters
26

Your program gives correct output in a debug build but wrong output in a release build. How do you investigate?

Save to ▾
Show answer
This usually means undefined behaviour or an uninitialised variable that the optimiser exploits. I would compile with -Wall -Wextra and fix every warning, then run tests with -fsanitize=address,undefined to catch out-of-bounds access, overflow and use-after-free. ThreadSanitizer checks for data races. Building with -O2 -g lets me debug the optimised code, and bisecting functions or adding assertions narrows down the faulty spot.
27

Code that takes bool& from a std::vector<bool> fails to compile. Why?

Save to ▾
Show answer
std::vector<bool> is a special case that packs bits, so elements are not real bool objects and operator[] returns a proxy object, not bool&. You cannot take a reference or pointer to a single element, and it is not thread-safe to write neighbouring elements from different threads. If you need real bools use std::vector<char>, std::deque<bool>, or std::bitset when the size is fixed.
28

What are type traits and how does if constexpr use them?

Save to ▾
Show answer
Type traits in <type_traits> answer questions about types at compile time, such as std::is_integral_v<T>, std::is_pointer_v<T> or std::is_same_v<T, U>, and transform types, such as std::remove_reference_t. if constexpr picks a branch at compile time and discards the other, so code like if constexpr (std::is_integral_v<T>) can call integer-only operations safely inside a generic function.
29

What is a custom deleter for a smart pointer and when do you need one?

Save to ▾
Show answer
A custom deleter tells the smart pointer how to release a resource that was not created with plain new. For example std::unique_ptr<FILE, decltype(&fclose)> f(fopen(path, mode), &fclose); closes the file automatically. Deleters are used for C library handles, sockets, OS handles and objects from custom pools. For unique_ptr the deleter is part of the type, while shared_ptr stores it in the control block.
30

Implement a simple String class that follows the rule of five.

Save to ▾
Show answer
#include <cstring> #include <utility> class String { char* data_ = nullptr; std::size_t len_ = 0; public: String(const char* s = "") : len_(std::strlen(s)) { data_ = new char[len_ + 1]; std::memcpy(data_, s, len_ + 1); } ~String() { delete[] data_; } String(const String& o) : String(o.data_) {} String(String&& o) noexcept : data_(std::exchange(o.data_, nullptr)), len_(std::exchange(o.len_, 0)) {} String& operator=(String o) noexcept { // copy-and-swap handles both std::swap(data_, o.data_); std::swap(len_, o.len_); return *this; } const char* c_str() const { return data_ ? data_ : ""; } };
31

Write a move constructor and move assignment operator for a class that owns a buffer.

Save to ▾
Show answer
#include <utility> class Buffer { std::size_t size_ = 0; int* data_ = nullptr; public: explicit Buffer(std::size_t n) : size_(n), data_(new int[n]{}) {} ~Buffer() { delete[] data_; } Buffer(Buffer&& other) noexcept : size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {} Buffer& operator=(Buffer&& other) noexcept { if (this != &other) { delete[] data_; size_ = std::exchange(other.size_, 0); data_ = std::exchange(other.data_, nullptr); } return *this; } };
32

What is std::atomic and what do memory orderings mean?

Save to ▾
Show answer
std::atomic<T> makes reads, writes and read-modify-write operations such as fetch_add indivisible and race-free without a mutex. Memory order controls how other memory operations are ordered around it. seq_cst, the default, gives one global order and is easiest to reason about. acquire and release pair up to publish data from one thread to another. relaxed only guarantees atomicity, suitable for simple counters. Weaker orders are faster but harder to get right.
33
Save to ▾
Show answer
Substitution Failure Is Not An Error: when the compiler substitutes template arguments and the resulting declaration is invalid, that candidate is silently removed from overload resolution instead of causing an error. It was traditionally used with std::enable_if to enable functions only for certain types, such as integral types. In C++20, concepts and requires clauses replace most SFINAE tricks with clearer syntax and error messages.
34

A program does millions of lookups in a std::map with string keys and is slow. How would you speed it up?

Save to ▾
Show answer
If ordering is not needed, switch to std::unordered_map and call reserve with the expected size to avoid rehashing. For string keys, use heterogeneous lookup with string_view to avoid building temporary strings. If keys are fixed after loading, a sorted vector with binary search is very cache friendly. I would profile before and after, because hashing long strings has its own cost.
35

A production service's memory usage grows steadily over several days. How do you find the cause?

Save to ▾
Show answer
First confirm it is a real leak and not a cache or allocator fragmentation by tracking RSS and allocator statistics. Then reproduce in staging with a heap profiler such as heaptrack or jemalloc profiling and compare snapshots to see which call stacks keep growing. LeakSanitizer catches lost blocks. Common culprits are unbounded maps, shared_ptr cycles and listeners never removed. After the fix, add a long soak test.
36

A low-latency system needs a queue between two threads with no locks. How would you approach it?

Save to ▾
Show answer
For one producer and one consumer, a single-producer single-consumer ring buffer is the standard answer: a fixed-size array with atomic head and tail indices, using release on publish and acquire on read, and padding so the indices sit on different cache lines. It avoids allocation and syscalls. For multiple producers I would use a proven library rather than writing my own, and verify with stress tests and TSan.
37

Two third-party libraries both define a global function called init and your build fails with duplicate symbols. How do you fix it?

Save to ▾
Show answer
The clean fix is to place each library's code in its own namespace so calls become liba::init and libb::init. If the source cannot change, I would check whether one function is only used internally and give it internal linkage, or wrap one library behind a shared library that hides its symbols using visibility settings. I avoid macro renaming hacks because they break silently on upgrades.
38

How do virtual functions work internally?

Save to ▾
Show answer
Most compilers give each polymorphic class a vtable, a static table of function pointers for its virtual functions. Every object stores a hidden vptr pointing to its class's vtable. A call through a base pointer loads the vptr, looks up the slot and jumps to the right override. The cost is one pointer per object, an indirect call and usually no inlining, which matters only in very hot loops.
39

Show the copy-and-swap idiom and explain why it gives strong exception safety.

Save to ▾
Show answer
class Buffer { std::size_t n_; int* p_; public: explicit Buffer(std::size_t n) : n_(n), p_(new int[n]{}) {} Buffer(const Buffer& o) : n_(o.n_), p_(new int[o.n_]) { std::copy(o.p_, o.p_ + n_, p_); } ~Buffer() { delete[] p_; } friend void swap(Buffer& a, Buffer& b) noexcept { std::swap(a.n_, b.n_); std::swap(a.p_, b.p_); } Buffer& operator=(Buffer other) noexcept { // copy made before we touch *this swap(*this, other); return *this; } }; // If the copy throws, *this is unchanged; self-assignment is safe too.
40

A low-latency service shows occasional latency spikes of several milliseconds. How would you investigate?

Save to ▾
Show answer
I would first capture latency percentiles and correlate spikes with events. Common causes are heap allocation in the hot path, page faults, lock contention, logging that blocks on I/O, CPU frequency changes and the OS scheduling other work on the same core. Tools include perf, off-CPU flame graphs and tracing. Typical fixes are preallocating memory, pinning threads to isolated cores, async logging and avoiding syscalls in hot paths.

Frequently asked questions

What is asked in C++ interviews for 10+ years of experience?

At ten years and beyond, candidates are evaluated as principal engineers, architects or engineering managers. Discussions cover technology strategy, long-term evolution of large C++ codebases, hiring and growing teams, and aligning engineering with business goals. Expect deep architecture discussions, stakeholder management scenarios and questions on how you influence standards across multiple teams.

How many questions should I practise before a C++ interview?

Work through all 40 questions here, then the company-wise sets for the employers you are targeting. Quality matters more than count: be able to explain why each answer is right.

Which topics matter most for 10+ years of experience?

Focus first on Technical strategy, Large-scale architecture, Organisational influence, Hiring and team growth.