C++ Interview Questions
All 40 C++ interview questions are now live, covering fundamentals through performance: pointers and references, STL, templates, smart pointers, concurrency, tooling, and systems-level optimization.
C++ is a general-purpose language that builds on C with classes, templates, RAII, stronger type abstraction, and a large standard library. It still allows low-level control, but it adds tools for safer and more expressive design.
C is mostly procedural, while C++ supports procedural, object-oriented, generic, and modern value-based styles. In interviews, the key difference is not just syntax but the richer abstraction model and resource-management patterns in C++.
#include <iostream>
#include <string>
struct User {
std::string name;
void greet() const {
std::cout << "Hello, " << name << "\n";
}
};
int main() {
User user{"Amit"};
user.greet();
}
A trading system used C++ instead of C to keep low latency while using safer abstractions for ownership and container management.
A weak answer says C++ is just C with classes. That ignores templates, RAII, overloads, and the STL.
C++ is basically C plus OOP.C++ extends C-style systems programming with object, generic, and RAII-based design tools.Why is RAII considered one of the biggest practical advantages of C++ over C?
A pointer stores an address and can be null, reassigned, or used for dynamic memory and indirection. You access the pointee explicitly with * and ->.
A reference is an alias to an existing object. It must be initialized when declared, cannot be reseated, and is usually easier to reason about for function parameters because it avoids null checks by default.
#include <iostream>
void incrementByPointer(int* value) {
if (value) {
++(*value);
}
}
void incrementByReference(int& value) {
++value;
}
int main() {
int score = 10;
int* ptr = &score;
int& ref = score;
incrementByPointer(ptr);
incrementByReference(ref);
std::cout << score << "\n";
}
API code often prefers references for required inputs and pointers for optional objects or ownership-aware interfaces.
Many candidates say references are just constant pointers. They are not objects with pointer semantics.
int& r = a;
r = b; // reseats the referencer = b; // assigns through the reference to aWhen would you deliberately choose a raw pointer over a reference in an interface?
Fundamental types include boolean, character, integer, floating-point, and void. Common examples are bool, char, int, float, and double.
Type modifiers such as signed, unsigned, short, and long adjust range or representation. In interviews, emphasize that exact sizes can vary by platform, so fixed-width types like std::int32_t are often safer for portable code.
#include <cstdint>
#include <iostream>
int main() {
unsigned int retries = 3;
long long bytesProcessed = 5000000;
double price = 149.99;
std::int32_t userId = 1024;
std::cout << retries << " " << bytesProcessed << " " << price << " " << userId << "\n";
}
A network protocol parser used fixed-width integer types so message layouts stayed identical across compilers and servers.
A common mistake is assuming int is always 32-bit and long is always 64-bit.
Why are fixed-width integer types often preferred in systems and serialization code?
Built-in arrays store contiguous elements of one type and have fixed size. When passed to functions, they often decay to pointers, so size information is easily lost unless passed separately or wrapped in a safer type.
C-strings are null-terminated character arrays, which makes them fast and interoperable but error-prone. std::string manages memory, size, and operations safely, so it is usually the better default in modern C++.
#include <cstring>
#include <iostream>
#include <string>
int main() {
char legacyName[] = "Amit";
std::string modernName = "Amit Kumar";
std::cout << std::strlen(legacyName) << "\n";
std::cout << modernName.size() << "\n";
modernName += " Singh";
std::cout << modernName << "\n";
}
A file parser used C-strings only at the OS boundary, then converted immediately to std::string for safer processing.
Many candidates treat C-strings and std::string as interchangeable, but one is a raw character buffer and the other is a managed class.
What risks appear when a character array is not properly null-terminated?
Pass-by-value copies the argument, so changes inside the function affect only the local copy. It is simple and safe, and for small types it is often cheap.
Pass-by-reference aliases the original object, while pass-by-pointer passes its address explicitly. References are cleaner for required inputs, and pointers are useful when nullability, optional output, or ownership signaling is needed.
#include <iostream>
#include <string>
void renameByValue(std::string name) {
name = "Changed";
}
void renameByReference(std::string& name) {
name = "Changed";
}
void renameByPointer(std::string* name) {
if (name) {
*name = "Changed";
}
}
int main() {
std::string user = "Amit";
renameByValue(user);
renameByReference(user);
renameByPointer(&user);
std::cout << user << "\n";
}
A hot path accepted large request objects by const reference to avoid copies while keeping the call site simple.
A shallow answer says reference and pointer parameters are the same. Their contracts differ, especially around null and call-site clarity.
Why is pass-by-const-reference common for large read-only objects?
A class defines a user-defined type with data and behavior, and an object is a concrete instance of that type. Classes let you model invariants and keep related logic together.
Constructors initialize objects when they are created, and destructors run automatically when objects are destroyed. In C++, destructors are central to RAII because they release resources such as files, locks, and heap memory.
#include <fstream>
#include <string>
class AuditWriter {
public:
explicit AuditWriter(const std::string& path) : file(path) {}
~AuditWriter() {
if (file.is_open()) {
file << "closing\n";
}
}
void write(const std::string& line) {
file << line << "\n";
}
private:
std::ofstream file;
};
int main() {
AuditWriter writer("audit.log");
writer.write("login success");
}
A service wrapped file and socket handles in classes so cleanup happened automatically during normal returns and exceptions.
Many candidates describe destructors only as memory cleanup, but they manage any owned resource, not just heap allocation.
Why should resource-owning classes usually define clear copy and move behavior?
Inheritance lets a derived class reuse and extend a base class interface or implementation. Single inheritance means one direct base, while multiple inheritance means a class can derive from more than one base.
Virtual inheritance is used to solve the diamond problem by ensuring only one shared base subobject exists. In practice, inheritance should model an is-a relationship; otherwise composition is usually cleaner.
#include <iostream>
struct Device {
void powerOn() const { std::cout << "power on\n"; }
};
struct Printer : virtual Device {};
struct Scanner : virtual Device {};
struct MultiFunctionUnit : Printer, Scanner {};
int main() {
MultiFunctionUnit unit;
unit.powerOn();
}
A hardware SDK used virtual inheritance to avoid duplicate device state when combining printer and scanner capabilities.
A common mistake is using inheritance for code reuse when the relationship is not truly substitutable.
When would composition be better than inheritance in C++ design?
Const correctness means marking values, parameters, pointers, and member functions as immutable when they should not modify state. It documents intent and lets the compiler prevent accidental mutation.
It matters because it improves API clarity, enables safer reuse, and allows const objects to call only const-qualified methods. In larger codebases, this reduces bugs caused by unexpected side effects.
#include <iostream>
#include <string>
class Report {
public:
explicit Report(std::string name) : name_(std::move(name)) {}
const std::string& name() const { return name_; }
private:
std::string name_;
};
void printReport(const Report& report) {
std::cout << report.name() << "\n";
}
int main() {
Report report("Q2 Revenue");
printReport(report);
}
A shared analytics library added const member functions so read-only data objects became usable across more pipelines without defensive copying.
Candidates often think const is cosmetic. In C++, it directly affects overload resolution, usability, and mutation safety.
What is the difference between const int* and int* const?
C++ stream I/O is built around stream objects such as std::cin, std::cout, and file streams from <fstream>. Extraction with >> reads formatted input, and insertion with << writes formatted output.
File streams like std::ifstream and std::ofstream wrap file handles with the same stream interface. In interviews, mention state checking because stream operations can fail due to bad input or I/O errors.
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ofstream out("users.txt");
out << "Amit,42\n";
out.close();
std::ifstream in("users.txt");
std::string line;
if (std::getline(in, line)) {
std::cout << line << "\n";
}
}
A batch job wrote audit rows through std::ofstream and rejected malformed input by checking stream state after each parse step.
A frequent mistake is assuming stream operations always succeed and skipping state checks after reads.
What is the difference between using operator>> and std::getline?
In C++, struct and class are almost the same language feature. Both can have methods, constructors, inheritance, and access control.
The main default difference is that struct members and inheritance are public by default, while class members and inheritance are private by default. Teams often use structs for plain data and classes for richer encapsulated behavior, but that is convention, not a language restriction.
#include <string>
struct Point {
int x;
int y;
};
class Account {
public:
explicit Account(std::string owner) : owner_(std::move(owner)) {}
private:
std::string owner_;
};
int main() {
Point p{10, 20};
Account a("Amit");
}
A rendering engine used structs for simple geometry records and classes for resource-owning objects with invariants.
Many candidates incorrectly say structs cannot have methods or constructors in C++.
Why do some codebases prefer structs for passive data types?
Operator overloading lets user-defined types define natural syntax for operations such as comparison, addition, indexing, or streaming. It makes custom types feel like built-in types when the semantics are intuitive.
Good overloads preserve expected meaning and avoid surprises. In interviews, mention that overloaded operators are just functions with special names and should not be abused for unrelated behavior.
#include <iostream>
class Money {
public:
explicit Money(int cents) : cents_(cents) {}
Money operator+(const Money& other) const {
return Money(cents_ + other.cents_);
}
friend std::ostream& operator<<(std::ostream& os, const Money& money) {
return os << money.cents_ << " cents";
}
private:
int cents_;
};
int main() {
Money subtotal(500), tax(90);
std::cout << (subtotal + tax) << "\n";
}
A pricing library overloaded arithmetic and comparison operators so monetary calculations stayed readable and type-safe.
A bad overload changes the expected meaning of an operator, which makes code harder to trust and review.
Which operators are commonly overloaded well, and which ones are usually risky?
Polymorphism lets code call behavior through a base interface while the derived implementation runs at runtime. In C++, runtime polymorphism is typically achieved with virtual functions.
Compilers usually implement virtual dispatch with a hidden virtual table pointer in polymorphic objects, though the exact layout is ABI-specific. The key interview point is dynamic dispatch cost versus the design flexibility it provides.
#include <iostream>
#include <memory>
#include <vector>
class PaymentMethod {
public:
virtual ~PaymentMethod() = default;
virtual void pay(double amount) const = 0;
};
class CardPayment : public PaymentMethod {
public:
void pay(double amount) const override {
std::cout << "Charged card: " << amount << "\n";
}
};
int main() {
std::vector<std::unique_ptr<PaymentMethod>> methods;
methods.push_back(std::make_unique<CardPayment>());
methods[0]->pay(499.0);
}
A plugin system used virtual interfaces so new handlers shipped independently without changing dispatch code.
Some candidates confuse function overloading with polymorphism. Overloading is compile-time; virtual dispatch is runtime.
When would you prefer static polymorphism over virtual dispatch?
An abstract class is a class that cannot be instantiated directly because it contains at least one pure virtual function. A pure virtual function is declared with = 0 and defines a required interface for derived classes.
Abstract classes are useful when you want a shared base type plus some common implementation, while still forcing concrete subclasses to provide specific behavior.
#include <iostream>
#include <memory>
class Exporter {
public:
virtual ~Exporter() = default;
virtual void exportData() const = 0;
};
class CsvExporter : public Exporter {
public:
void exportData() const override {
std::cout << "exporting csv\n";
}
};
int main() {
std::unique_ptr<Exporter> exporter = std::make_unique<CsvExporter>();
exporter->exportData();
}
A reporting platform exposed an abstract exporter interface so teams could add CSV, JSON, and PDF outputs independently.
A common mistake is saying an abstract class cannot have implemented methods or data members. It can.
How is an abstract class different from a pure interface style base class?
Templates let you write generic code that the compiler instantiates for specific types. Function templates generalize algorithms, and class templates generalize data structures or wrappers.
Templates are resolved at compile time, which can produce efficient type-specific code without runtime overhead. The tradeoff is heavier compile-time complexity and sometimes harder error messages.
#include <iostream>
#include <vector>
template <typename T>
T maxValue(const T& a, const T& b) {
return (a < b) ? b : a;
}
template <typename T>
class Buffer {
public:
void add(const T& value) { values_.push_back(value); }
std::size_t size() const { return values_.size(); }
private:
std::vector<T> values_;
};
int main() {
Buffer<int> ids;
ids.add(10);
std::cout << maxValue(4, 9) << " " << ids.size() << "\n";
}
A core library used templates to share one container implementation across multiple domain types without virtual overhead.
Many candidates describe templates as text substitution. They are a compile-time type system feature, not just macros.
Why are template definitions usually placed in header files?
STL containers provide standard data structures with defined complexity tradeoffs. vector is a dynamic contiguous array, map and set are ordered tree-based containers, unordered_map is hash-based, and deque supports efficient insertion at both ends.
The interview goal is not memorizing names but choosing the right container based on ordering, lookup cost, iteration patterns, memory layout, and insertion behavior.
#include <deque>
#include <map>
#include <set>
#include <unordered_map>
#include <vector>
int main() {
std::vector<int> ids{1, 2, 3};
std::map<int, const char*> ordered{{2, "two"}, {1, "one"}};
std::set<int> uniqueIds{3, 1, 3};
std::unordered_map<int, const char*> fastLookup{{1, "Amit"}};
std::deque<int> tasks{10, 20, 30};
}
A matching engine kept hot sequential data in vector, user metadata in unordered_map, and ranked views in map.
A weak answer treats unordered_map as always faster than map, ignoring ordering, memory, and worst-case behavior.
Why is vector often preferred over linked structures in modern C++?
Iterators generalize pointers for navigating container elements. Different iterator categories support different operations, such as single-pass traversal, bidirectional movement, or random access.
STL algorithms work on iterator ranges rather than specific container types. That separation is powerful because one algorithm like std::find_if can work across many containers as long as the iterator requirements are met.
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{42, 87, 91, 65};
auto it = std::find_if(scores.begin(), scores.end(), [](int value) {
return value >= 90;
});
if (it != scores.end()) {
std::cout << *it << "\n";
}
}
A metrics pipeline reused the same STL algorithms across vectors and deques by coding against iterator ranges instead of concrete container types.
A common mistake is thinking iterators are always raw pointers. Some are pointer-like, but many are richer abstractions.
What breaks when you use an algorithm requiring random-access iterators on a list-like container?
Smart pointers wrap raw pointers with ownership semantics. std::unique_ptr represents exclusive ownership, std::shared_ptr represents shared reference-counted ownership, and std::weak_ptr is a non-owning observer for breaking cycles.
The main interview point is to match pointer type to ownership intent. Prefer unique_ptr by default because it is simpler, cheaper, and makes lifetime easier to reason about.
#include <iostream>
#include <memory>
class CacheEntry {
public:
explicit CacheEntry(int id) : id_(id) {}
int id() const { return id_; }
private:
int id_;
};
int main() {
auto owner = std::make_unique<CacheEntry>(101);
auto shared = std::make_shared<CacheEntry>(202);
std::weak_ptr<CacheEntry> observer = shared;
std::cout << owner->id() << " " << shared->id() << "\n";
}
A service replaced scattered raw owning pointers with unique_ptr and removed a class of shutdown leaks during error handling.
Many candidates overuse shared_ptr. Shared ownership should be deliberate, not the default.
How can shared_ptr cycles cause leaks, and how does weak_ptr help?
C++ exceptions signal failure by throwing an object that unwinds the stack until a matching catch block is found. During unwinding, destructors run, which is why RAII integrates well with exception safety.
noexcept declares that a function is not expected to throw. It matters for optimization and for standard-library behavior, especially move operations, but violating it usually terminates the program.
#include <iostream>
#include <stdexcept>
#include <string>
int parsePort(const std::string& text) {
int port = std::stoi(text);
if (port <= 0) {
throw std::invalid_argument("port must be positive");
}
return port;
}
int main() {
try {
std::cout << parsePort("8080") << "\n";
} catch (const std::exception& ex) {
std::cout << ex.what() << "\n";
}
}
Configuration loading used exceptions to abort startup on invalid settings while RAII cleaned partially constructed resources automatically.
A shallow answer says exceptions are just like return codes but cleaner. The key difference is stack unwinding and exception-safety guarantees.
What is the difference between basic, strong, and no-throw exception safety?
Namespaces group related declarations and prevent name collisions across large codebases or libraries. They are especially important in C++ because templates, utilities, and domain types often share generic names.
You can qualify names explicitly with ::, bring selected names into scope with using, or open nested namespaces. In production code, broad using namespace directives are usually avoided in headers.
#include <iostream>
#include <string>
namespace billing {
struct Invoice {
std::string id;
};
void print(const Invoice& invoice) {
std::cout << invoice.id << "\n";
}
}
int main() {
billing::Invoice invoice{"INV-42"};
billing::print(invoice);
}
A monorepo used namespaces to separate billing, auth, and reporting models that all had types like Request and Result.
A common mistake is putting using namespace std; in headers, which pollutes every includer.
Why is using namespace in a header considered risky?
Lambdas create unnamed function objects inline. They are useful for short local behavior such as predicates, callbacks, and custom transformations.
The capture list controls what outside variables the lambda can access and whether they are captured by value or reference. In interviews, explain captures clearly because correctness often depends on object lifetime and mutation semantics.
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices{120, 80, 300};
int threshold = 100;
auto expensiveCount = std::count_if(prices.begin(), prices.end(), [threshold](int price) {
return price > threshold;
});
std::cout << expensiveCount << "\n";
}
A UI layer used lambdas for event callbacks without creating dozens of one-off function objects.
Candidates often ignore capture lifetime and accidentally return lambdas that reference dead local variables.
What is the difference between capturing by value and capturing by reference?
Move semantics let C++ transfer resources from temporary or expiring objects instead of copying them. This is enabled by rvalue references, written as T&&, and move constructors or move assignment operators.
std::move does not move by itself; it casts an object to an rvalue so move-aware operations can run. This is valuable for resource-owning types like strings, vectors, and file wrappers because it avoids unnecessary deep copies.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::string payload = "large response body";
std::vector<std::string> queue;
queue.push_back(std::move(payload));
std::cout << "queue size: " << queue.size() << "\n";
std::cout << "payload after move: " << payload << "\n";
}
A messaging service cut allocation churn by moving large buffers between stages instead of copying them repeatedly.
A common mistake is saying std::move physically moves data. It only enables a move operation if one exists.
What state guarantees exist for a moved-from standard library object?
Perfect forwarding preserves the value category of a function argument when passing it onward, so lvalues stay lvalues and rvalues stay rvalues. This matters in generic wrapper code that should not accidentally force copies or moves.
std::forward is used with forwarding references in templates to preserve the original category. The typical use case is factory helpers like emplace or wrapper APIs that construct objects in place.
#include <iostream>
#include <string>
#include <utility>
struct User {
explicit User(std::string name) : name(std::move(name)) {}
std::string name;
};
template <typename... Args>
User makeUser(Args&&... args) {
return User(std::forward<Args>(args)...);
}
int main() {
auto user = makeUser("Amit");
std::cout << user.name << "\n";
}
A framework used perfect forwarding in factory helpers so constructor arguments flowed through without extra temporary objects.
Many candidates use std::move inside forwarding wrappers, which wrongly turns lvalues into rvalues.
What is a forwarding reference, and when does T&& become one?
RAII means tying resource lifetime to object lifetime: acquire resources in constructors and release them in destructors. This makes cleanup deterministic and exception-safe.
The Rule of 0 says types should usually rely on compiler-generated special members if they do not own resources directly. The Rules of 3 and 5 say that if you define one ownership-sensitive special member, you often need the related copy and move operations as well.
#include <mutex>
class ScopedLock {
public:
explicit ScopedLock(std::mutex& m) : mutex_(m) { mutex_.lock(); }
~ScopedLock() { mutex_.unlock(); }
ScopedLock(const ScopedLock&) = delete;
ScopedLock& operator=(const ScopedLock&) = delete;
private:
std::mutex& mutex_;
};
int main() {
std::mutex m;
ScopedLock lock(m);
}
A storage client used RAII wrappers for sockets and locks, which removed many early-return cleanup bugs.
A common mistake is manually pairing allocate and free calls across many code paths instead of encapsulating ownership in a type.
When would you need to implement the Rule of 5 instead of relying on the Rule of 0?
Template metaprogramming uses templates to compute types, values, or behavior at compile time. Older styles relied on recursive template instantiation and type traits, while modern C++ often mixes templates with constexpr and concepts for better clarity.
Its value is generating specialized code and enforcing rules before runtime. The tradeoff is more complex compile-time logic, so it should be used when it materially improves performance, expressiveness, or correctness.
#include <iostream>
#include <type_traits>
template <typename T>
struct IsPointer {
static constexpr bool value = false;
};
template <typename T>
struct IsPointer<T*> {
static constexpr bool value = true;
};
int main() {
std::cout << IsPointer<int>::value << "\n";
std::cout << IsPointer<int*>::value << "\n";
}
A serialization library chose code paths at compile time based on whether a type was trivially copyable or pointer-like.
A weak answer treats metaprogramming as only obscure recursion tricks, ignoring modern traits, constexpr, and constrained templates.
How has modern constexpr reduced the need for older TMP patterns?
SFINAE means substitution failure is not an error: when template substitution fails in certain contexts, that overload is simply removed from consideration. It was widely used to constrain templates before the language had clearer constraint syntax.
C++20 concepts provide readable, first-class constraints on template parameters. They make intent clearer, improve diagnostics, and are usually easier to maintain than heavy SFINAE-based enable_if patterns.
#include <concepts>
#include <iostream>
template <std::integral T>
T doubleValue(T value) {
return value * 2;
}
int main() {
std::cout << doubleValue(21) << "\n";
}
A generic math library migrated from enable_if-heavy code to concepts and made template errors far easier for downstream teams to understand.
Many candidates can define SFINAE but cannot explain the practical benefit: controlling which templates participate in overload resolution.
Why do concepts usually produce better compiler diagnostics than SFINAE?
Variadic templates let a template accept an arbitrary number of template or function arguments. They are the type-safe replacement for old C-style variadic functions in many generic APIs.
Fold expressions, introduced in C++17, simplify operating over parameter packs. They remove a lot of recursive boilerplate and make code like summing or combining arguments much clearer.
#include <iostream>
template <typename... Args>
auto sum(Args... args) {
return (args + ...);
}
int main() {
std::cout << sum(10, 20, 30, 40) << "\n";
}
A logging utility used variadic templates to accept typed fields without unsafe printf-style formatting.
A common mistake is confusing variadic templates with C varargs, which lack type safety and compile-time checking.
What is the difference between a left fold and a right fold?
constexpr allows values and functions to be evaluated at compile time when given constant inputs, while still remaining usable at runtime in many cases. It is a major tool for zero-overhead abstractions and precomputed logic.
consteval is stricter: it requires compile-time evaluation. Use it when a result must exist during compilation, such as validated generated constants or checked metaprogramming helpers.
#include <array>
#include <iostream>
constexpr int square(int value) {
return value * value;
}
consteval int tableSize() {
return 8;
}
int main() {
std::array<int, tableSize()> values{};
std::cout << square(12) << " " << values.size() << "\n";
}
A protocol library computed lookup tables at compile time to reduce startup work and eliminate runtime branching.
Many candidates say constexpr always means compile-time execution. It only does so when the context and inputs permit it.
When would consteval be better than constexpr in a public API?
C++20 coroutines let a function suspend and resume without blocking a thread. The compiler transforms the function into a state machine that stores its local state across suspension points such as co_await, co_yield, or co_return.
The language provides the transformation model, but real usefulness comes from libraries that define awaitable types and schedulers. In interviews, focus on suspension semantics, not on assuming coroutines automatically create threads.
#include <coroutine>
#include <iostream>
struct SimpleTask {
struct promise_type {
SimpleTask get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
SimpleTask runTask() {
std::cout << "before suspend point\n";
co_return;
}
int main() {
runTask();
}
An async gateway used coroutines to express request pipelines sequentially while still integrating with an event loop.
A common mistake is saying a coroutine runs on another thread by default. Scheduling is library-defined, not built into the keyword itself.
How is a coroutine different from a callback chain or a thread?
The C++ memory model defines how threads interact through memory and what counts as a data race. If two threads access the same object concurrently and at least one write is not synchronized, the behavior is undefined.
std::atomic provides lock-free or lock-based atomic operations with explicit memory ordering semantics such as relaxed, acquire, and release. The main interview point is that atomicity alone is not enough; visibility and ordering matter too.
#include <atomic>
#include <thread>
int main() {
std::atomic<bool> ready{false};
int data = 0;
std::thread producer([&] {
data = 42;
ready.store(true, std::memory_order_release);
});
std::thread consumer([&] {
while (!ready.load(std::memory_order_acquire)) {}
int value = data;
(void)value;
});
producer.join();
consumer.join();
}
A low-latency feed handler used acquire-release atomics to publish parsed message batches without coarse locking.
Many candidates think volatile is a threading primitive in C++. It is not a replacement for atomic synchronization.
When is memory_order_relaxed correct, and what does it not guarantee?
Lock-free data structures guarantee that at least one thread makes progress even under contention, usually through atomic operations like compare-and-swap. They can reduce blocking and avoid some scheduler-induced latency spikes.
They are hard to design correctly because of ABA issues, memory reclamation, ordering constraints, and debugging complexity. You should use them only when profiling shows contention is a real bottleneck and simpler synchronization is insufficient.
#include <atomic>
struct Node {
int value;
Node* next;
};
int main() {
std::atomic<Node*> head{nullptr};
Node node{42, nullptr};
Node* expected = nullptr;
head.compare_exchange_strong(expected, &node);
}
A telemetry ingest path used a lock-free queue only after mutex contention showed up clearly under burst traffic.
A bad answer assumes lock-free automatically means faster. Under low contention, the added complexity may not pay off.
What memory reclamation problem appears in lock-free linked structures?
CRTP uses a derived type as a template parameter to a base class, enabling static polymorphism and compile-time reuse. Pimpl hides implementation details behind an opaque pointer to reduce compile dependencies and stabilize interfaces.
Type erasure hides concrete types behind a uniform runtime wrapper, as seen in tools like std::function. These patterns solve different problems: CRTP for zero-overhead polymorphism, Pimpl for build and ABI hygiene, and type erasure for flexible runtime abstraction.
#include <iostream>
template <typename Derived>
class Printable {
public:
void print() const {
static_cast<const Derived*>(this)->printImpl();
}
};
class Invoice : public Printable<Invoice> {
public:
void printImpl() const {
std::cout << "invoice\n";
}
};
int main() {
Invoice invoice;
invoice.print();
}
A public SDK used Pimpl to hide private members and reduce rebuilds across teams consuming the library.
Many candidates list the patterns without explaining what concrete engineering problem each one solves.
Why does Pimpl often help ABI stability and incremental build times?
CMake is a build-system generator. You describe targets, sources, include paths, compile features, and dependencies in CMakeLists.txt, and CMake generates native build files such as Ninja or Visual Studio projects.
Modern CMake is target-based: you attach properties and dependencies to targets rather than using many global flags. In interviews, that target-centric model is more important than memorizing every command.
cmake_minimum_required(VERSION 3.20)
project(BillingApp LANGUAGES CXX)
add_executable(billing main.cpp)
target_compile_features(billing PRIVATE cxx_std_20)
target_include_directories(billing PRIVATE include)
A cross-platform product used one CMake configuration to generate Linux Ninja builds and Windows Visual Studio projects from the same source tree.
A common mistake is treating CMake like a Makefile replacement rather than a generator with its own target model.
Why is target_link_libraries preferred over manually appending global linker flags?
ABI compatibility means separately compiled binaries agree on details such as symbol names, calling conventions, object layout, exception handling, and standard-library expectations. It matters whenever compiled components must link or load together across versions.
Changing member layout, virtual functions, template exposure, compiler settings, or standard library versions can break ABI even when source code still compiles. That is why library maintainers often hide implementation details and version public interfaces carefully.
class ApiClient {
public:
void send();
private:
int timeoutMs;
};
A shared enterprise SDK had to preserve ABI so older plugins continued loading after the core application updated.
Many candidates assume recompiling is always an option. ABI matters precisely when independent binaries are versioned or distributed separately.
How does the Pimpl pattern help reduce ABI breakage risk?
Effective unit tests isolate one behavior, make assertions on observable outcomes, and run quickly enough to be part of normal development. Frameworks like GTest and Catch2 mainly provide structure, fixtures, assertions, and reporting.
Strong tests cover success paths, edge cases, and failure behavior without over-coupling to implementation details. In interviews, emphasize deterministic inputs, small scope, and meaningful naming over framework trivia.
#include <gtest/gtest.h>
int discount(int price, int percent) {
return price - (price * percent / 100);
}
TEST(DiscountTest, AppliesPercentageCorrectly) {
EXPECT_EQ(discount(200, 10), 180);
EXPECT_EQ(discount(500, 0), 500);
}
A pricing library caught a rounding regression in CI because narrow unit tests covered the contract before integration tests ran.
A weak test asserts internal steps instead of stable outputs, which makes refactoring harder without improving confidence.
When should you use mocks, and when do they make tests too brittle?
Static analysis tools inspect code for likely bugs, style issues, lifetime problems, and unsafe constructs without relying only on runtime failures. Tools like clang-tidy and cppcheck are useful for catching patterns that reviews may miss.
Sanitizers such as ASan, UBSan, and TSan instrument binaries to detect memory errors, undefined behavior, or data races at runtime. The best practice is to use both static and dynamic analysis in CI because they catch different classes of defects.
# Example build flags
-fsanitize=address,undefined -fno-omit-frame-pointer
# Example clang-tidy
clang-tidy src/order.cpp --checks=modernize-*,bugprone-*
A backend team found a use-after-free in staging with ASan that code review had missed for months.
A common mistake is enabling tools once and ignoring the output. They only help when warnings are triaged and integrated into normal workflows.
What kinds of bugs will AddressSanitizer catch that unit tests may not reveal reliably?
Cache-friendly code arranges data and access patterns so the CPU can fetch and reuse nearby memory efficiently. Data-oriented design focuses on how data is laid out and traversed rather than only on object hierarchy.
In practice, contiguous storage, tight loops, smaller working sets, and structure-of-arrays layouts often outperform pointer-heavy object graphs. Modern C++ performance work is frequently limited more by memory access than by arithmetic.
#include <vector>
struct ParticleData {
std::vector<float> x;
std::vector<float> y;
std::vector<float> velocity;
};
void update(ParticleData& data, float dt) {
for (std::size_t i = 0; i < data.x.size(); ++i) {
data.x[i] += data.velocity[i] * dt;
data.y[i] += data.velocity[i] * dt;
}
}
A simulation engine sped up updates by replacing scattered heap objects with contiguous arrays for hot fields.
Many candidates optimize instructions first and ignore cache misses, even though memory stalls often dominate runtime.
Why can a vector of structs be slower than a structure of arrays for some workloads?
Modern CPUs predict branch outcomes to keep instruction pipelines full. When the prediction is wrong, the pipeline is flushed, which can make unpredictable branches expensive in tight loops.
Branch-friendly code reduces unpredictable control flow by keeping common paths dominant, simplifying conditions, batching rare cases, or using data layouts that improve predictability. The goal is not to remove every branch but to reduce costly mispredictions in hot code.
#include <vector>
int countValid(const std::vector<int>& values) {
int count = 0;
for (int value : values) {
if (value >= 0) {
++count;
}
}
return count;
}
A parser improved throughput after separating rare malformed records from the common fast path to reduce branch misses.
A common mistake is trying to make code branchless everywhere without measuring whether misprediction is actually the bottleneck.
What kind of input pattern usually makes branch prediction perform poorly?
SIMD intrinsics expose CPU vector instructions that operate on multiple data elements in parallel, such as processing four or eight integers in one instruction group. They are architecture-specific wrappers around low-level vector operations.
You should use them only in proven hotspots with regular data patterns and a clear fallback or abstraction strategy. They can deliver major speedups, but they reduce portability and increase maintenance cost.
#include <immintrin.h>
int main() {
__m128 a = _mm_set_ps(1.f, 2.f, 3.f, 4.f);
__m128 b = _mm_set_ps(5.f, 6.f, 7.f, 8.f);
__m128 c = _mm_add_ps(a, b);
(void)c;
}
An image-processing path used SIMD intrinsics only in its pixel blend kernel after profiling showed it dominated frame time.
Many candidates jump to intrinsics before checking whether the compiler already auto-vectorizes the loop well enough.
What tradeoffs do intrinsics introduce compared with relying on compiler auto-vectorization?
Profiling means measuring where time, memory, cache misses, allocations, or stalls are actually spent before optimizing. Tools such as Valgrind, perf, and VTune show different views of runtime behavior, from function hotspots to microarchitectural bottlenecks.
The right workflow is to reproduce a representative workload, capture measurements, optimize the dominant issue, and re-measure. Good performance work is evidence-driven, not intuition-driven.
# Linux perf
perf record ./pricing_service
perf report
# Valgrind callgrind
valgrind --tool=callgrind ./pricing_service
A service team discovered that a suspected math bottleneck was actually allocator overhead after sampling the real production workload.
A common mistake is timing debug builds or synthetic microbenchmarks that do not match the production execution path.
Why is representative workload selection critical when profiling?
Custom allocators such as pools and arenas specialize memory allocation for known object lifetimes or fixed-size patterns. They can reduce fragmentation, improve locality, and lower allocator overhead in allocation-heavy systems.
They help when profiling shows general-purpose allocation is a bottleneck or when many objects share simple lifetime rules. They are not free: they add complexity, can waste memory if sized poorly, and must integrate cleanly with ownership rules.
#include <cstddef>
#include <vector>
class Arena {
public:
explicit Arena(std::size_t size) : buffer_(size), offset_(0) {}
void* allocate(std::size_t bytes) {
void* ptr = buffer_.data() + offset_;
offset_ += bytes;
return ptr;
}
private:
std::vector<std::byte> buffer_;
std::size_t offset_;
};
int main() {
Arena arena(1024);
void* block = arena.allocate(128);
(void)block;
}
A parser used an arena for per-request temporary objects and freed the whole batch at once after each request completed.
Many candidates suggest custom allocators too early. They should come after profiling, not before it.
Why are arena allocators especially effective for batch-style request lifetimes?
Frequently Asked Questions
This page contains the full 40-question C++ track across all 5 levels: basic, intermediate, advanced, experienced, and performance.
The live set covers language fundamentals, STL, templates, smart pointers, RAII, modern C++ features, concurrency, build tooling, testing, and performance tuning topics.
They lean toward modern C++ interview expectations, including RAII, smart pointers, move semantics, concepts, and practical performance trade-offs.
Yes. The experienced and performance sections cover the memory model, atomics, lock-free trade-offs, cache behavior, SIMD, profiling, and allocators.
Strong answers connect syntax to ownership, lifetime, abstraction cost, and real trade-offs rather than just repeating language definitions.
Yes. The roadmap includes STL containers, iterators, templates, template metaprogramming, SFINAE, concepts, and variadic templates.
Yes. The set is broad enough for general backend, platform, low-latency, and systems-oriented C++ interviews.
Because ownership, lifetime, and resource safety are central to writing correct C++ in production. Weak answers there usually signal deeper design problems.