Skip to content

02_Modern_CPP — Interview questions and model answers

38 questions, ordered from basic to senior. Answer out loud, then compare. A good answer is short, precise, and ends with a consequence ("so in production you...") when there is one. The module that covers each question is in brackets.

Levels: Q1 to Q10 basic, Q11 to Q24 intermediate, Q25 to Q38 senior.


Basic

Q1. You are a C programmer. What are the most important differences when you move to C++? [01]

C++ adds constructors and destructors, which make RAII possible: cleanup is tied to scope instead of to every error path. It adds references, overloading (and therefore name mangling), namespaces, templates, and a stricter type system (no implicit void* to T* conversion). The standard library gives you containers, strings and smart pointers so you rarely manage memory by hand. The compile and link model, the stack, the heap and the ABI concepts are the same, so C debugging skills carry over directly.

Q2. What is the difference between a pointer and a reference? [01]

A reference is an alias that must be bound to an object at initialisation and cannot be re-seated or null. A pointer is an object holding an address; it can be null, reassigned, and used in arithmetic. Under the hood a reference is usually implemented as a pointer. Use a reference for "must exist, will not change what it refers to" parameters, a pointer (or std::optional<std::reference_wrapper>) when "nothing" is a valid value.

Q3. What is name mangling and why does extern "C" exist? [01]

Because C++ allows overloading, the compiler encodes the parameter types (and namespace) into the symbol name, for example _Z7depositR7Accountx. C has no overloading and uses the plain name. extern "C" tells the C++ compiler to use C linkage (no mangling) so a C++ program can call a C library, or expose functions to C or to dlsym. Wrap C headers in #ifdef __cplusplus extern "C" {.

Q4. In what order are members and bases constructed and destroyed? [02]

Virtual bases first, then direct bases in declaration order, then members in declaration order, then the constructor body. The order in the member-initialiser list does not matter; GCC warns with -Wreorder if it differs. Destruction is the exact reverse. If a constructor throws, only the already-constructed bases and members are destroyed, and the object's own destructor does not run.

Q5. What does explicit do on a constructor? [02]

It forbids the constructor from being used for implicit conversions and copy-initialisation, so Temperature t = 36.6; and passing 36.6 to a Temperature parameter both fail to compile, while Temperature t{36.6}; works. Single-argument constructors should be explicit by default; it stops silent conversions such as an int turning into a buffer of that size.

Q6. What is RAII? [03]

Resource Acquisition Is Initialisation: a resource (memory, file, socket, lock, transaction) is acquired in a constructor and released in the destructor of an object with automatic lifetime. Because destructors run on every exit from a scope, including return, break and exceptions, cleanup cannot be forgotten. It replaces the C pattern of goto cleanup and makes exception-safe code possible. std::lock_guard, std::unique_ptr and std::fstream are all RAII types.

Q7. What are the special member functions, and what are the rules of zero, three and five? [02, 04]

Default constructor, destructor, copy constructor, copy assignment, move constructor, move assignment. Rule of five: if you write any of destructor, copy or move operations, you probably need to think about all five, because you are managing a resource by hand. Rule of three is the pre-C++11 version. Rule of zero: prefer composing classes from members that already manage themselves (std::string, std::vector, std::unique_ptr) so you write none of them and the compiler-generated ones are correct.

Q8. What does const on a member function mean? [01, 11]

It means the function does not modify the observable state of the object; inside it, this is a pointer to const. You can call only const member functions on a const object or through a const&. mutable members (for example a cache or a mutex) are exempt. In production, const-correctness is also a thread-safety signal: the standard library assumes const operations are safe to call concurrently.

Q9. std::vector vs std::list: which do you default to and why? [06]

std::vector, almost always. It is contiguous, so iteration is cache-friendly and the hardware prefetcher helps, while every std::list node is a separate heap allocation reached by a pointer chase. Even middle insertions are often faster on a vector for small-to-medium sizes because moving contiguous memory is cheap. Use list only when you need stable iterators across insertions and erasures or O(1) splice, such as an LRU cache.

Q10. What is a lambda, and what does it compile into? [07]

A lambda is an unnamed function object. The compiler generates a unique class whose data members are the captures and whose operator() is the body. Capture by value copies into members; capture by reference stores references, which dangle if the lambda outlives the scope. Because each lambda has its own type, passing it to a template lets the compiler inline the call, unlike a function pointer.

Intermediate

Q11. What are lvalues and rvalues, and what is an rvalue reference? [04]

An lvalue has an identity you can take the address of (a named variable); an rvalue is a temporary or a value about to expire. T&& is a reference that binds only to rvalues, which lets an overload know "nobody else will look at this object again, so I may steal its resources". Precisely, C++17 has lvalues, xvalues and prvalues; xvalues (such as the result of std::move(x)) and prvalues together are rvalues.

Q12. What does std::move actually do? [04]

Nothing at run time. It is a static_cast<T&&> that turns an lvalue into an xvalue so that overload resolution picks the move constructor or move assignment. The move itself is done by those functions. If the type has no move constructor, the copy constructor is chosen instead, silently. And std::move on a const object yields const T&&, which binds to the copy constructor, so it copies.

Q13. A function takes std::string&& s and does v.push_back(s);. What is wrong? [04]

s has a name, so the expression s is an lvalue even though its type is an rvalue reference. push_back(const std::string&) is chosen and the string is copied. Write v.push_back(std::move(s));. In templates with forwarding references (T&& where T is deduced), use std::forward<T>(s) instead so lvalue arguments are still copied.

Q14. What state is a moved-from object in? [04]

For standard library types, "valid but unspecified": you may destroy it, assign to it, and call functions without preconditions such as clear() or size(), but you must not assume its value. For your own types you choose the guarantee; for resource handles the convention is to leave the source empty (fd = -1, pointer = null) so its destructor does nothing. Using a moved-from object's value is a logic bug, and clang-tidy's bugprone-use-after-move catches many cases.

Q15. What is copy elision, and what changed in C++17? [04]

Copy elision lets the compiler construct a returned object directly in the caller's storage, so no copy or move happens. Since C++17 it is mandatory for prvalues: T x = T{}; and return T{}; construct in place, and the type need not even be copyable or movable. NRVO (returning a named local) is still optional but done by GCC and Clang in practice. Writing return std::move(local); defeats NRVO and is a pessimisation.

Q16. How should a function take a std::string parameter? [04, 13]

Read-only and not stored: std::string_view (or const std::string& if you need a null-terminated c_str()). Stored into a member ("sink"): take std::string by value and std::move it into place; callers passing rvalues pay one move, lvalues pay one copy plus one move. The const& plus && overload pair saves that one move but doubles the code; use it only on hot paths.

Q17. When do you use unique_ptr, shared_ptr, weak_ptr, or a raw pointer? [05]

unique_ptr for single ownership; it is the default, and costs the same as a raw pointer with a stateless deleter. shared_ptr only when there are genuinely several owners whose lifetimes you cannot order. weak_ptr for observers that must not keep the object alive and to break cycles. A raw pointer or reference for non-owning access when the owner provably outlives the user. In code review, every shared_ptr should be able to name its second owner.

Q18. How do you wrap a C API handle such as FILE* or sqlite3* in a smart pointer? [05]

Use std::unique_ptr<T, Deleter> with a custom deleter, ideally a stateless struct: struct FileCloser { void operator()(FILE* f) const noexcept { std::fclose(f); } }; and using FilePtr = std::unique_ptr<FILE, FileCloser>;. A stateless deleter adds no size thanks to the empty-base optimisation; a function-pointer deleter doubles the size. For an int file descriptor, which is not a pointer, write a small move-only class instead.

Q19. What does vector::push_back cost, and when does it invalidate iterators? [06]

Amortised O(1). When size() == capacity(), the vector allocates a larger buffer (libstdc++ doubles; MSVC grows by 1.5x), moves or copies every element across, and frees the old buffer. That invalidates every iterator, pointer and reference into the vector. Without reallocation only end() is invalidated. If you know the size, reserve() first; and never hold a pointer into a vector across a push_back unless capacity is guaranteed.

Q20. Why does std::vector copy your elements during reallocation instead of moving them? [04, 06]

Because your move constructor is not noexcept. vector gives the strong exception guarantee on push_back: if reallocation fails half-way, the original contents must be intact. Moving destructively and then throwing would lose data, so it uses std::move_if_noexcept, which copies when the move might throw and a copy exists. Mark move operations noexcept (they almost never need to throw) and check with static_assert(std::is_nothrow_move_constructible_v<T>).

Q21. Iterator invalidation rules for map and unordered_map? [06]

std::map is a node-based red-black tree: insertion invalidates nothing, erase invalidates only iterators to the erased element. std::unordered_map is a bucket array of node chains: erase invalidates only the erased element, insertion invalidates iterators (not pointers or references to elements) if it triggers a rehash. Use reserve() to prevent rehashes, and use the iterator returned by erase when erasing inside a loop.

Q22. std::map vs std::unordered_map: how do you choose? [06]

map gives ordered iteration, range queries (lower_bound) and guaranteed O(log n), but each lookup chases about log2(n) pointers through separate nodes. unordered_map gives average O(1) with a single hash and usually one or two cache misses, but no ordering, worst-case O(n) with a bad hash, and rehash spikes. For read-mostly data that is built once, a sorted vector with binary search often beats both because it is contiguous.

Q23. Why must templates usually be defined in headers? [08]

A template is not code; it is a recipe. The compiler generates code only when it sees the template used with concrete types, and it needs the full definition at that point in that translation unit. If the definition lives in a .cpp, other translation units cannot instantiate it and you get undefined-symbol link errors. The exception is explicit instantiation (template class Foo<int>;) paired with extern template declarations, which also cuts build time.

Q24. What is SFINAE, and what does if constexpr replace? [08, 13]

Substitution Failure Is Not An Error: if substituting template arguments into a declaration produces invalid code, that overload is silently removed from the candidate set rather than causing an error. It is used with std::enable_if and std::void_t to select overloads by type properties. C++17's if constexpr discards the untaken branch at compile time inside one function, which replaces most SFINAE overload sets with readable code. C++20 concepts replace most of the rest.

Senior

Q25. Describe the memory layout of an object with virtual functions, and how a virtual call compiles. [09]

Each polymorphic object carries a hidden vptr, typically at offset 0 under the Itanium ABI used by GCC and Clang, pointing into its class's vtable in .rodata. The vtable holds offset-to-top, a typeinfo pointer, then one function pointer per virtual function in declaration order, with derived overrides replacing base entries. A call p->area() compiles to: load the vptr from *p, load the slot at a fixed offset, indirect call. The constructor sets the vptr, which is why a virtual call inside a base constructor dispatches to the base version.

Q26. Why does a polymorphic base class need a virtual destructor? [09]

delete base_ptr; on an object of derived type, through a base with a non-virtual destructor, is undefined behaviour. In practice only the base destructor runs, so the derived class's members (strings, buffers, sockets) leak, and the deleting destructor may free with the wrong size. With a virtual destructor, the vtable's deleting-destructor slot dispatches to the most-derived destructor. Rule: public and virtual, or protected and non-virtual. shared_ptr created with make_shared<Derived> captures the right deleter, but do not rely on that.

Q27. What is object slicing? [09]

Copying a derived object into a base object by value copies only the base subobject; the derived members are dropped and the vptr is the base's, so virtual calls go to base versions. It happens with Base b = derived;, by-value parameters, and std::vector<Base>. Prevent it by passing polymorphic types by reference or pointer, storing std::vector<std::unique_ptr<Base>>, and deleting or protecting the base's copy operations.

Q28. What does final buy you beyond documentation? [09]

It lets the compiler devirtualise. When the static type is a final class, or a final virtual function, the dynamic type is known exactly, so the indirect call becomes a direct call that can be inlined. That removes the vtable load and the indirect branch, and, more importantly, enables inlining and further optimisation of the caller. GCC's -Wsuggest-final-types points to candidates.

Q29. What is in a shared_ptr control block, and what does make_shared change? [05]

A shared_ptr is two pointers: one to the object, one to a control block holding the strong count, the weak count, the deleter and the allocator (type-erased). Counts are atomic, so copying a shared_ptr across threads costs a locked instruction on a shared cache line. shared_ptr<T>(new T) makes two allocations; make_shared<T>() makes one block containing both, which is faster and more cache-friendly. The catch: with make_shared the object's memory cannot be freed until the last weak_ptr is gone, because it shares the allocation with the control block (the object is still destroyed when the strong count hits zero).

Q30. Is shared_ptr thread-safe? [05]

The control block is: different threads may copy and destroy different shared_ptr instances that share one object, and the count stays correct. The pointee is not protected at all; concurrent access to the object needs its own synchronisation. And one shared_ptr instance is not safe to read and write from two threads at once; for that you need std::atomic_load/atomic_store on it in C++17 or std::atomic<std::shared_ptr<T>> in C++20, or a mutex.

Q31. How do you create a reference cycle with shared_ptr, and how do you fix it? [05]

A parent holding shared_ptr to children while each child holds shared_ptr to its parent: when the last external owner goes, every strong count is still at least 1, so nothing is freed. Valgrind reports the blocks as definitely and indirectly lost. Fix it by making the back edge a weak_ptr (lock it to use it) or a raw pointer when the parent provably outlives the children. The design rule is that ownership must form a tree or a DAG, never a cycle.

Q32. What are the exception-safety guarantees? [10]

No-throw: the operation never fails (destructors, swaps, moves, noexcept functions). Strong: if it throws, state is exactly as before, like a transaction. Basic: if it throws, no resources leak and all invariants hold, but the state may have changed. No guarantee: anything can happen. The usual route to the strong guarantee is "do all throwing work on a copy, then commit with a non-throwing swap", which is copy-and-swap. Destructors must not throw; since C++11 they are noexcept by default and a throw calls std::terminate.

Q33. What happens at run time when an exception is thrown, and what does it cost? [03, 10]

GCC and Clang use the zero-cost table-based model: the normal path has no checks, and per-function unwind tables plus .gcc_except_table describe what to clean up. A throw allocates the exception object (__cxa_allocate_exception), then the unwinder walks the stack twice, first to find a handler, then to run landing pads that call destructors, and finally enters the catch. That costs on the order of microseconds per throw (typical), so exceptions suit rare failures, not control flow or per-request expected errors like "key not found".

Q34. How do you choose between exceptions, std::optional, std::error_code and an expected type? [10]

Exceptions for rare, cannot-continue failures far from where they can be handled (out of memory, corrupt configuration at start-up); they keep the happy path clean. std::optional when absence is normal and needs no explanation (find). std::error_code, or an expected-style variant<T, Error>, when failure is a normal outcome the caller must handle and you need the reason, especially on hot paths, across a C ABI, or in code built with -fno-exceptions. In a server, never let an exception escape a thread function: catch at the request boundary, log, and return an error response.

Q35. Name the main categories of undefined behaviour, and why the optimiser makes them dangerous. [12]

Memory: out-of-bounds access, use-after-free, double free, null dereference, uninitialised reads. Arithmetic: signed overflow, shift by negative or by at least the bit width, division by zero. Type: strict-aliasing violations, calling through the wrong function type, misaligned access. Lifetime and concurrency: dangling references, data races, modifying a const object. The compiler assumes UB never happens and optimises on that assumption, so if (x + 1 < x) is deleted and a null check after a dereference is removed. The bug therefore appears only at -O2 and far from its cause; catch it with -fsanitize=address,undefined and TSan.

Q36. What is padding, and how does layout affect performance? [12]

Each type has an alignment; the compiler inserts padding so every member is aligned and the struct size is a multiple of its largest alignment. {char; double; char;} is 24 bytes, reordered {double; char; char;} is 16. Smaller structs mean more objects per 64-byte cache line and fewer misses. False sharing is the opposite trap: two threads writing different variables on the same line make the line bounce between cores; separate hot per-thread data with alignas(64).

Q37. Which C++17 features do you actually use in production, and what is the trap in string_view? [13]

Structured bindings (for (auto& [k, v] : map)), if (auto it = m.find(k); it != m.end()), std::optional, std::variant with std::visit, std::string_view, std::filesystem, std::shared_mutex for read-mostly data, [[nodiscard]], if constexpr, fold expressions, inline variables and CTAD. The string_view trap is lifetime: it does not own its characters, so returning a view of a local string, or storing a view of a temporary, leaves it dangling. It is also not null-terminated, so never pass sv.data() to a C API that expects a C string.

Q38. Design the ownership for a TCP server: listener, connections, worker pool, shared store. [05, 14]

The Server owns the listening socket as a move-only SocketFd by value, the thread pool by value, and the store by value, so destruction order is declared order reversed: declare the store before the pool so workers are joined before the store dies. Each accepted connection is a move-only Connection holding a SocketFd, moved into the task that serves it, so exactly one owner closes each fd. Workers get the store by reference; the store guards its map with a std::shared_mutex (shared_lock for reads, unique_lock for writes). shared_ptr appears only if a connection genuinely has two owners, such as async I/O plus a registry, with the registry holding weak_ptr. This is exactly the tcpkv design in 10_Capstone_Projects/tcpkv.