Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C++ for Interviews

Prerequisites

  • Basic C++ (variables, loops, functions, classes, pointers)
  • Familiarity with at least one STL container
  • Understanding of compilation and linking basics

Interview Frequency

★★★★★ — Every C++ interview. Even “algorithm” interviews in C++ implicitly test language knowledge through your use of templates, iterators, lambdas, and containers.

Companies

Google, Meta, Amazon, Microsoft, Apple, Bloomberg, Goldman Sachs, Jane Street, Citadel, Two Sigma, Uber, Lyft, Airbnb, Stripe, Dropbox — any company with a C++ codebase or that accepts C++ in interviews.


Overview

This chapter covers the C++ features and idioms that matter most in interviews. Not everything in the language — just what you’ll actually use or be asked about.

TopicInterview Relevance
TemplatesCode reuse, type genericity
ReferencesEfficient parameter passing
Move SemanticsPerformance optimization
constexprCompile-time computation
Lambda ExpressionsConcise callbacks, STL algorithms
FunctorsCustom comparators, stateful callbacks
Iterator CategoriesChoosing the right algorithm
Allocator BasicsHow STL manages memory
Exception SafetyRobust code design
STL InternalsHow vector/map/unordered_map work
Container SelectionChoosing the right tool
Undefined BehaviorAvoiding traps
Common STL PitfallsIterator invalidation, comparator bugs
Memory ComplexitySpace analysis
Thread SafetyConcurrent access rules

1. Templates

Function Templates

#include <iostream>
#include <string>

template<typename T>
T max_val(T a, T b) {
    return (a > b) ? a : b;
}

// Explicit specialization for const char*
template<>
const char* max_val<const char*>(const char* a, const char* b) {
    return (std::strcmp(a, b) > 0) ? a : b;
}

int main() {
    std::cout << max_val(3, 7) << "\n";           // 7 — deduced as int
    std::cout << max_val(3.14, 2.71) << "\n";     // 3.14 — deduced as double
    std::cout << max_val<std::string>("a", "b") << "\n"; // explicit
}

Class Templates

#include <vector>
#include <stdexcept>

template<typename T, int N>
class FixedStack {
    T data[N];
    int top_ = 0;
public:
    void push(const T& val) {
        if (top_ >= N) throw std::overflow_error("Stack full");
        data[top_++] = val;
    }
    T pop() {
        if (top_ <= 0) throw std::underflow_error("Stack empty");
        return data[--top_];
    }
    const T& top() const {
        if (top_ <= 0) throw std::underflow_error("Stack empty");
        return data[top_ - 1];
    }
    bool empty() const { return top_ == 0; }
    int size() const { return top_; }
};

// Usage: FixedStack<int, 100> s;

SFINAE Basics

Substitution Failure Is Not An Error: If template argument substitution fails, the compiler silently removes that overload instead of erroring.

#include <type_traits>
#include <iostream>

// Only enabled for integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
safe_div(T a, T b) {
    if (b == 0) throw std::domain_error("Division by zero");
    return a / b;
}

// Only enabled for floating-point types
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
safe_div(T a, T b) {
    return a / b; // floating point division by zero gives inf, not UB
}

int main() {
    std::cout << safe_div(10, 3) << "\n";    // 3 — integral overload
    std::cout << safe_div(10.0, 3.0) << "\n"; // 3.333... — floating-point overload
}

Interview Application

Q: “What’s the difference between template<typename T> and template<class T>?”

Nothing — they’re interchangeable. Convention: typename for templates, class when you want to emphasize the type must be a class (though it doesn’t enforce it).

Q: “When does template instantiation happen?”

At the point of use (implicit instantiation) or when explicitly requested. This is why template definitions are usually in header files — the compiler needs the full definition to instantiate.


2. References

Lvalue References

void modify(int& x) { x = 42; }       // Must bind to an lvalue
void read(const int& x) { /* ... */ } // Can bind to rvalues too

int main() {
    int a = 10;
    modify(a);   // OK: a is an lvalue
    // modify(5); // ERROR: 5 is an rvalue, can't bind to non-const lvalue ref
    read(5);     // OK: const lvalue ref extends lifetime of temporary
}

Rvalue References

void process(std::string&& s) {
    // s is an rvalue reference — we can steal its resources
    std::string result = std::move(s);
    // ...
}

int main() {
    std::string hello = "hello";
    // process(hello);           // ERROR: hello is an lvalue
    process(std::move(hello));   // OK: cast to rvalue
    process("temporary");        // OK: string literal creates temporary
}

When to Pass What

Parameter TypeWhen to Use
T (by value)Small types (int, char, pointer), or when you need a copy anyway
const T&Read-only access, don’t need to own, don’t want copy cost
T&Mutable reference, parameter must be modified
T&&Taking ownership, perfect forwarding
const T&&Rarely used; generally avoid

3. Move Semantics and Perfect Forwarding

Perfect Forwarding

#include <utility>
#include <string>
#include <iostream>

class Wrapper {
    std::string data;
public:
    // Perfect forwarding constructor
    template<typename T>
    Wrapper(T&& val) : data(std::forward<T>(val)) {}

    // std::forward preserves the value category:
    // - If called with lvalue, T = std::string&, forward returns lvalue ref
    // - If called with rvalue, T = std::string,  forward returns rvalue ref
};

int main() {
    std::string s = "hello";
    Wrapper w1(s);              // lvalue: copies s into data
    Wrapper w2(std::move(s));   // rvalue: moves s into data
    Wrapper w3("literal");      // rvalue: moves from temporary string
}

std::move vs std::forward

FunctionWhat It DoesWhen to Use
std::move(x)Unconditionally casts to rvalueWhen you want to move and don’t care about x
std::forward<T>(x)Conditionally casts based on TIn forwarding templates, preserving value category

Interview Application

Q: “What’s the difference between std::move and std::forward?”

std::move says “I’m done with this object, take its resources.” std::forward says “pass this along with the same value category it was given to me.” std::move is always an rvalue cast; std::forward is a conditional cast that depends on the template parameter.


4. constexpr

Compile-Time Computation

#include <array>
#include <iostream>

constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i)
        result *= i;
    return result;
}

constexpr bool is_prime(int n) {
    if (n < 2) return false;
    for (int i = 2; i * i <= n; ++i)
        if (n % i == 0) return false;
    return true;
}

int main() {
    constexpr int fact10 = factorial(10);  // Computed at compile time
    static_assert(fact10 == 3628800);

    // Can use in array size (requires compile-time constant)
    std::array<int, factorial(5)> arr;  // size = 120

    // Runtime usage also works
    int n;
    // std::cin >> n;
    // int f = factorial(n);  // OK: computed at runtime if n is not constexpr

    // Compile-time prime checking
    static_assert(is_prime(17));
    static_assert(!is_prime(15));

    std::cout << "10! = " << fact10 << "\n";
}

Interview Application

constexpr functions can be evaluated at compile time when given constant expressions, or at runtime when given non-constant arguments. They’re regular functions with an extra superpower.

Q: “Can constexpr functions have loops?”

Yes, since C++14. They can also have local variables, conditionals, and multiple return statements. They just can’t have static variables, new/delete, or virtual functions (in C++14; C++20 relaxes some restrictions).


5. Lambda Expressions

Capture Lists

#include <algorithm>
#include <vector>
#include <iostream>
#include <string>

int main() {
    std::vector<int> v = {5, 3, 1, 4, 2};

    int threshold = 3;
    // Capture by value
    auto count_above = std::count_if(v.begin(), v.end(),
        [threshold](int x) { return x > threshold; });

    // Capture by reference
    int total = 0;
    std::for_each(v.begin(), v.end(),
        [&total](int x) { total += x; });

    // Capture all by value [=] or all by reference [&]
    int offset = 10;
    auto add_offset = [=](int x) { return x + offset; }; // offset by value

    // Mutable lambda (can modify captured-by-value)
    int counter = 0;
    auto inc = [counter]() mutable { return ++counter; };
    std::cout << inc() << "\n"; // 1
    std::cout << inc() << "\n"; // 2

    std::cout << "count_above: " << count_above << "\n"; // 2 (5 and 4)
    std::cout << "total: " << total << "\n";              // 15
}

Generic Lambdas (C++14)

#include <iostream>
#include <string>

auto add = [](auto a, auto b) { return a + b; };

int main() {
    std::cout << add(3, 4) << "\n";           // 7 (int + int)
    std::cout << add(3.14, 2.71) << "\n";     // 5.85 (double + double)
    std::cout << add(std::string("a"), std::string("b")) << "\n"; // "ab"
}

Lambdas in STL

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<std::pair<int, std::string>> students = {
        {85, "Alice"}, {92, "Bob"}, {78, "Charlie"}, {92, "David"}
    };

    // Sort by score descending, then by name ascending
    std::sort(students.begin(), students.end(),
        [](const auto& a, const auto& b) {
            if (a.first != b.first) return a.first > b.first;
            return a.second < b.second;
        });

    for (auto& [score, name] : students)
        std::cout << name << ": " << score << "\n";
    // Bob: 92
    // David: 92
    // Alice: 85
    // Charlie: 78
}

Interview Application

Lambdas are essential for concise STL usage. In interviews, prefer lambdas over functors for one-off operations. Use named lambdas for complex logic:

auto is_valid = [&](const Node& node) {
    return node.value >= 0 && visited.find(node.id) == visited.end();
};

6. Functors (Function Objects)

#include <algorithm>
#include <vector>
#include <iostream>

class MultiplyBy {
    int factor;
public:
    explicit MultiplyBy(int f) : factor(f) {}
    int operator()(int x) const { return x * factor; }
};

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::transform(v.begin(), v.end(), v.begin(), MultiplyBy(3));
    for (int x : v) std::cout << x << " "; // 3 6 9 12 15
    std::cout << "\n";
}

Functor vs Lambda

AspectFunctorLambda
StateEasy (member variables)Via capture
ReusabilityNamed class, reusableUsually one-off
InliningCompiler can inlineCompiler can inline
ReadabilityVerbose for simple opsConcise
Interview preferenceCustom comparators with stateEverything else

7. Iterator Categories

Input → Forward → Bidirectional → Random Access → Contiguous
CategoryOperationsExample Containers
Input++, *, ==istream_iterator
ForwardInput + multi-passforward_list, unordered_set
BidirectionalForward + --list, set, map
Random AccessBidirectional + +n, -n, [], <deque
ContiguousRandom Access + contiguous memoryvector, array, string

Why It Matters

// std::sort requires Random Access iterators
std::sort(v.begin(), v.end());           // OK: vector has random access
// std::sort(l.begin(), l.end());        // ERROR: list only has bidirectional
l.sort();                                 // OK: list has its own sort

// std::reverse requires Bidirectional
std::reverse(l.begin(), l.end());        // OK: list has bidirectional

8. Allocator Basics

How STL Allocates Memory

Every STL container takes an allocator as a template parameter (default: std::allocator<T>). The allocator handles:

  1. allocate(n) — request memory for n objects of type T
  2. deallocate(p, n) — return memory
  3. construct(p, args...) — call constructor at address p
  4. destroy(p) — call destructor at address p
#include <memory>
#include <vector>
#include <iostream>

int main() {
    std::allocator<int> alloc;

    // Allocate space for 5 ints
    int* p = alloc.allocate(5);

    // Construct objects
    for (int i = 0; i < 5; ++i)
        std::allocator_traits<decltype(alloc)>::construct(alloc, p + i, i * 10);

    for (int i = 0; i < 5; ++i)
        std::cout << p[i] << " "; // 0 10 20 30 40
    std::cout << "\n";

    // Destroy and deallocate
    for (int i = 0; i < 5; ++i)
        std::allocator_traits<decltype(alloc)>::destroy(alloc, p + i);
    alloc.deallocate(p, 5);
}

Interview Application

You rarely write custom allocators in interviews, but understanding them explains why vector is fast (contiguous allocation, amortized O(1) push_back) and how pmr::vector with a pool allocator can eliminate fragmentation.


9. Exception Safety

Three Guarantees

GuaranteeWhat It MeansExample
NothrowOperation never throwsDestructors, std::move on primitives
StrongIf exception thrown, state is unchangedvector::push_back (copies to new buffer, swaps)
BasicIf exception thrown, no leaks, invariants preservedMost operations

Example: Strong Exception Guarantee

#include <vector>
#include <iostream>

class Widget {
    int id;
public:
    Widget(int i) : id(i) {}
    Widget(const Widget& other) : id(other.id) {
        if (id < 0) throw std::runtime_error("Negative ID");
    }
    int get() const { return id; }
};

int main() {
    std::vector<Widget> v;
    v.push_back(Widget(1));
    v.push_back(Widget(2));

    try {
        v.push_back(Widget(-1)); // Throws during copy
    } catch (const std::exception& e) {
        std::cout << "Exception: " << e.what() << "\n";
    }

    // Strong guarantee: v still contains {1, 2}
    std::cout << "Size: " << v.size() << "\n"; // 2
}

Interview Application

Q: “What exception guarantee does vector::push_back provide?”

Strong guarantee. If the element’s copy constructor throws, the vector remains unchanged. This works because push_back copies to the new buffer first, and only commits (by swapping) if all copies succeed.


10. STL Internals

std::vector

  • Storage: Contiguous array on the heap
  • Growth: When capacity is exceeded, allocates a new buffer (typically 2×), moves/copies elements, frees old buffer
  • push_back: Amortized O(1), worst-case O(n) when reallocation needed
  • insert/erase at end: O(1). At middle: O(n) due to shifting
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v;
    std::cout << "capacity doubles:\n";
    for (int i = 0; i < 20; ++i) {
        std::cout << "size=" << v.size() << " cap=" << v.capacity() << "\n";
        v.push_back(i);
    }
}
// Typical output: capacity goes 0, 1, 2, 4, 8, 16, 32...

std::map (Red-Black Tree)

  • Structure: Balanced binary search tree (red-black)
  • Ordering: Elements sorted by key
  • Operations: O(log n) search, insert, delete
  • Memory: Each node has key, value, color, left, right, parent pointers (~48 bytes overhead per node)

std::unordered_map (Hash Table)

  • Structure: Array of buckets, each bucket is a linked list (or vector)
  • Hashing: Key → hash → bucket index
  • Operations: Average O(1), worst O(n) if all keys collide
  • Rehash: When load factor exceeds threshold (default 1.0), rehashes all elements

Comparison Table

Aspectvectormapunordered_map
OrderingInsertion orderSorted by keyNo ordering
SearchO(n)O(log n)O(1) average
Insert endO(1) amortizedN/AO(1) average
Insert middleO(n)O(log n)O(1) average
Memory overheadMinimal~48 bytes/node~48 bytes + bucket array
Cache behaviorExcellentPoor (pointer chasing)Moderate (bucket chains)
Iterator invalidationOn reallocation/insert/eraseOnly on erase of that elementOn rehash/erase

11. Container Selection Guide

Decision Tree

Need ordered data?
├── Yes → Need O(log n) search/insert/delete?
│   ├── Yes → std::map or std::set
│   └── No  → std::vector (sort once, binary search)
├── No → Need O(1) lookup?
│   ├── Yes → std::unordered_map or std::unordered_set
│   └── No → What operations?
│       ├── Push/pop front and back → std::deque
│       ├── Frequent middle insert/delete → std::list (rare in practice)
│       └── Default → std::vector
└── Need both order AND O(1) lookup?
    → Use both: unordered_map + sorted vector (or boost::bimap)

When to Use Each Container

ContainerBest ForAvoid When
vectorDefault choice, random access, cache-friendlyFrequent middle insert/delete
dequePush/pop at both endsNeed contiguous memory
listFrequent splice operationsAlmost everything else (cache-unfriendly)
forward_listMemory-critical, singly-linkedNeed backward traversal
map/setNeed sorted order, O(log n) operationsCache matters, O(1) possible
unordered_map/unordered_setO(1) lookup, no order neededNeed iteration in order
priority_queueMax/min extractionNeed arbitrary access
stack/queueLIFO/FIFO semanticsNeed iteration

12. Undefined Behavior

Common UB Traps

// 1. Signed integer overflow
int x = INT_MAX;
x += 1;  // UB!

// 2. Null pointer dereference
int* p = nullptr;
*p = 42;  // UB!

// 3. Use after free
int* p = new int(42);
delete p;
*p = 10;  // UB!

// 4. Array out of bounds
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 10;  // UB! (arr[5] is one past the end — legal to compute, not to write)

// 5. Signed/unsigned comparison bugs
unsigned int a = 0;
int b = -1;
if (a < b) {
    // This is TRUE because b is converted to unsigned!
    // (unsigned)-1 = UINT_MAX
}

// 6. Null dereference through member access
struct S { int x; };
S* p = nullptr;
int y = p->x;  // UB!

// 7. Strict aliasing violation
float f = 3.14f;
int* ip = reinterpret_cast<int*>(&f);
int i = *ip;  // UB! (strict aliasing violation)

// 8. Modifying a const object
const int x = 42;
const_cast<int&>(x) = 10;  // UB if x was originally const!

Interview Application

Q: “What’s undefined behavior? Why does it matter?”

UB means the compiler can do anything — crash, produce wrong results, or appear to work (the worst case). It matters because:

  1. Optimizations assume UB doesn’t happen
  2. Code that “works” may break with a different compiler or optimization level
  3. Security vulnerabilities often stem from UB

13. Common STL Pitfalls

Iterator Invalidation

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    // WRONG: erase invalidates iterators
    // for (auto it = v.begin(); it != v.end(); ++it)
    //     if (*it % 2 == 0) v.erase(it);  // UB!

    // CORRECT: erase returns next valid iterator
    for (auto it = v.begin(); it != v.end(); ) {
        if (*it % 2 == 0)
            it = v.erase(it);
        else
            ++it;
    }

    // Or use erase-remove idiom
    v = {1, 2, 3, 4, 5};
    v.erase(std::remove_if(v.begin(), v.end(),
            [](int x) { return x % 2 == 0; }),
            v.end());

    for (int x : v) std::cout << x << " "; // 1 3 5
    std::cout << "\n";
}

Comparator Requirements

#include <set>
#include <iostream>

// WRONG: comparator must define strict weak ordering
struct BadComp {
    bool operator()(int a, int b) const {
        return a <= b; // Not strict weak ordering! (a <= a is true)
    }
};

// CORRECT: strict weak ordering (irreflexive, antisymmetric, transitive)
struct GoodComp {
    bool operator()(int a, int b) const {
        return a < b;
    }
};

int main() {
    // std::set<int, BadComp> bad; // May cause infinite loops or crashes
    std::set<int, GoodComp> good = {3, 1, 4, 1, 5};
    for (int x : good) std::cout << x << " "; // 1 3 4 5
    std::cout << "\n";
}

std::string::npos Comparison

#include <string>
#include <iostream>

int main() {
    std::string s = "hello";
    auto pos = s.find("world");

    // WRONG: pos is size_t (unsigned), comparing with -1 is dangerous
    // if (pos == -1) { ... }

    // CORRECT:
    if (pos == std::string::npos) {
        std::cout << "Not found\n";
    }
}

14. Memory Complexity of STL

ContainerMemory OverheadElement Storage
vector<T>3 pointers (begin, end, capacity) = 24 bytesContiguous, no per-element overhead
deque<T>Array of pointers to fixed-size blocks + metadata (~80 bytes)Per-block overhead
list<T>3 pointers per node (prev, next, data) = 24 bytes + elementHeap-allocated nodes
forward_list<T>1 pointer per node (next) = 8 bytes + elementHeap-allocated nodes
map<K,V>~48 bytes per node (key, value, left, right, parent, color)Red-black tree nodes
unordered_map<K,V>~48 bytes per node + bucket array (~1 byte per bucket)Hash table with chains
set<T>~48 bytes per nodeSame as map
priority_queue<T>Container overhead (default: vector)Same as underlying container

15. Thread Safety of STL

The Rule

The standard guarantees:

  • Const member functions can be called concurrently on the same object
  • Non-const member functions on the same object are NOT thread-safe
  • Different objects can be accessed concurrently without synchronization

Common Pitfalls

// UNSAFE: concurrent modification
std::vector<int> v = {1, 2, 3};
// Thread 1: v.push_back(4);  // May trigger reallocation
// Thread 2: int x = v[0];    // May read from freed memory during reallocation

// UNSAFE: concurrent read and modification
std::map<int, int> m = {{1, 10}, {2, 20}};
// Thread 1: m[3] = 30;      // May rehash (for unordered_map)
// Thread 2: auto it = m.find(1); // Iterator may be invalidated

Safe Patterns

#include <shared_mutex>
#include <map>

template<typename K, typename V>
class ThreadSafeMap {
    mutable std::shared_mutex mtx_;
    std::map<K, V> data_;
public:
    V get(const K& key) const {
        std::shared_lock lock(mtx_);  // Multiple readers OK
        auto it = data_.find(key);
        return it != data_.end() ? it->second : V{};
    }
    void put(const K& key, const V& val) {
        std::unique_lock lock(mtx_);  // Exclusive access
        data_[key] = val;
    }
};

Design Decisions

When NOT to Use Templates

  • When the code is simple and specific to one type → just write it for that type
  • When runtime polymorphism is needed → use virtual functions
  • When compile times matter → templates increase compilation time
  • When debugging → template error messages can be cryptic

Alternatives: Virtual functions (runtime polymorphism), std::variant + std::visit (type-safe union), std::any (type-erased container).

When NOT to Use Lambdas

  • When the logic is complex and reusable → use a named class/function
  • When you need recursive lambdas → use std::function or a named function
  • When the capture list gets complicated → extract to a class with members

Trade-offs

FeatureProsCons
TemplatesZero-cost abstraction, compile-time optimizationCode bloat, long compile times, cryptic errors
Virtual functionsRuntime flexibility, clean interfacesvtable overhead, can’t inline
LambdasConcise, inline-ableCan’t be recursive easily, complex captures
ExceptionsClean error propagationPerformance cost, hard to reason about
std::variantType-safe unionVisitor pattern complexity

Summary

C++ is a large language, but interviews test a focused subset: templates for generic code, references and move semantics for efficiency, lambdas for concise STL usage, and deep knowledge of how STL containers work internally. Know the common pitfalls (iterator invalidation, UB, comparator requirements), understand the performance characteristics of each container, and be able to articulate why you’d choose one over another. The best C++ code in interviews is not the cleverest — it’s the clearest, most correct, and most appropriate for the problem.