C++ remains essential for systems programming, game engines, high-frequency trading, embedded systems, and performance-critical applications. C++ interviews at companies like Google, Jane Street, Bloomberg, and game studios test deep language knowledge: memory management, move semantics, templates, and the concurrency model. This guide covers the questions tested at senior C++ interviews.
Smart Pointers and RAII
RAII (Resource Acquisition Is Initialization): tie resource lifetime to object lifetime. When the object is destroyed (goes out of scope), the resource is released. This is the foundational C++ idiom for avoiding leaks. Smart pointers implement RAII for heap memory: (1) unique_ptr — exclusive ownership. Cannot be copied (compile error). Can be moved: auto p2 = std::move(p1). When the unique_ptr is destroyed, the owned object is deleted. Use for: owning heap objects with a single owner (the common case). Zero overhead (same size and performance as a raw pointer). (2) shared_ptr — shared ownership via reference counting. Multiple shared_ptrs can own the same object. The object is deleted when the last shared_ptr is destroyed (reference count reaches 0). Overhead: atomic reference count increment/decrement (thread-safe but not free). Use for: shared ownership across components (e.g., a shared cache). (3) weak_ptr — non-owning reference to a shared_ptr-managed object. Does not increment the reference count. Call lock() to get a shared_ptr (returns nullptr if the object was deleted). Use for: breaking circular references (A owns B, B has a weak reference to A) and caches (check if the cached object still exists). Interview question: “When would you use shared_ptr vs unique_ptr?” Answer: unique_ptr by default (single ownership, zero overhead). shared_ptr only when multiple owners genuinely need to share the object lifetime. Use weak_ptr to avoid cycles in shared_ptr graphs.
Move Semantics and Perfect Forwarding
Move semantics (C++11) enable transferring resources from one object to another without copying. Before C++11: returning a vector from a function copied the entire contents. With move: the internal buffer pointer is transferred (O(1) instead of O(N)). Rvalue references (T&&): bind to temporary objects (rvalues). The move constructor: MyClass(MyClass&& other) noexcept : data(other.data) { other.data = nullptr; }. “Steal” the resources from other and leave it in a valid but empty state. std::move: casts an lvalue to an rvalue reference, enabling move: auto v2 = std::move(v1). After the move, v1 is in a “moved-from” state (valid but unspecified — can be reassigned or destroyed). Rule of five: if you define any of: destructor, copy constructor, copy assignment, move constructor, move assignment — define all five (or = default them). Rule of zero: prefer using RAII types (smart pointers, containers) so your class needs no custom resource management. Perfect forwarding: template<typename T> void wrapper(T&& arg) { inner(std::forward<T>(arg)); }. T&& in a template is a forwarding reference (not an rvalue reference). std::forward preserves the value category (lvalue or rvalue) when passing to inner. Use for: factory functions, emplace methods, and any generic wrapper that should not change the argument semantics. Interview question: “What is the difference between std::move and std::forward?” Answer: move unconditionally casts to rvalue (always enables moving). forward conditionally preserves the original value category (lvalue stays lvalue, rvalue stays rvalue). Forward is for templates; move is for explicit transfer.
Templates and Compile-Time Programming
Templates enable generic programming: template<typename T> T max(T a, T b) { return (a > b) ? a : b; }. The compiler generates a specialized version for each type used (monomorphization). Zero runtime overhead — the specialized code is as fast as hand-written type-specific code. Class templates: template<typename T> class Stack { std::vector<T> data; … };. Variadic templates: template<typename… Args> void log(Args… args) — accepts any number of arguments. Used in: printf-style functions, tuple, and make_shared. SFINAE (Substitution Failure Is Not An Error): if template substitution fails, the template is silently removed from the overload set (not a compilation error). Used for: enabling/disabling template overloads based on type traits. Modern alternative: concepts (C++20). Concepts (C++20): constrained templates. template<typename T> requires std::integral<T> T gcd(T a, T b). The compiler gives clear errors when constraints are not met (instead of cryptic template errors). constexpr (C++11/14/17): compile-time evaluation. constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n-1); }. factorial(10) is computed at compile time. C++20 consteval: forced compile-time evaluation (error if cannot be computed at compile time). if constexpr (C++17): compile-time conditional. Branches not taken are not compiled. Interview question: "What are C++20 concepts?" Answer: type constraints for templates. Replace SFINAE with readable, declarative constraints. Provide clear error messages when a type does not meet requirements.
Virtual Functions and Polymorphism
Virtual functions enable runtime polymorphism. class Base { virtual void speak() { cout << "Base"; } }; class Derived : public Base { void speak() override { cout <speak(); // “Derived”. The vtable (virtual function table): each class with virtual functions has a vtable — an array of function pointers. Each object has a vptr pointing to its class vtable. A virtual call: dereference the vptr, index into the vtable, call the function pointer. Cost: one extra indirection per virtual call (vtable lookup). This prevents inlining — the compiler cannot inline a virtual call because the target is not known at compile time. For performance-critical code: consider CRTP (Curiously Recurring Template Pattern) for static polymorphism (compile-time dispatch, no vtable). Pure virtual functions: virtual void draw() = 0; makes the class abstract. Cannot be instantiated directly. Subclasses must implement the function. Virtual destructor: always make the base class destructor virtual when using polymorphism. Without it: delete base_ptr only calls the base destructor, leaking derived class resources. Rule: if a class has any virtual function, its destructor should be virtual. override keyword (C++11): void speak() override. Compile error if the function does not actually override a base class virtual function. Catches: typos in function names, wrong parameter types, and missing virtual in the base class. Always use override for safety.
STL Containers and Algorithms
Key containers: (1) vector — dynamic array. Contiguous memory (cache-friendly). O(1) amortized push_back. Use by default for sequences. (2) unordered_map — hash map. O(1) average lookup/insert. Use for: key-value storage, counting, and caching. (3) map — red-black tree. O(log N) lookup/insert. Sorted keys. Use when: you need ordered iteration or range queries. (4) unordered_set — hash set. O(1) membership test. (5) deque — double-ended queue. O(1) push/pop at both ends. Used by std::queue and std::stack. (6) priority_queue — max-heap. O(log N) push/pop. Use for: top-K, Dijkstra, merge K sorted. Iterators: the uniform interface for traversing containers. for (auto it = v.begin(); it != v.end(); ++it). Range-based for loop: for (auto& x : v). Algorithms: std::sort, std::find, std::transform, std::accumulate, std::any_of, std::partition. Ranges (C++20): auto result = v | views::filter(is_even) | views::transform(square) | views::take(10). Lazy, composable transformations (similar to Java Streams or Rust iterators). Interview question: “When would you use map vs unordered_map?” Answer: unordered_map for: O(1) lookups when order does not matter (the common case). map for: sorted iteration, range queries (lower_bound/upper_bound), and when keys are not hashable. unordered_map has higher constant factors (hashing) but better asymptotic performance. For small N (< 100), map may be faster due to cache locality.
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”When should you use unique_ptr vs shared_ptr in C++?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”unique_ptr by default: exclusive ownership, zero overhead (same size/performance as raw pointer), cannot be copied (prevents accidental sharing), can be moved (std::move). Use for: any heap object with a single owner (the common case). shared_ptr only when multiple owners genuinely share the object lifetime: reference-counted, thread-safe increment/decrement. Object deleted when last shared_ptr is destroyed. Use for: shared caches, objects passed to callbacks that may outlive the creator. weak_ptr for breaking cycles in shared_ptr graphs (A owns B via shared_ptr, B has weak_ptr to A — prevents memory leak from circular references) and caches (check if object still exists without preventing deletion). Rule: start with unique_ptr. Move to shared_ptr only if ownership truly needs to be shared. Use weak_ptr to break cycles and for non-owning references.”}},{“@type”:”Question”,”name”:”What is the difference between std::move and std::forward in C++?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”std::move: unconditionally casts to rvalue reference. Enables moving resources from one object to another. auto v2 = std::move(v1) transfers v1 internal buffer to v2 (O(1) instead of copying O(N)). After move, v1 is in a valid but unspecified state. Use for: explicit resource transfer when you know you are done with the source. std::forward: conditionally preserves the original value category. Used in template functions with forwarding references (T&&). If the argument was an lvalue, forward passes it as lvalue. If rvalue, passes as rvalue. Use for: generic wrappers that should not change argument semantics (factory functions, emplace methods). Key difference: move always enables moving. forward preserves what the caller intended. move is for explicit transfer; forward is for templates.”}}]}