Skip to content

Senior C++ Engineer / Tech Lead Interview Questions & Answers

40 questions for Senior C++ Engineer / Tech Lead interviews — 1 easy, 18 medium, 21 hard.

About the Senior C++ Engineer / Tech Lead role

A senior C++ engineer leads the design of major components, sets coding standards, mentors juniors and makes decisions on architecture, modernisation and performance. They review complex changes, investigate production incidents and balance delivery with code quality. Interviews include system design, deep C++ trade-offs such as templates versus virtual functions, concurrency design and behavioural questions about leadership. Typically seven or more years of experience.

System design Mentoring Architecture decisions Concurrency design Incident handling
Solved for this roleStart 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

Why are templates usually defined in header files?

Save to ▾
Show answer
A template is only a pattern; code is generated when it is instantiated with concrete types, and the compiler must see the full definition at that point. If the body lives in a .cpp file, other translation units cannot instantiate it and you get linker errors. The usual fix is defining templates in headers, or using explicit instantiation in the .cpp for a known, fixed set of types.
02

What does C++20 std::jthread add over std::thread?

Save to ▾
Show answer
std::jthread joins automatically in its destructor, so forgetting join no longer calls std::terminate. It also supports cooperative cancellation: the thread function can take a std::stop_token, check stop_requested() in its loop, and the owner calls request_stop(), which the destructor also does before joining. std::stop_callback lets you register cleanup actions. This makes long-running worker threads safer to write.
03

Use a C++20 ranges pipeline to get squares of the first three even numbers.

Save to ▾
Show answer
#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8}; auto result = v | std::views::filter([](int x) { return x % 2 == 0; }) | std::views::transform([](int x) { return x * x; }) | std::views::take(3); for (int x : result) std::cout << x << ' '; // 4 16 36 } // Views are lazy: elements are processed only while iterating.
04

Your application is slow and you are asked to optimise it. Where do you start?

Save to ▾
Show answer
I start by measuring, not guessing. I define a target, such as p99 under 50 ms, create a repeatable benchmark, and profile with perf to find where time actually goes. Then I fix the biggest hotspot first, usually algorithmic complexity, unnecessary copies, allocations or I/O, and re-measure after each change. I keep correctness tests running and stop when the target is met.
05
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.
06

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(); } };
07

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.
08

How does std::condition_variable work and what are spurious wakeups?

Save to ▾
Show answer
A condition variable lets a thread sleep until another thread signals that some state changed. The waiter holds a unique_lock and calls cv.wait(lock, predicate), which releases the mutex while sleeping and reacquires it on wake. The notifier changes the state under the mutex and calls notify_one or notify_all. Threads can wake without notification, called spurious wakeups, so always wait with a predicate that rechecks the condition.
09

A C++ server crashes with a segmentation fault in production. How do you debug it?

Save to ▾
Show answer
Make sure core dumps are enabled with ulimit -c unlimited or systemd-coredump, and keep debug symbols for release builds. Load the core with gdb binary core, run bt to see the stack trace, then inspect frames and variables. If the stack looks corrupted, reproduce in staging with AddressSanitizer. I would also check logs around the crash time and recent deployments, then add a regression test with the fix.
10

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.
11

A teammate complains that a template function produces hundreds of lines of compiler errors when misused. How do you improve this?

Save to ▾
Show answer
Constrain the template so misuse fails early with a clear message. With C++20 I would add a concept or requires clause, for example template <std::totally_ordered T>, so the error names the unmet requirement at the call site. On older standards, a static_assert with a readable message at the top of the function body helps. Good naming and a comment on expected type properties also help.
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

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.
14

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.
15

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.
16

What is the difference between full and partial template specialization?

Save to ▾
Show answer
Full specialization provides a separate implementation for one exact set of template arguments, such as template <> class Hash<std::string>. Partial specialization, allowed only for class and variable templates, covers a family of arguments, such as template <typename T> class Hash<T*> for all pointer types. Function templates cannot be partially specialised; overloading is used instead.
17

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
18

Implement your own is_pointer type trait.

Save to ▾
Show answer
template <typename T> struct IsPointer { static constexpr bool value = false; }; template <typename T> struct IsPointer<T*> { // partial specialization for pointers static constexpr bool value = true; }; template <typename T> inline constexpr bool IsPointer_v = IsPointer<T>::value; static_assert(IsPointer_v<int*>); static_assert(!IsPointer_v<int>); // The real std::is_pointer also strips const and volatile first.
19

How do you decide between templates and virtual functions for polymorphism?

Save to ▾
Show answer
Templates give static polymorphism: resolved at compile time, inlinable and fast, but types must be known at compile time and code size grows. Virtual functions give dynamic polymorphism: one compiled body, types can be chosen at run time, plugins are possible, at the cost of an indirect call. I use templates for performance-critical generic algorithms and virtual interfaces for runtime-configurable components.
20

Explain move semantics and what std::move actually does.

Save to ▾
Show answer
Move semantics let an object transfer its resources, such as a heap buffer, to another object instead of copying them. A move constructor takes T&& and usually steals the pointer and leaves the source empty but valid. std::move does not move anything; it is just a cast to an rvalue reference that allows a move overload to be chosen. After moving, only assign to or destroy the source.
21

What does noexcept mean and why should move constructors be noexcept?

Save to ▾
Show answer
noexcept promises a function will not throw; if it does, std::terminate is called. Containers rely on this: when std::vector reallocates, it moves elements only if the move constructor is noexcept, otherwise it copies them to keep the strong exception guarantee. So a missing noexcept on a move constructor can silently make vector growth much slower. Destructors are noexcept by default.
22

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.
23

Compute a factorial at compile time.

Save to ▾
Show answer
constexpr unsigned long long factorial(unsigned n) { unsigned long long r = 1; for (unsigned i = 2; i <= n; ++i) r *= i; return r; } static_assert(factorial(5) == 120); constexpr auto f10 = factorial(10); // computed by the compiler // Pre-C++14 style used template recursion: // template <unsigned N> struct Fact { static const unsigned long long v = N * Fact<N - 1>::v; }; // template <> struct Fact<0> { static const unsigned long long v = 1; };
24

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.
25

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.
26

An asynchronous callback crashes intermittently. It is a lambda that captures local variables by reference. Explain the problem and fix.

Save to ▾
Show answer
The lambda runs later on another thread or event loop, after the function that created it has returned, so the captured references point to destroyed stack variables. Fix it by capturing by value, moving owned data into the lambda with init captures, or capturing a shared_ptr or weak_ptr for objects whose lifetime must be extended or checked. Avoid default [&] for any callback that can outlive the scope.
27

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.
28

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.
29

Write a GoogleTest unit test for a function, including an edge case.

Save to ▾
Show answer
#include <gtest/gtest.h> #include <stdexcept> #include <vector> double average(const std::vector<int>& v) { if (v.empty()) throw std::invalid_argument("empty"); long long s = 0; for (int x : v) s += x; return static_cast<double>(s) / v.size(); } TEST(AverageTest, ComputesMean) { EXPECT_DOUBLE_EQ(average({2, 4, 6}), 4.0); } TEST(AverageTest, ThrowsOnEmpty) { EXPECT_THROW(average({}), std::invalid_argument); }
30

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.
31

You parallelised a loop across eight threads but it became slower than the single-threaded version. What could be wrong?

Save to ▾
Show answer
Likely causes are lock contention when all threads share one mutex, false sharing when per-thread counters sit on the same 64-byte cache line, too little work per task so thread creation dominates, or memory bandwidth limits. I would profile with perf, pad per-thread data with alignas(64) or std::hardware_destructive_interference_size, use thread-local accumulation and a thread pool, and make chunks larger.
32

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.
33

What is a deadlock and how do you prevent it?

Save to ▾
Show answer
A deadlock happens when threads wait forever for each other's locks, for example thread A holds m1 and waits for m2 while thread B holds m2 and waits for m1. It needs mutual exclusion, hold and wait, no preemption and circular wait. Prevent it by always locking in a fixed global order, locking several mutexes together with std::scoped_lock, keeping critical sections small and never calling unknown code while holding a lock.
34

A service occasionally freezes and thread dumps show two threads each waiting on a mutex held by the other. How do you fix it?

Save to ▾
Show answer
This is a lock-order deadlock. I would find the two code paths, for example transfer locking account A then B while another locks B then A. The fix is to lock both together with std::scoped_lock(a.m, b.m), or to define a global ordering such as by account ID and always lock lower first. I would also shrink critical sections and add a stress test, and consider TSan or lock hierarchy checks.
35

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(); } };
36

What does the C++20 ranges library add?

Save to ▾
Show answer
Ranges let algorithms take whole containers, as in std::ranges::sort(v), instead of begin and end pairs. Views such as filter, transform, take and reverse are lazy and compose with the pipe operator, for example v | std::views::filter(isEven) | std::views::transform(square). Nothing is computed until you iterate. Ranges also use concepts, so errors are clearer, and projections let you sort by a member easily.
37

Write a deadlock-free money transfer between two bank accounts.

Save to ▾
Show answer
#include <mutex> #include <stdexcept> struct Account { std::mutex m; long long balance = 0; }; void transfer(Account& from, Account& to, long long amount) { if (&from == &to) return; std::scoped_lock lock(from.m, to.m); // locks both without deadlock if (from.balance < amount) throw std::runtime_error("insufficient funds"); from.balance -= amount; to.balance += amount; }
38

Implement a simple fixed-size thread pool.

Save to ▾
Show answer
#include <condition_variable> #include <functional> #include <mutex> #include <queue> #include <thread> #include <vector> class ThreadPool { std::vector<std::thread> workers_; std::queue<std::function<void()>> tasks_; std::mutex m_; std::condition_variable cv_; bool stop_ = false; public: explicit ThreadPool(std::size_t n) { for (std::size_t i = 0; i < n; ++i) workers_.emplace_back([this] { while (true) { std::function<void()> task; { std::unique_lock<std::mutex> lk(m_); cv_.wait(lk, [this] { return stop_ || !tasks_.empty(); }); if (stop_ && tasks_.empty()) return; task = std::move(tasks_.front()); tasks_.pop(); } task(); } }); } void submit(std::function<void()> f) { { std::lock_guard<std::mutex> lk(m_); tasks_.push(std::move(f)); } cv_.notify_one(); } ~ThreadPool() { { std::lock_guard<std::mutex> lk(m_); stop_ = true; } cv_.notify_all(); for (auto& w : workers_) w.join(); } };
39

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.
40

What is std::string_view and what are its pitfalls?

Save to ▾
Show answer
string_view is a non-owning pointer and length pair that refers to existing characters, so passing it avoids copies and accepts both std::string and literals. The main pitfall is lifetime: it must not outlive the data it views, so returning a string_view to a local string or a temporary dangles. It is also not guaranteed to be null-terminated, so do not pass data() to C functions expecting C strings.

Frequently asked questions

What does a Senior C++ Engineer / Tech Lead do?

A senior C++ engineer leads the design of major components, sets coding standards, mentors juniors and makes decisions on architecture, modernisation and performance. They review complex changes, investigate production incidents and balance delivery with code quality. Interviews include system design, deep C++ trade-offs such as templates versus virtual functions, concurrency design and behavioural questions about leadership. Typically seven or more years of experience.

Which skills are tested in a Senior C++ Engineer / Tech Lead interview?

Expect questions on System design, Mentoring, Architecture decisions, Concurrency design, Incident handling. Most rounds also ask you to explain a project and solve one practical problem live.

How do I prepare for a Senior C++ Engineer / Tech Lead interview?

Revise Modern C++ (C++11 to C++23), Multithreading and Concurrency, Performance, Debugging and Build Tools, Templates and Generic Programming first using the topic sheets, practise the questions on this page aloud, then do one full mock interview to practise timing.