C++

Modern C++ (C++11 and later) is a very different language from the C++ of the 1990s. Smart pointers, move semantics, lambdas, and constexpr transformed how idiomatic C++ is written. Raw new/delete is rare in modern codebases.


🟢 Junior

Types, References, and Const Correctness

C++ distinguishes values, references, and pointers at the type level.

int x = 42;
int& ref = x;     // reference — alias for x, cannot be null, cannot be reseated
int* ptr = &x;    // pointer — can be null, can point elsewhere

ref = 10;         // modifies x
*ptr = 20;        // modifies x via pointer dereference
ptr = nullptr;    // pointer can now point to nothing

const int& cref = x;  // read-only reference — common for function parameters

Pass large objects by const& to avoid copying. Pass small types (int, char, pointer) by value.

Classes and Access Control

class BankAccount {
public:
    explicit BankAccount(double initial_balance)
        : balance_(initial_balance) {}  // member initializer list

    void deposit(double amount) {
        if (amount > 0) balance_ += amount;
    }

    bool withdraw(double amount) {
        if (amount > 0 && balance_ >= amount) {
            balance_ -= amount;
            return true;
        }
        return false;
    }

    double balance() const { return balance_; }  // const method — cannot modify members

private:
    double balance_;  // trailing underscore convention for private members
};

BankAccount acc(100.0);
acc.deposit(50.0);
acc.withdraw(30.0);
acc.balance(); // 120.0

explicit prevents implicit single-argument constructor calls — always use it to avoid accidental conversions.

std::vector and Common Containers

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

std::vector<int> nums = {5, 2, 8, 1, 9};

nums.push_back(3);
nums.emplace_back(7);    // constructs in-place — avoids copy for complex types
nums.reserve(100);       // pre-allocate — avoids reallocations in a known-size loop

std::sort(nums.begin(), nums.end());
auto it = std::find(nums.begin(), nums.end(), 8);
bool found = (it != nums.end());

Other key containers: std::map<K,V> (sorted tree), std::unordered_map<K,V> (hash table), std::set<T>, std::deque<T>, std::array<T,N> (fixed-size stack array).


🟡 Medior

RAII and Smart Pointers

RAII (Resource Acquisition Is Initialization) ties a resource’s lifetime to an object’s lifetime — the destructor releases the resource. Smart pointers implement RAII for heap allocations.

#include <memory>

// unique_ptr — sole ownership, move-only
std::unique_ptr<int> p = std::make_unique<int>(42);
*p = 100;
// p goes out of scope → memory automatically freed, no delete needed

// shared_ptr — shared ownership via reference counting
std::shared_ptr<std::string> s1 = std::make_shared<std::string>("hello");
std::shared_ptr<std::string> s2 = s1;  // reference count = 2
s1.reset();  // count = 1, string still alive
// s2 goes out of scope → count = 0, string freed

// weak_ptr — non-owning observer, breaks reference cycles
std::weak_ptr<std::string> w = s1;
if (auto locked = w.lock()) {
    // locked is a shared_ptr, valid only inside this block
}

Never use raw new/delete — use make_unique and make_shared instead.

Move Semantics and std::move

Copy is expensive for large objects. Move transfers ownership of resources (heap memory, file handles) from one object to another — it is O(1) regardless of size.

std::vector<int> source(1'000'000, 0);
std::vector<int> dest = std::move(source);  // no copy — pointer swap
// source is now in a valid but unspecified state (do not use its contents)

When you write a class that manages a resource, implement the Rule of Five: destructor, copy constructor, copy assignment, move constructor, move assignment. Or use the Rule of Zero: delegate ownership to smart pointers and let the compiler generate everything.

Lambdas and std::function

Lambdas are anonymous function objects that can capture local variables.

int threshold = 5;
auto is_big = [threshold](int x) { return x > threshold; };
// [=] captures everything by value, [&] by reference, [x, &y] mixed

std::vector<int> nums = {1, 8, 3, 9, 2, 7};
nums.erase(
    std::remove_if(nums.begin(), nums.end(), is_big),
    nums.end()
);  // nums is now [1, 3, 2]

std::function<R(Args...)> is a type-erased callable — slower than a plain lambda due to heap allocation and virtual dispatch. Use it only when you need to store heterogeneous callables (e.g., a std::vector<std::function<void()>>).

Templates

Templates generate code for specific types at compile time. There is no runtime overhead.

template<typename T>
T clamp(T value, T lo, T hi) {
    return std::max(lo, std::min(value, hi));
}

clamp(15, 0, 10);   // int version
clamp(3.7, 0.0, 5.0); // double version

// Class template
template<typename T, std::size_t N>
struct Stack {
    std::array<T, N> data;
    std::size_t top = 0;

    void push(const T& v) { data[top++] = v; }
    T pop() { return data[--top]; }
};

Stack<int, 100> stack;
stack.push(42);

C++20 Concepts constrain template parameters with readable error messages:

template<std::integral T>
T factorial(T n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

🔴 Senior

Memory Layout and Cache Effects

Modern CPUs are 100–1000× faster than RAM. Cache misses are the #1 performance bottleneck in C++ code.

Struct of Arrays (SoA) is more cache-friendly than Array of Structs (AoS) when iterating over one field of many objects:

// AoS — iterating over x forces loading y and z too
struct Particle { float x, y, z, mass; };
std::vector<Particle> particles;

// SoA — iterating over x only touches x memory
struct Particles {
    std::vector<float> x, y, z, mass;
    std::size_t count;
};

Game engines and physics simulations use SoA for this reason. Profile with perf (Linux) or Intel VTune before switching — the transformation adds code complexity.

Concurrency with std::thread and Atomics

#include <thread>
#include <atomic>
#include <mutex>

std::atomic<int> counter{0};  // lock-free for simple increments

void increment_many() {
    for (int i = 0; i < 100'000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

std::thread t1(increment_many), t2(increment_many);
t1.join(); t2.join();
// counter == 200'000

// For complex critical sections, use mutex
std::mutex mu;
std::vector<int> shared_log;

void log_event(int event) {
    std::lock_guard<std::mutex> lock(mu);  // RAII — unlocks automatically
    shared_log.push_back(event);
}

std::memory_order_relaxed allows the compiler and CPU to reorder the atomic operation — use only when ordering doesn’t matter (e.g., simple counters). Use memory_order_seq_cst (the default) when you need sequential consistency.

Undefined Behavior

UB means the C++ standard makes no guarantee about what the program does. In practice, the optimizer exploits UB to produce incorrect-but-fast code.

Signed integer overflow is UB in C++. The compiler assumes it cannot happen and optimizes accordingly:

// Compiler may optimize this loop assuming i never overflows (wraps around)
for (int i = 0; i >= 0; i++) { ... }
// With UB, compiler may turn this into an infinite loop

Out-of-bounds array accessarr[i] where i >= size is UB. Sanitize with bounds checks in debug builds (-D_GLIBCXX_DEBUG, AddressSanitizer -fsanitize=address).

Use-after-free and dangling references:

int* get_local() {
    int x = 42;
    return &x;  // UB — x is destroyed when function returns
}

std::string_view sv;
{
    std::string s = "hello";
    sv = s;  // sv points into s
}
// sv is now a dangling reference — UB to use it

Always use -fsanitize=address,undefined in debug/test builds to catch these at runtime.

Senior Gotchas

The most dangerous rule: initialize all members in constructors. Uninitialized members have indeterminate values — reading them is UB, not necessarily zero.

std::vector::push_back that causes a reallocation invalidates all iterators, pointers, and references into the vector. Storing an iterator, then appending, then using the iterator is a subtle bug.

Virtual destructor: if a class is used polymorphically (base class pointer to derived object), the base class destructor must be virtual. Without it, delete base_ptr only calls the base destructor — the derived destructor is skipped and resources leak.

const_cast to remove const and then write through the resulting pointer is UB if the original object was actually const. It is only safe to use const_cast to pass a non-const pointer to a legacy API that should have taken const but doesn’t.