Skip to content

C++ Cheatsheet

General-purpose language with OOP, generics, and low-level control.

01

Getting Started

Hello World

Every C++ program begins in main(). <iostream> provides std::cout (standard output) and std::cin (standard input). The std:: prefix refers to the standard namespace; using namespace std; can avoid it but is discouraged in headers because it pollutes the global namespace.

cpp
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

Variables & auto

auto deduces the type from the initializer (C++11). Use auto when the type is obvious or verbose (iterators). const makes a value immutable; constexpr evaluates at compile time for true constants embedded in the binary.

cpp
int age = 30;
double pi = 3.14159;
char grade = 'A';
bool is_dev = true;
std::string name = "Alice";
auto x = 42;          // int
auto y = 3.14;        // double
const double TAX = 0.08;

Input & Output

std::getline reads a full line including spaces, while std::cin >> stops at whitespace. Mixing them leaves a newline in the buffer; call std::cin.ignore() between getline and >> to discard it.

cpp
#include <iostream>
#include <string>

int main() {
    std::string name;
    int age;
    std::cout << "Name: ";
    std::getline(std::cin, name);
    std::cout << "Age: ";
    std::cin >> age;
    std::cout << "Hi " << name << ", " << age << "\n";
}

References

References are aliases that must be initialized and cannot be reseated to another object. Pass-by-reference avoids copying and allows modifying the caller's variable. Use const T& for read-only parameters to avoid expensive copies.

cpp
int x = 10;
int &ref = x;   // alias for x
ref = 20;
std::cout << x;   // 20

void increment(int &n) { n++; }
int a = 5;
increment(a);   // a is now 6

Type Conversion

Prefer static_cast over C-style casts because it is explicit and checked at compile time, making intent clear. std::stoi, std::stod convert strings to numbers; std::to_string does the reverse. Watch for std::out_of_range on bad input.

cpp
double d = 3.99;
int i = (int)d;               // C-style: 3
int j = static_cast<int>(d);  // C++: 3

int n = 65;
char c = static_cast<char>(n);  // 'A'

// string <-> number
int num = std::stoi("42");
std::string s = std::to_string(3.14);
02

Strings

std::string Basics

std::string manages its own memory and grows as needed. Unlike C char arrays, you don't manually manage length. .find() returns std::string::npos (a huge value) when the substring is not found, so always compare against npos.

cpp
#include <string>
std::string s = "Hello";
std::cout << s.length();      // 5
std::cout << s[0];            // H
s += ", World!";
std::cout << s;               // Hello, World!
std::cout << s.substr(0, 3);  // Hel
std::cout << s.find("World"); // 7

Comparison & Search

Comparison is lexicographic (dictionary order). .find() searches forward, .rfind() searches backward. Both return std::string::npos if not found, so always compare against npos rather than treating the result as a boolean.

cpp
std::string a = "apple", b = "banana";
if (a == b) { /* equal */ }
if (a < b) { /* apple comes before banana */ }
if (a.find("pp") != std::string::npos) {
    std::cout << "found\n";
}
size_t pos = a.rfind("p");  // last occurrence

Stringstream

stringstream bridges strings and typed values, useful for building formatted strings (like a buffer) or parsing whitespace-separated tokens. It's slower than direct operations but very flexible for serialization and deserialization.

cpp
#include <sstream>
// Build a string
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string result = oss.str();

// Parse tokens
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;   // a=10, b=20, c=30

Raw Strings & Multiline

Raw string literals R"(...)" treat backslashes and quotes literally, ideal for regex patterns, Windows file paths, and JSON/XML templates. The delimiters inside the parens are arbitrary, e.g. R"x(...)x" to allow ) inside.

cpp
std::string raw = R"(C:\Users\name\file.txt)";
// No need to escape backslashes

std::string json = R"({
  "name": "Alice",
  "age": 30
})";

char Arrays vs std::string

C-style char arrays require manual size management and are error-prone (buffer overflows). Prefer std::string; use .c_str() when interfacing with C APIs that expect const char*. Note that c_str() is only valid while the string is alive and unmodified.

cpp
char cstr[] = "Hello";   // null-terminated, size 6
std::string cppstr = "Hello";

#include <cstring>
std::cout << strlen(cstr);      // 5
std::cout << cppstr.length();   // 5

std::string from_c = cstr;          // C -> C++
const char* to_c = cppstr.c_str();  // C++ -> C
03

Numbers & Math

Integer & Floating Types

Use <cstdint> fixed-width types (int32_t, int64_t) when exact size matters across platforms. The ' digit separator (C++14) improves readability of large numbers. double is the default floating type and is preferred over float for precision.

cpp
#include <cstdint>
int32_t a = 100;
int64_t big = 9'000'000'000LL;
uint8_t byte = 255;
size_t sz = sizeof(a);   // 4

float f = 1.5f;          // 4 bytes
double d = 3.14159265;   // 8 bytes (preferred)

Numeric Limits

<limits> provides type traits for numeric properties. Use these instead of hardcoded INT_MAX macros. epsilon() gives the smallest difference distinguishable by floating point, useful for comparing doubles with a tolerance.

cpp
#include <limits>
std::cout << std::numeric_limits<int>::max();    // 2147483647
std::cout << std::numeric_limits<int>::min();    // -2147483648
std::cout << std::numeric_limits<double>::infinity();
std::cout << std::numeric_limits<double>::epsilon();

Math Functions

<cmath> provides standard math functions. Integer overflow is undefined behavior in C++; use int64_t or check bounds. For financial code, remember floating point is imprecise—consider integer cents or a decimal library.

cpp
#include <cmath>
double x = 2.5;
std::pow(x, 3);     // 15.625
std::sqrt(x);       // 1.581
std::abs(-5);       // 5
std::floor(3.7);    // 3.0
std::ceil(3.2);     // 4.0
std::round(3.5);    // 4.0
std::fmod(10.5, 3); // 1.5

Random Numbers

Modern C++ uses the <random> library instead of rand(). mt19937 is a high-quality PRNG. Distributions (uniform_int, uniform_real, normal) map raw bits to the desired range without the modulo bias that plagues rand() % N.

cpp
#include <random>
std::random_device rd;
std::mt19937 gen(rd());  // Mersenne Twister engine
std::uniform_int_distribution<int> dist(1, 100);

for (int i = 0; i < 5; i++) {
    std::cout << dist(gen) << " ";
}

Integer Overflow & Casts

Signed integer overflow is undefined behavior in C++ (the compiler may optimize assuming it never happens). Always cast to a wider type before multiplying, or check bounds. Unsigned overflow wraps modulo 2^n and is well-defined.

cpp
int a = 100'000;
int b = a * a;   // overflow! undefined behavior
long long safe = (long long)a * a;  // OK

// Check before multiplying
if (a > 0 && b > INT_MAX / a) {
    // would overflow, handle it
}
04

Control Flow

If / Else

C++17 introduces if with initializer: if (auto it = m.find(k); it != m.end()) { ... }. This scopes the variable to the if/else block, keeping the surrounding scope clean and avoiding accidental reuse.

cpp
int score = 85;
if (score >= 90) {
    std::cout << "A\n";
} else if (score >= 80) {
    std::cout << "B\n";
} else {
    std::cout << "C\n";
}

Switch

Always include break to prevent unintended fall-through. C++17 [[fallthrough]] attribute documents intentional fall-through to silence warnings. Switch works on integral and enum types, not strings or floats.

cpp
int day = 3;
switch (day) {
    case 1: std::cout << "Mon"; break;
    case 2: std::cout << "Tue"; break;
    case 3: std::cout << "Wed"; break;
    default: std::cout << "Other";
}

For Loops

Range-based for iterates containers cleanly. Use const auto& to avoid copying elements (important for strings and large objects). To modify elements in place, use auto& (non-const reference).

cpp
// Classic for
for (int i = 0; i < 5; i++) {
    std::cout << i << " ";
}

// Range-based for (C++11)
std::vector<int> v = {1, 2, 3};
for (int n : v) std::cout << n;
for (const auto &n : v) std::cout << n;  // no copy

While & Do-While

while checks the condition before executing; do-while executes the body at least once before checking. do-while is useful for input validation and menu loops where the body must run before the condition can be evaluated.

cpp
int n = 5;
while (n > 0) {
    std::cout << n-- << " ";
}

int x;
do {
    std::cin >> x;
} while (x < 0);  // runs at least once

Break, Continue & Nested Loops

break exits the nearest enclosing loop; continue skips to the next iteration. C++ has no labeled break like Java; use a flag variable, or extract the loop into a function and use return to break out of nested loops.

cpp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    std::cout << i << " ";  // 0 1 2 4 5 6
}

// C++ has no labeled break; use a flag
bool found = false;
for (int i = 0; i < n && !found; i++) {
    for (int j = 0; j < m; j++) {
        if (matrix[i][j] == target) { found = true; break; }
    }
}
05

Functions & Lambdas

Define & Multiple Returns

C++17 structured bindings (auto [a, b] = ...) unpack tuples, pairs, and structs cleanly. Before C++17, use std::tie or output parameters. Returning by value is cheap due to move semantics (RVO) which elides the copy.

cpp
#include <tuple>
std::tuple<int, int> divide(int a, int b) {
    return {a / b, a % b};
}

auto [q, r] = divide(17, 5);  // q=3, r=2 (C++17)

Default & Inline

Default arguments let callers omit trailing parameters. inline is a hint to the compiler to expand the function inline; modern compilers decide inlining themselves based on optimization flags, so inline is mostly about ODR (one definition rule).

cpp
inline int power(int base, int exp = 2) {
    int r = 1;
    for (int i = 0; i < exp; i++) r *= base;
    return r;
}
// power(3) == 9, power(2, 5) == 32

Function Overloading

Overloading lets functions share a name but differ by parameter types. The compiler picks the best match via overload resolution. Ambiguous overloads cause compile errors; prefer templates when the body is identical across types.

cpp
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(std::string a, std::string b) { return a + b; }

add(1, 2);          // int version
add(1.5, 2.5);      // double version
add("a", "b");      // string version

Lambda Expressions

Lambdas create anonymous function objects inline. The [] captures variables: [=] by value, [&] by reference, [x] specific by value, [&x] specific by ref. They are essential for STL algorithms and callbacks. Beware dangling references when capturing by reference.

cpp
auto square = [](int x) { return x * x; };
std::cout << square(5);  // 25

int factor = 3;
auto multiply = [factor](int x) { return x * factor; };

std::vector<int> v = {1, 2, 3};
std::for_each(v.begin(), v.end(), [](int n) {
    std::cout << n << " ";
});

Function Pointers & std::function

std::function (from <functional>) holds any callable: functions, lambdas, functors. It's more flexible than raw function pointers but has a small runtime overhead due to type erasure. Use it for callbacks and storing callables in containers.

cpp
#include <functional>
int add(int a, int b) { return a + b; }

int (*fp)(int, int) = add;             // raw function pointer
std::function<int(int, int)> f = add;  // flexible wrapper
f = [](int a, int b) { return a * b; };

std::cout << f(3, 4);  // 12
06

STL Containers

vector

vector is a dynamic array and the default container choice. push_back is amortized O(1). .at() does bounds checking (throws std::out_of_range), operator[] does not. Call reserve() upfront if you know the size to avoid reallocations.

cpp
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();
v.size();          // 3
v[0] = 0;
v.at(5);           // throws std::out_of_range
for (int n : v) std::cout << n;

map & unordered_map

map keeps keys sorted (O(log n) operations); unordered_map uses hashing (O(1) average). Use map when you need ordered iteration or range queries; unordered_map for pure lookup speed. Iterating unordered_map is unordered.

cpp
#include <map>
#include <unordered_map>
std::map<std::string, int> ordered;   // sorted by key (red-black tree)
ordered["Alice"] = 30;

std::unordered_map<std::string, int> hashed;  // hash table
hashed["Bob"] = 25;

for (auto &[k, v] : ordered) {  // C++17 structured binding
    std::cout << k << ":" << v;
}

set & unordered_set

set stores unique sorted elements (O(log n)). unordered_set is the hash-based version (O(1) average). Use them for deduplication and membership testing. lower_bound/upper_bound enable range queries on ordered sets.

cpp
#include <set>
std::set<int> s = {3, 1, 4, 1, 5};
// s contains: 1, 3, 4, 5 (sorted, unique)
s.insert(2);
s.erase(1);
if (s.count(4)) std::cout << "found";
auto it = s.lower_bound(3);  // first >= 3

array & deque

array is a fixed-size stack-allocated array with STL interface (safer than C arrays, no decay to pointer). deque (double-ended queue) supports O(1) push/pop at both ends, unlike vector which is O(n) at the front.

cpp
#include <array>
#include <deque>
std::array<int, 3> arr = {1, 2, 3};  // fixed size, stack-allocated
arr.size();  // 3

std::deque<int> dq = {1, 2, 3};
dq.push_front(0);
dq.push_back(4);
// dq: 0, 1, 2, 3, 4

tuple & pair

tuple holds heterogeneous values of any types. pair is a 2-element tuple. Structured bindings (C++17) decompose them into named variables. Common when iterating maps whose elements are pairs of (key, value).

cpp
#include <tuple>
std::tuple<int, std::string, double> t = {1, "Alice", 3.14};
auto [id, name, val] = t;  // C++17 structured binding

std::pair<int, int> p = {1, 2};
std::cout << p.first << p.second;

auto [a, b] = std::make_pair(10, 20);
07

Pointers & Memory

Raw Pointers

Pointers store memory addresses. & gets the address, * dereferences. Pointer arithmetic works on arrays. Raw pointers don't track ownership, leading to leaks and dangling pointers—prefer smart pointers for owned resources.

cpp
int x = 10;
int *ptr = &x;        // address of x
std::cout << *ptr;    // 10 (dereference)
*ptr = 20;
std::cout << x;       // 20

int arr[] = {1, 2, 3};
int *p = arr;
std::cout << *(p + 1);  // 2

References vs Pointers

References are safer (never null, always valid) and have cleaner syntax. Use references for function parameters and return values. Use pointers when null is a meaningful state or when you need to reassign what is pointed to.

cpp
int x = 10;
int &ref = x;    // must init, cannot reseat
int *ptr = &x;   // can be null, can reassign

ref = 20;        // x = 20
*ptr = 30;       // x = 30

// References cannot be null, safer for parameters
void foo(const std::string &s);  // preferred
void bar(std::string *s);        // s might be null

unique_ptr

unique_ptr is sole ownership of a heap object. It cannot be copied, only moved. Automatically deletes when it goes out of scope (RAII). This is the default smart pointer for most use cases—zero overhead over a raw pointer.

cpp
#include <memory>
auto p = std::make_unique<int>(42);
std::cout << *p;  // 42
// auto p2 = p;   // ERROR: cannot copy
auto p2 = std::move(p);  // transfer ownership
// p is now nullptr

shared_ptr & weak_ptr

shared_ptr uses reference counting; the object is freed when the last shared_ptr is destroyed. weak_ptr observes without affecting the count, breaking reference cycles. Avoid cycles of shared_ptr (they leak because the count never reaches zero).

cpp
#include <memory>
auto a = std::make_shared<int>(42);
auto b = a;  // both point to same object
std::cout << a.use_count();  // 2

std::weak_ptr<int> w = a;  // observer, no ownership
if (auto locked = w.lock()) {
    std::cout << *locked;  // 42
}

RAII & new/delete

RAII ties resource lifetime to object scope: constructors acquire, destructors release. This guarantees cleanup even when exceptions propagate. Prefer vectors and smart pointers over manual new/delete—they implement RAII for you.

cpp
// Manual new/delete (avoid in modern C++)
int *p = new int(42);
delete p;

// RAII: resource acquisition is initialization
class Buffer {
    int *data;
public:
    Buffer(size_t n) : data(new int[n]) {}
    ~Buffer() { delete[] data; }  // auto cleanup
};

// Modern: use containers/smart pointers instead
std::vector<int> buf(100);  // no manual delete needed
08

Classes & OOP

Class & Constructor

The member initializer list (: name(...), age(...)) initializes members before the body runs, more efficient than assignment in the body. Mark getters const to allow calling on const objects. std::move avoids copying the string parameter.

cpp
class Person {
    std::string name;
    int age;
public:
    Person(std::string n, int a) : name(std::move(n)), age(a) {}
    std::string getName() const { return name; }
    int getAge() const { return age; }
};

Person p("Alice", 30);
std::cout << p.getName();

Access Modifiers & Encapsulation

private members are accessible only within the class; protected allows subclasses; public is open to all. Encapsulation hides implementation details, exposing a stable interface. Use private by default and expose only what's needed.

cpp
class Account {
private:
    double balance;
protected:
    std::string owner;
public:
    Account(double b) : balance(b) {}
    double getBalance() const { return balance; }
    void deposit(double amt) { if (amt > 0) balance += amt; }
};

Inheritance & Virtual

virtual enables runtime polymorphism—calling speak() through an Animal* dispatches to Dog's version. Always declare a virtual destructor in base classes so deleting through a base pointer calls the derived destructor. override catches typos.

cpp
class Animal {
public:
    virtual void speak() { std::cout << "..."; }
    virtual ~Animal() = default;  // virtual destructor!
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof"; }
};

Animal *a = new Dog();
a->speak();  // Woof (polymorphism)
delete a;

Abstract Classes & Interfaces

A pure virtual function (= 0) makes the class abstract—you cannot instantiate it. Classes with only pure virtuals act like Java interfaces. Concrete subclasses must implement all pure virtuals or they remain abstract.

cpp
class Shape {
public:
    virtual double area() const = 0;  // pure virtual
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double r;
public:
    Circle(double r) : r(r) {}
    double area() const override {
        return 3.14159 * r * r;
    }
};

Operator Overloading

Operator overloading lets user types work with +, <<, ==, etc. Overload only when the meaning is intuitive (math types, iterators). The << operator is commonly overloaded via friend for stream output, enabling cout << myObject.

cpp
class Vec2 {
    double x, y;
public:
    Vec2(double x, double y) : x(x), y(y) {}
    Vec2 operator+(const Vec2 &o) const {
        return {x + o.x, y + o.y};
    }
    friend std::ostream &operator<<(std::ostream &os, const Vec2 &v) {
        return os << "(" << v.x << "," << v.y << ")";
    }
};

Vec2 a(1, 2), b(3, 4);
std::cout << a + b;  // (4,6)
09

Templates & Generics

Function Templates

Function templates generate type-specific versions at compile time. The compiler deduces T from arguments; you can also specify it explicitly. Templates are zero-cost abstractions—no runtime overhead, but they increase compile time and binary size.

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

std::cout << max_val(3, 7);           // int: 7
std::cout << max_val(3.14, 2.71);     // double: 3.14
std::cout << max_val<std::string>("a", "b");  // explicit

Class Templates

Class templates parameterize entire classes over types. The standard containers (vector, map) are all templates. Template code must be in headers (or use explicit instantiation) because the compiler needs the full definition to generate code.

cpp
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(T v) { data.push_back(v); }
    T pop() { T v = data.back(); data.pop_back(); return v; }
    bool empty() const { return data.empty(); }
};

Stack<int> si;
si.push(1); si.push(2);
Stack<std::string> ss;
ss.push("hi");

Template Specialization

Full specialization provides a custom implementation for a specific type. Partial specialization (only for class templates) customizes for a category of types (e.g., all pointer types). Useful for optimizing or special-casing behavior.

cpp
template <typename T>
T identity(T x) { return x; }

// Full specialization for bool
template <>
bool identity<bool>(bool x) {
    std::cout << "bool!";
    return x;
}

identity(42);     // generic
identity(true);   // specialized

Variadic Templates

Variadic templates accept any number of arguments via parameter packs (...). They recurse to process each argument. C++17 fold expressions simplify this: (std::cout << ... << args). Used heavily in std::make_shared, std::tuple.

cpp
template <typename T>
void print(T v) { std::cout << v << "\n"; }

template <typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << ", ";
    print(rest...);  // recurse
}

print(1, "hello", 3.14);  // 1, hello, 3.14

Concepts (C++20)

Concepts (C++20) constrain template parameters with readable requirements, replacing arcane enable_if/SFINAE. They produce much clearer error messages when constraints aren't met. Use standard concepts like std::integral, std::floating_point, std::convertible_to.

cpp
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }

template <typename T>
requires std::floating_point<T>
T half(T x) { return x / 2; }

// Abbreviated form
auto square(std::integral auto x) { return x * x; }
10

STL Algorithms

sort & find

STL algorithms operate on iterator ranges [begin, end). sort is O(n log n). find is linear; for sorted ranges use binary_search/lower_bound (O(log n)). Pass custom comparators (lambdas or std::greater) for custom ordering.

cpp
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());          // 1 1 3 4 5 9
std::sort(v.begin(), v.end(), std::greater<>());  // descending

auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) std::cout << "found";

transform & for_each

transform maps each element to a new value (like map in functional languages). for_each applies a function for side effects. C++20 ranges allow v | views::transform(...) for a cleaner pipeline style without begin/end iterators.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
               [](int x) { return x * x; });
// squared: 1 4 9 16

std::for_each(v.begin(), v.end(), [](int &x) { x *= 2; });
// v: 2 4 6 8

accumulate & count

accumulate (from <numeric>) folds a range with an operation. The third argument is the initial value and determines the result type—use 0.0 for double sums. count returns how many elements equal a value; count_if uses a predicate.

cpp
#include <numeric>
std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0);  // 15
int product = std::accumulate(v.begin(), v.end(), 1,
                              std::multiplies<>());  // 120

int cnt = std::count(v.begin(), v.end(), 3);  // 1

copy, remove & unique

remove doesn't actually erase—it shifts non-matching elements forward and returns a new end iterator. Pair with .erase() for the erase-remove idiom. unique similarly compacts consecutive duplicates; sort first to dedupe fully.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> dest;
std::copy(v.begin(), v.end(), std::back_inserter(dest));

auto end = std::remove(v.begin(), v.end(), 3);  // erase-remove idiom
v.erase(end, v.end());

std::vector<int> u = {1, 1, 2, 2, 3};
u.erase(std::unique(u.begin(), u.end()), u.end());  // 1 2 3

min, max & clamp

min/max return the smaller/larger of two values or an initializer list. minmax returns both as a pair. clamp (C++17) restricts a value to a range, replacing manual if/else bounds checks—useful for input validation and UI coordinates.

cpp
int a = 3, b = 7;
std::cout << std::max(a, b);   // 7
std::cout << std::min(a, b);   // 3

auto [mn, mx] = std::minmax({3, 1, 4, 1, 5});  // mn=1, mx=5

int score = 105;
int clamped = std::clamp(score, 0, 100);  // 100
11

Error Handling

Exceptions: try/catch

Throw exceptions by value, catch by const reference to avoid slicing. Catching std::exception catches all standard exceptions via the base class. Exceptions are for exceptional cases, not normal control flow—they have overhead when thrown.

cpp
#include <stdexcept>
try {
    int x = 10, y = 0;
    if (y == 0) throw std::runtime_error("division by zero");
    std::cout << x / y;
} catch (const std::exception &e) {
    std::cerr << "Error: " << e.what();
}

Standard Exception Hierarchy

<stdexcept> provides standard exception types. logic_error is for programmer errors (detectable before runtime); runtime_error for unforeseen runtime conditions. Derive custom exceptions from std::runtime_error so they integrate with standard catch blocks.

cpp
#include <stdexcept>
throw std::runtime_error("runtime error");
throw std::logic_error("logic error");
throw std::out_of_range("index out of range");
throw std::invalid_argument("bad argument");
throw std::bad_alloc();  // thrown by new on allocation failure

// std::exception is the base; .what() returns the message

Custom Exceptions

Derive custom exceptions from a standard base so they integrate with catch(const std::exception&). Add context fields (file paths, error codes) that help debugging. Always pass the message to the base constructor so what() works.

cpp
class FileError : public std::runtime_error {
public:
    FileError(const std::string &msg, const std::string &path)
        : std::runtime_error(msg + ": " + path), path_(path) {}
    const std::string &path() const { return path_; }
private:
    std::string path_;
};

try { throw FileError("not found", "data.txt"); }
catch (const FileError &e) {
    std::cerr << e.what() << " at " << e.path();
}

noexcept & RAII Safety

noexcept promises a function won't throw, enabling compiler optimizations. If it does throw, std::terminate is called. RAII guarantees destructors run during stack unwinding, so resources are freed even when exceptions propagate up the call stack.

cpp
void safe_op() noexcept {
    // guaranteed not to throw
}

void risky() {
    std::vector<int> v(1000);  // RAII: cleanup on exception
    throw std::runtime_error("oops");
    // v's destructor still runs during stack unwinding
}

Assertions

assert() checks conditions in debug builds; it's removed in release (when NDEBUG is defined) so don't use it for production checks. Use it for internal invariants that indicate bugs. For user-facing validation, throw exceptions or return error codes.

cpp
#include <cassert>
double sqrt_safe(double x) {
    assert(x >= 0 && "sqrt of negative");
    return std::sqrt(x);
}
// In release builds (NDEBUG defined), assert is removed
12

File I/O & Streams

Reading a File

ifstream opens a file for reading. Always check if the open succeeded (!file evaluates true on failure). getline reads line by line including spaces. The stream's destructor closes the file automatically (RAII), so no manual close needed.

cpp
#include <fstream>
#include <string>
std::ifstream file("input.txt");
if (!file) { std::cerr << "cannot open"; return 1; }

std::string line;
while (std::getline(file, line)) {
    std::cout << line << "\n";
}

Writing a File

ofstream writes to a file, truncating by default. Use std::ios::app to append, std::ios::binary for binary mode. The << operator works exactly like std::cout. Flush with out.flush() or use std::endl (which also flushes).

cpp
#include <fstream>
std::ofstream out("output.txt");
if (!out) return 1;

out << "Line 1\n";
out << "Value: " << 42 << "\n";

out.close();  // optional, destructor closes
// Append mode: std::ofstream("f.txt", std::ios::app);

String Streams

ostringstream builds strings from mixed types (like a buffer). istringstream parses strings into typed values. They're slower than direct string operations but very convenient for serialization, URL building, and token parsing.

cpp
#include <sstream>
// Build a string from mixed types
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string url = oss.str();

// Parse tokens from a string
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;

Binary Files

Binary mode avoids newline translation and is more compact than text. write/read take char* and byte count—use reinterpret_cast for structs. Note: binary files are not portable across architectures (endianness, struct padding differ).

cpp
#include <fstream>
struct Record { int id; double value; };

// Write
std::ofstream out("data.bin", std::ios::binary);
Record r{1, 3.14};
out.write(reinterpret_cast<char*>(&r), sizeof(r));

// Read
std::ifstream in("data.bin", std::ios::binary);
Record r2;
in.read(reinterpret_cast<char*>(&r2), sizeof(r2));

Formatted Output (C++20 fmt)

std::format (C++20) brings Python-style format strings to C++, replacing messy iomanip manipulators. For older code, <iomanip> provides setprecision, setw, setfill. The {fmt} library is a popular pre-C++20 alternative with the same syntax.

cpp
#include <format>  // C++20
std::string s = std::format("Hello, {}! You are {}.", "Alice", 30);
std::cout << std::format("{:.2f}", 3.14159);  // 3.14
std::cout << std::format("{:>10}", "right");   // padded

// Pre-C++20: use iomanip
#include <iomanip>
std::cout << std::fixed << std::setprecision(2) << 3.14159;
13

Smart Pointers

unique_ptr - Exclusive Ownership

unique_ptr is the default smart pointer—use it when one owner is enough. It has zero overhead vs raw pointers. make_unique is preferred (exception-safe). Cannot copy, only move. Custom deleters enable RAII for C resources like FILE* or sockets.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // ERROR: cannot copy
std::unique_ptr<int> p2 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

// Custom deleter
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("f.txt", "r"), deleter);

shared_ptr - Shared Ownership

shared_ptr uses reference counting—multiple pointers can own the same object. Object is destroyed when refcount hits 0. make_shared is preferred (single allocation for object + control block). Heavier than unique_ptr due to atomic refcount and control block. Use when ownership is genuinely shared.

cpp
#include <memory>
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: both share ownership
std::cout << *p1 << " " << p1.use_count();  // 42 2

// Control block holds refcount + deleter + allocator
// Refcount is atomic (thread-safe), but object access is NOT
std::shared_ptr<int> p3{new int{10}};  // uses non-array new

// Thread-safe: refcount operations are atomic
// NOT thread-safe: accessing the pointed-to object

weak_ptr - Breaking Cycles

weak_ptr is a non-owning observer of a shared_ptr. Doesn't increase refcount. Use lock() to temporarily get a shared_ptr (returns null if object was destroyed). Essential for breaking reference cycles (e.g., doubly-linked lists, parent-child relationships) that would cause memory leaks.

cpp
#include <memory>
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak to avoid cycle
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // weak_ptr from shared_ptr

// To use a weak_ptr, lock() into a shared_ptr
if (auto locked = b->prev.lock()) {
    std::cout << "prev exists";
} else {
    std::cout << "prev expired";
}

Smart Pointers with Arrays

Smart pointers can manage arrays. unique_ptr<T[]> provides operator[] and correct delete[]. shared_ptr<T[]> is supported since C++17. However, std::vector or std::array are almost always better—safer, more ergonomic, and self-documenting. Use smart array pointers only when interfacing with legacy APIs.

cpp
#include <memory>
// C++17: shared_ptr supports arrays
std::shared_ptr<int[]> arr1(new int[10]);
arr1[0] = 42;

// unique_ptr with arrays (partial specialization exists)
std::unique_ptr<int[]> arr2 = std::make_unique<int[]>(10);
arr2[5] = 100;

// Prefer std::array or std::vector over raw arrays
#include <vector>
std::vector<int> v(10);  // better choice

enable_shared_from_this

When an object needs to return a shared_ptr to itself, enable_shared_from_this provides safe shared_from_this(). Calling shared_ptr<T>(this) directly would create a second control block, leading to double-free. The object must already be managed by a shared_ptr, or shared_from_this() throws bad_weak_ptr.

cpp
#include <memory>
class Widget : public std::enable_shared_from_this<Widget> {
public:
    std::shared_ptr<Widget> getPtr() {
        return shared_from_this();  // safe
        // return shared_ptr<Widget>(this);  // BAD: double delete
    }
};

auto w = std::make_shared<Widget>();
auto w2 = w->getPtr();  // shares ownership, refcount = 2
14

Move Semantics & Rvalue References

Lvalues, Rvalues, and References

Lvalues have identity and persist beyond a single expression (named objects). Rvalues are temporary or literal values. T& binds to lvalues, T&& binds to rvalues. const T& is special—it binds to both. Understanding this distinction is the foundation of move semantics.

cpp
int x = 10;          // x is an lvalue
int& lref = x;       // lvalue reference
int&& rref = 20;     // rvalue reference (binds to temporary)
// int& bad = 20;    // ERROR: can't bind lref to rvalue
const int& cref = 20;  // OK: const lref binds to rvalue

int y = x + 5;       // (x + 5) is an rvalue (prvalue)
std::string s1 = "hi";
std::string&& rr = std::move(s1);  // rr is named rvalue

std::move and Move Constructors

std::move doesn't move anything—it casts to rvalue, enabling move constructor/assignment to be selected. Move operations should be noexcept so containers can use them during reallocation (otherwise they fall back to copy for exception safety). After a move, the source object is in a valid-but-unspecified state.

cpp
class Buffer {
    int* data;
    size_t size;
public:
    // Move constructor: steal resources
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data; size = other.size;
            other.data = nullptr; other.size = 0;
        }
        return *this;
    }
};

Buffer a(100);
Buffer b = std::move(a);  // calls move ctor, a is now empty

Perfect Forwarding

Perfect forwarding passes arguments to another function while preserving their value category (lvalue vs rvalue). T&& in a deduced context is a 'forwarding reference' (not rvalue reference). std::forward<T> conditionally casts: lvalue if T is T&, rvalue if T is T&&. Essential for factory functions and wrappers.

cpp
#include <utility>
template <typename T, typename Arg>
auto make_unique(Arg&& arg) {
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}

template <typename... Args>
void log(Args&&... args) {
    // std::forward preserves value category
    log_impl(std::forward<Args>(args)...);
}

std::string s = "hello";
log(s);              // Arg = std::string&  (lvalue)
log(std::string());  // Arg = std::string&& (rvalue)

Rule of Five / Rule of Zero

Rule of Five: if a class manages a resource, you must define destructor, copy ctor, copy assign, move ctor, move assign. Rule of Zero: prefer composing RAII types (vector, string, smart pointers) so the compiler-generated special members are correct. This eliminates bug-prone manual resource management.

cpp
// Rule of Five: if you define any of these, define all 5
class Resource {
    int* data;
public:
    Resource(size_t n) : data(new int[n]) {}
    ~Resource() { delete[] data; }
    Resource(const Resource& o);             // copy ctor
    Resource& operator=(const Resource& o);  // copy assign
    Resource(Resource&& o) noexcept;         // move ctor
    Resource& operator=(Resource&& o) noexcept;  // move assign
};

// Rule of Zero: prefer to use RAII types
class Better {
    std::vector<int> data;  // handles everything
public:
    Better() = default;  // compiler-generated funcs are correct
};

Return Value Optimization (RVO/NRVO)

RVO/NRVO allows the compiler to construct the return value directly in the caller's storage, avoiding copies/moves entirely. C++17 makes RVO mandatory for prvalues. Never write return std::move(local)—it inhibits NRVO and forces a (slower) move. Just return the local by name and let the compiler optimize.

cpp
std::vector<int> makeVec() {
    std::vector<int> v;
    v.push_back(1); v.push_back(2);
    return v;  // NRVO: no copy, no move!
}

std::string makeStr() {
    return std::string("hello");  // RVO
}

// C++17 guarantees RVO (mandatory copy elision) for prvalues
std::vector<int> v = makeVec();  // constructed in place

// Don't std::move return values—it disables RVO!
std::vector<int> bad() {
    std::vector<int> v;
    return std::move(v);  // WORSE: forces move, blocks NRVO
}
15

Concurrency (thread, mutex, async)

std::thread Basics

std::thread launches a new OS thread. You MUST call join() (wait) or detach() (let it run independently) before the thread object is destroyed, else std::terminate is called. Arguments are passed by value by default—use std::ref for references, std::move for move-only types. Prefer join unless you have a clear reason to detach.

cpp
#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "Thread " << id << "\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2([]{ std::cout << "Lambda thread\n"; });

    t1.join();   // wait for t1 to finish
    t2.join();   // must join or detach before destruction
    // t1.detach();  // runs independently (risky)

    // Hardware concurrency hint
    unsigned n = std::thread::hardware_concurrency();
}

Mutex and Lock Guard

Always protect shared data with a mutex. std::lock_guard is the simplest RAII lock—acquires on construction, releases on destruction. std::scoped_lock (C++17) safely locks multiple mutexes using a deadlock-avoidance algorithm. std::unique_lock offers more flexibility (manual lock/unlock, deferred locking) for use with condition variables.

cpp
#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> shared;

void safePush(int val) {
    // RAII lock: unlocks even if exception thrown
    std::lock_guard<std::mutex> lock(mtx);
    shared.push_back(val);
}  // lock released here

// std::scoped_lock (C++17) locks multiple mutexes deadlock-free
std::mutex m1, m2;
void transfer() {
    std::scoped_lock lock(m1, m2);  // atomic
}

// std::unique_lock: lockable/unlockable, movable
std::unique_lock<std::mutex> ul(mtx);
ul.unlock();
ul.lock();

Condition Variables

condition_variable lets threads wait for a condition. Always use a predicate with wait() to handle spurious wakeups. The mutex must be held by a unique_lock when calling wait(), which releases it while waiting and re-acquires before returning. notify_one wakes one waiter, notify_all wakes all. This pattern implements thread-safe queues and producer-consumer pipelines.

cpp
#include <condition_variable>
#include <queue>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        tasks.push(42);
    }
    cv.notify_one();  // wake one waiting consumer
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return !tasks.empty(); });  // predicate prevents spurious wakeup
    int task = tasks.front();
    tasks.pop();
}

std::async and Futures

std::async is a high-level way to run tasks asynchronously, returning a future. std::launch::async forces a new thread; std::launch::deferred runs lazily on get(). Default policy may choose either—be explicit for predictable behavior. For more control, use std::promise/future pairs. Always call get() on a future before destruction, else the destructor may block.

cpp
#include <future>
#include <iostream>

int slowComputation() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
}

// async policy: may run in new thread or deferred
auto fut = std::async(std::launch::async, slowComputation);
// do other work...
int result = fut.get();  // blocks until ready

// std::launch::async: definitely new thread
// std::launch::deferred: lazy, runs on get()
// std::launch::async | deferred: implementation chooses

// std::promise for manual control
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread([&p]{ p.set_value(10); }).detach();
f.get();  // 10

Atomic Operations

std::atomic provides lock-free thread-safe operations for primitive types. Heavier than a plain int but much lighter than mutex for simple counters/flags. Memory ordering affects visibility: relaxed (no ordering), acquire/release (pair for synchronization), seq_cst (default, strongest). Use atomics for counters/flags; use mutex for complex critical sections.

cpp
#include <atomic>
#include <iostream>

std::atomic<int> counter{0};
std::atomic<bool> ready{false};

void worker() {
    while (!ready.load(std::memory_order_acquire));
    counter.fetch_add(1, std::memory_order_relaxed);
}

// Compare-and-swap (CAS) loop
std::atomic<int> val{0};
int expected = 0;
bool success = val.compare_exchange_weak(
    expected, 1,
    std::memory_order_acq_rel);

// Atomic is lock-free for most primitive types
static_assert(std::atomic<int>::is_always_lock_free);

// std::atomic_flag: guaranteed lock-free, for spinlocks
std::atomic_flag spin = ATOMIC_FLAG_INIT;
while (spin.test_and_set(std::memory_order_acquire));  // acquire
spin.clear(std::memory_order_release);  // release
16

Template Metaprogramming

Template Specialization

Template specialization provides custom implementations for specific types. Full specialization fixes all template parameters. Partial specialization (only for class templates) specializes some parameters while keeping others generic. Used heavily in type traits, std::vector<bool>, and optimizing for known types.

cpp
// Primary template
template <typename T>
struct TypeName {
    static const char* get() { return "unknown"; }
};

// Full specialization for int
template <>
struct TypeName<int> {
    static const char* get() { return "int"; }
};

// Full specialization for const char*
template <>
struct TypeName<const char*> {
    static const char* get() { return "string"; }
};

std::cout << TypeName<int>::get();           // "int"
std::cout << TypeName<double>::get();        // "unknown"
std::cout << TypeName<const char*>::get();   // "string"

SFINAE and enable_if

SFINAE (Substitution Failure Is Not An Error) lets you enable/disable template overloads based on type properties. std::enable_if conditionally defines a type. When substitution fails, the overload is silently removed instead of causing an error. C++17's if constexpr and C++20 concepts often replace SFINAE with cleaner syntax.

cpp
#include <type_traits>

// SFINAE: Substitution Failure Is Not An Error
// Only enable this overload if T is integral
template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T x) { std::cout << "integral\n"; }

// Only enable if T is floating point
template <typename T,
          std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T x) { std::cout << "floating\n"; }

process(10);    // integral
process(3.14);  // floating

// void_t trick (C++17) for detecting member existence
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

if constexpr (C++17)

if constexpr evaluates the condition at compile time and discards the false branch entirely (not even type-checked). This replaces many SFINAE patterns with much cleaner code. Particularly useful for template recursion (the base case terminates the recursion) and for branching on type traits without instantiation errors.

cpp
#include <type_traits>
template <typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // only compiled if T is a pointer
    } else if constexpr (std::is_integral_v<T>) {
        return t * 2;
    } else {
        return t;
    }
}

int x = 5;
int* p = &x;
getValue(x);   // 10
getValue(p);   // 5
getValue(3.14);  // 3.14

// Compile-time recursion with termination
template <int N>
constexpr int factorial() {
    if constexpr (N <= 1) return 1;
    else return N * factorial<N - 1>();
}

Variadic Templates and Fold Expressions

Variadic templates accept any number of arguments via parameter packs (typename... Args). C++17 fold expressions apply an operator to all pack elements: unary fold (... op pack), binary fold (init op ... op pack). Before C++17, you needed recursion with a base case. Variadic templates power std::make_unique, std::tuple, printf-like functions.

cpp
#include <iostream>

// Parameter pack
template <typename... Args>
void print(Args... args) {
    // C++17 fold expression
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

print(1, "hello", 3.14, 'x');  // 1 hello 3.14 x

// Sum with fold
template <typename... T>
auto sum(T... args) {
    return (args + ...);  // binary fold: ((a+b)+c)+d
}

// Sum with initial value
template <typename... T>
auto sumFrom0(T... args) {
    return (0 + ... + args);  // left fold with init
}

// Base case recursion (pre-C++17)
template <typename T>
void printOne(T t) { std::cout << t; }

Concepts (C++20)

Concepts (C++20) replace SFINAE with readable, intention-revealing constraints. They produce much better error messages than SFINAE. Define concepts with concept Name = constraint;. Use them in template parameters, requires clauses, or abbreviated templates (auto with concept). The standard library provides many useful concepts in <concepts>.

cpp
#include <concepts>

// Define a concept
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Use as constraint
template <Numeric T>
T add(T a, T b) { return a + b; }

// Requires clause
template <typename T>
requires requires(T t) { t.size(); }
auto getSize(const T& t) { return t.size(); }

// Concepts in abbreviated function templates
void process(std::integral auto x) { /* ... */ }
void process(std::ranges::range auto& r) { /* ... */ }

// Standard concepts: integral, floating_point, equality_comparable,
// totally_ordered, copyable, movable, default_initializable, etc.
17

Lambda Expressions Deep Dive

Capture Modes

Lambdas capture variables from the enclosing scope. [=] captures all by value, [&] by reference—convenient but error-prone (dangling references, unintended captures). Prefer explicit captures [x, &y] for clarity. Init captures [name = expr] (C++14) allow renaming, moving, and computing captured values. Capture [&] carefully—lambdas outliving the scope cause dangling references.

cpp
int x = 10, y = 20;
std::vector<int> v;

auto a = [] { /* no capture */ };
auto b = [x] { return x; };           // capture x by value
auto c = [&x] { x = 100; };           // capture x by reference
auto d = [=] { return x + y; };       // capture all by value
auto e = [&] { x = 1; y = 2; };       // capture all by reference
auto f = [x, &y] { return x + y; };   // mixed
auto g = [=, &x] { x = 1; return y; }; // default value, x by ref
auto h = [this] { return member; };   // capture this (C++17: *this)
auto i = [x = x + 5] { return x; };   // init capture (C++14)

// Best practice: capture only what you need, explicitly

Generic Lambdas (C++14)

Generic lambdas use auto parameters (C++14) or explicit template parameters (C++20). They're essentially compiler-generated template operator() overloads. C++20 template lambdas let you access the type parameter T directly. Recursive lambdas need std::function (or C++23 deducing this) because a plain auto lambda can't refer to itself by name before its type is known.

cpp
// auto parameters (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2);        // int
add(1.5, 2.5);    // double
add(std::string("a"), std::string("b"));

// Template lambdas (C++20)
auto getSize = []<typename T>(const std::vector<T>& v) {
    return v.size();
};

// Perfect forwarding in lambdas (C++20)
auto wrapper = []<typename T>(T&& arg) {
    return process(std::forward<T>(arg));
};

// Recursive lambda with std::function
#include <functional>
std::function<int(int)> fib = [&](int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
};

Mutable and Stateful Lambdas

By default, value-captured variables in a lambda are const—call operator() is const. The mutable keyword removes this constness, allowing modification of captured-by-value variables (modifying the copy, not the original). This enables stateful lambdas like counters. Each copy of a lambda has its own captured state. Use std::function to store lambdas of different types in containers.

cpp
#include <functional>
int counter = 0;

// Without mutable, value-captured vars are const
auto inc = [counter]() mutable {
    return ++counter;  // modifies the captured copy
};
inc(); inc();
std::cout << counter;  // still 0 (original unchanged)

// Stateful counter with std::function
auto makeCounter(int start) {
    return [count = start]() mutable { return ++count; };
}
auto c = makeCounter(10);
c(); c();  // 11, 12

// Lambda as callback storage
std::vector<std::function<void()>> callbacks;
callbacks.push_back([&]{ std::cout << "click"; });

Lambdas with STL Algorithms

Lambdas shine as predicates/comparators for STL algorithms. They replace the old functor (function object) approach with inline, readable code. Common uses: comparators for sort, predicates for find_if/remove_if/ copy_if, transformations for transform. C++20 ranges make this even cleaner with the pipe operator for composable transformations.

cpp
#include <algorithm>
#include <vector>

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

// Sort descending
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;
});

// Find first even
auto it = std::find_if(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });

// Remove odds (erase-remove idiom)
v.erase(std::remove_if(v.begin(), v.end(),
    [](int x) { return x % 2 != 0; }), v.end());

// Transform
std::transform(v.begin(), v.end(), v.begin(),
    [](int x) { return x * x; });

// C++20 ranges: cleaner
auto evens = v | std::views::filter([](int x){ return x%2==0; })
               | std::views::transform([](int x){ return x*2; });

Higher-Order Functions

Lambdas enable functional programming patterns in C++. Functions can return lambdas (closures) or accept them as parameters. std::function wraps any callable for type-erased storage. Function composition builds pipelines by chaining lambdas. This style is powerful for callbacks, event handlers, and data transformation pipelines, though template-based approaches avoid std::function overhead.

cpp
#include <functional>
// Function returning a function
auto makeMultiplier(int factor) {
    return [factor](int x) { return x * factor; };
}
auto doubleIt = makeMultiplier(2);
auto tripleIt = makeMultiplier(3);
doubleIt(5);  // 10
tripleIt(5);  // 15

// Function taking a function
template <typename F>
void applyTwice(int x, F f) {
    std::cout << f(f(x));
}
applyTwice(5, [](int x){ return x + 3; });  // 11

// Composition
auto compose = [](auto f, auto g) {
    return [f, g](auto x) { return f(g(x)); };
};
auto addOne = [](int x){ return x + 1; };
auto square = [](int x){ return x * x; };
auto sqThenAdd = compose(addOne, square);
sqThenAdd(3);  // 10
18

Namespaces & Modules

Namespace Basics

Namespaces prevent name collisions in large projects. They can be nested and aliased for convenience. Anonymous namespaces give symbols internal linkage (visible only in the current translation unit), replacing the C-style static keyword for this purpose. Avoid using namespace in headers—it pollutes every including file.

cpp
namespace math {
    double pi = 3.14159;
    double square(double x) { return x * x; }

    namespace geometry {  // nested
        double circleArea(double r) { return pi * square(r); }
    }
}

// Usage
double a = math::square(5);
double area = math::geometry::circleArea(2.0);

// Namespace alias
namespace geo = math::geometry;
geo::circleArea(1.0);

// Anonymous namespace: internal linkage (like 'static')
namespace {
    int internalVar = 42;  // only visible in this file
}

using Declarations and Directives

using declaration (using std::cout) imports a single name—safe and recommended. using directive (using namespace std) imports everything—convenient but risky, especially in headers (causes name conflicts and ambiguity). Keep using directives to small scope (function/implementation files) and never in headers. C++20 using enum imports all enumerators of a scoped enum.

cpp
#include <iostream>
#include <vector>

// using declaration: brings one name
using std::cout;
using std::vector;
cout << "hello";
vector<int> v;

// using directive: brings entire namespace
using namespace std;  // brings ALL of std
// AVOID in headers! Pollutes global namespace.

// Namespace-scoped using (safe)
namespace mylib {
    using std::string;  // only affects mylib
    using std::vector;
    string s;
}

// C++20: using enum
enum class Color { Red, Green, Blue };
void print() {
    using enum Color;
    auto c = Red;  // no Color:: needed here
}

Argument-Dependent Lookup (ADL)

ADL (Koenig lookup) finds free functions based on the namespaces of their arguments. This is why std::cout << x works without std::operator<<—the compiler looks in x's namespace. ADL is essential for operator overloads and customization points like swap. The 'using std::swap; swap(a,b);' pattern lets user types provide optimized swaps while falling back to std::swap.

cpp
namespace mylib {
    struct Widget {
        int value;
    };
    // Operator overload found via ADL
    std::ostream& operator<<(std::ostream& os, const Widget& w) {
        return os << "Widget(" << w.value << ")";
    }

    void helper(const Widget&) {}
}

int main() {
    mylib::Widget w{42};
    // ADL: finds operator<< in mylib without qualification
    std::cout << w;  // works! no mylib:: needed

    // ADL applies to free functions too
    helper(w);  // wait—needs mylib::helper unless ADL applies
    mylib::helper(w);  // explicit
}

// swap is the classic ADL use case
namespace ns { struct X {};
    void swap(X&, X&) {}  // customized swap
}
void f(ns::X& a, ns::X& b) {
    using std::swap;
    swap(a, b);  // ADL picks ns::swap if available
}

Inline Namespaces (Versioning)

Inline namespaces expose their members as if they were in the enclosing namespace. This enables library versioning: make the newest version inline so users get it by default, while old versions remain accessible via explicit qualification. It's also used for ABI compatibility and feature toggling. Changing which namespace is inline shifts the default version without modifying user code.

cpp
// Inline namespace members are part of the enclosing namespace
namespace mylib {
    inline namespace v2 {
        struct Widget { int x, y, z; };  // newer version
        void process(Widget) {}
    }

    namespace v1 {
        struct Widget { int x, y; };  // older version
    }
}

// v2::Widget is accessible as mylib::Widget (inline)
mylib::Widget w;  // actually mylib::v2::Widget
mylib::process(w);

// Explicitly use v1 if needed
mylib::v1::Widget oldW;

// ABI versioning: change which namespace is inline
// to change default version without breaking old code

C++20 Modules

C++20 modules replace #include with a faster, more robust system. export module declares a module; export marks visible declarations. Modules are compiled once (not re-parsed per translation unit), dramatically improving build times. They avoid macro pollution and header order issues. Adoption is gradual—toolchain support (CMake, build systems) is still maturing as of 2024.

cpp
// math.cppm (module interface unit)
export module math;

export double pi = 3.14159;

export double square(double x) {
    return x * x;
}

// Internal (not exported)
double internalHelper(double x) {
    return x * 2;
}

// main.cpp
import math;
import std;  // standard library module (C++23)

int main() {
    return square(pi);  // 9.87
    // internalHelper(3);  // ERROR: not exported
}

// Module partitions (sub-modules)
export module math:geometry;
export double circleArea(double r);
19

Preprocessor & Macros

Include Guards and #pragma once

Include guards prevent a header from being processed multiple times in one translation unit, avoiding redefinition errors. #ifndef/#define/#endif is standard and portable. #pragma once is simpler and avoids macro name collisions but is technically non-standard (supported by all major compilers). Modern code often uses #pragma once for simplicity.

cpp
// Traditional include guard (header.h)
#ifndef MY_HEADER_H
#define MY_HEADER_H

// declarations here
class Widget { /* ... */ };

#endif  // MY_HEADER_H

// Alternative: #pragma once (non-standard but widely supported)
#pragma once

class Widget { /* ... */ };

// #pragma once pros: simpler, no risk of macro name collision
// Include guards pros: standard, works everywhere
// Both prevent multiple inclusion in a single TU

Function-like Macros

Function-like macros are text substitution. ALWAYS parenthesize each argument and the whole expression to avoid precedence bugs. The do { ... } while (0) idiom makes a macro behave like a single statement. Macros have no type checking, no scope, and can have side effects (MAX(i++, j++) increments twice). Prefer constexpr/inline/templates in modern C++.

cpp
// Basic macro
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

int m = MAX(3, 5);      // ((3) > (5) ? (3) : (5))
int s = SQUARE(4);      // ((4) * (4))

// ALWAYS parenthesize arguments!
#define BAD(x) x * x
BAD(1 + 2);  // 1 + 2 * 1 + 2 = 5, not 9!

// Multi-line macro with do-while
#define LOG(msg) do { \
    std::cerr << __FILE__ << ":" << __LINE__ \
              << " " << msg << "\n"; \
} while (0)

// Variadic macro (C++11)
#define PRINT(...) printf(__VA_ARGS__)
PRINT("x=%d, y=%d\n", x, y);

Stringification and Concatenation

# (stringification) converts a macro argument to a string literal. ## (token pasting) concatenates two tokens into one. The two-level STR/XSTR trick first expands macros then stringifies them. These operators are essential for code generation, logging macros, and X-macros. Modern C++ often replaces these with constexpr functions, but they remain useful for compile-time string/token generation.

cpp
// # (stringification) turns a macro argument into a string literal
#define STR(x) #x
#define XSTR(x) STR(x)
const char* s1 = STR(hello world);  // "hello world"
const char* s2 = STR(42);           // "42"

// Two-level macro to expand macros before stringifying
#define VERSION 100
const char* v1 = STR(VERSION);   // "VERSION"
const char* v2 = XSTR(VERSION);  // "100"

// ## (token pasting) concatenates tokens
#define CONCAT(a, b) a ## b
#define MAKE_VAR(n) var_ ## n
int MAKE_VAR(1) = 10;  // int var_1 = 10;
int CONCAT(foo, bar) = 5;  // int foobar = 5;

// Useful for generating unique names
#define UNIQUE(prefix) prefix ## __LINE__
int UNIQUE(tmp_) = 0;  // int tmp_42 = 0; (if on line 42)

Conditional Compilation

Conditional compilation (#if, #ifdef, #ifndef, #elif, #else, #endif) includes/excludes code at compile time. Used for platform-specific code, debug builds, and feature flags. defined(X) checks if X is defined (value irrelevant). #error aborts compilation with a message. Prefer constexpr if when possible—it's type-safe and the code is always compiled (catching errors in all configurations).

cpp
#define DEBUG 1
#define PLATFORM "windows"

#if DEBUG
    std::cout << "Debug mode\n";
#endif

#ifdef DEBUG
    logDebug("entered function");
#endif

#ifndef NDEBUG
    assert(ptr != nullptr);
#endif

#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
#endif

// Check C++ standard
#if __cplusplus >= 202002L
    #define CPP20_OR_LATER
#elif __cplusplus >= 201703L
    #define CPP17_OR_LATER
#endif

// #error and #warning
#if !defined(VERSION)
    #error "VERSION must be defined"
#endif

Predefined Macros and __VA_OPT__

Predefined macros provide compile-time info: __FILE__/__LINE__ for logging, __cplusplus for standard version, __func__ for function names (it's an identifier, not a macro). __VA_OPT__ (C++20) conditionally inserts a comma only when __VA_ARGS__ is non-empty, fixing the trailing comma problem in variadic macros. __has_include (C++17) enables optional header inclusion for portability.

cpp
// Standard predefined macros
std::cout << __FILE__;       // source file path
std::cout << __LINE__;       // current line number
std::cout << __DATE__;       // compilation date "Mmm dd yyyy"
std::cout << __TIME__;       // compilation time "hh:mm:ss"
std::cout << __cplusplus;    // C++ standard version
std::cout << __func__;       // current function name (not macro)

// Compiler-specific
#ifdef __GNUC__
    std::cout << __GNUC__ << "." << __GNUC_MINOR__;
#endif
#ifdef _MSC_VER
    std::cout << "MSVC " << _MSC_VER;
#endif

// __VA_OPT__ (C++20): expands to its arg if variadic has args
#define LOG(fmt, ...) \
    printf(fmt __VA_OPT__(,) __VA_ARGS__)
LOG("plain");              // printf("plain")
LOG("x=%d", x);            // printf("x=%d", x)

// __has_include (C++17): check if header exists
#if __has_include(<optional>)
    #include <optional>
#endif
20

Design Patterns in C++

Singleton (Meyers' Singleton)

Singleton ensures a class has one instance with global access. Meyers' Singleton (static local variable) is the cleanest C++ implementation—thread-safe initialization is guaranteed since C++11. Delete copy operations to prevent duplication. Singletons are controversial (global state, hard to test); prefer dependency injection when feasible. Use when genuinely one instance is needed (logger, config, hardware interface).

cpp
class Logger {
public:
    static Logger& instance() {
        // Meyers' singleton: thread-safe in C++11+
        static Logger inst;
        return inst;
    }
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << "\n";
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;
};

// Usage
Logger::instance().log("started");

// Avoid: lazy init with new + manual sync (error-prone)
// The static local is initialized once, thread-safely

RAII (Resource Acquisition Is Initialization)

RAII is C++'s most important idiom: acquire resources in constructors, release in destructors. This guarantees cleanup even when exceptions are thrown. Smart pointers, std::lock_guard, std::fstream, and std::vector all use RAII. The destructor runs during stack unwinding, making exception-safe code natural. RAII eliminates manual new/delete, lock/unlock, open/close—embrace it everywhere.

cpp
// RAII: resource tied to object lifetime
class FileHandle {
    FILE* fp;
public:
    explicit FileHandle(const char* path) : fp(fopen(path, "r")) {
        if (!fp) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (fp) fclose(fp); }
    FILE* get() { return fp; }
    // disable copy to prevent double-close
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    // allow move
    FileHandle(FileHandle&& o) noexcept : fp(o.fp) { o.fp = nullptr; }
};

{
    FileHandle f("data.txt");
    // use f.get()
}  // fclose called automatically, even on exception

Pimpl Idiom (Pointer to Implementation)

Pimpl (Pointer to Implementation) hides implementation details behind a pointer. Benefits: faster compilation (private members don't appear in header), true ABI stability (changing Impl doesn't break ABI), reduced include dependencies. The destructor must be defined in the .cpp file because unique_ptr<Impl> needs Impl to be complete. Common in library design for stable APIs.

cpp
// widget.h - public interface
class Widget {
public:
    Widget();
    ~Widget();  // must define in .cpp (unique_ptr needs complete type)
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
    void doSomething();
private:
    class Impl;  // forward declaration
    std::unique_ptr<Impl> pimpl;
};

// widget.cpp - implementation
#include "widget.h"
#include <vector>
class Widget::Impl {
public:
    std::vector<int> data;
    void doSomething() { /* ... */ }
};
Widget::Widget() : pimpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
void Widget::doSomething() { pimpl->doSomething(); }

Observer Pattern

Observer lets subjects notify subscribers of changes without coupling. std::function makes it easy to accept lambdas, member functions, and functors. For production code, consider a token-based unsubscribe mechanism (return an ID on subscribe, use it to remove). Watch for dangling references if observers capture [&] and outlive the subject. Qt's signals/slots and C# events are mature implementations.

cpp
#include <functional>
#include <vector>
#include <string>

class Subject {
    std::vector<std::function<void(const std::string&)>> observers;
public:
    void subscribe(std::function<void(const std::string&)> cb) {
        observers.push_back(cb);
    }
    void notify(const std::string& event) {
        for (auto& cb : observers) cb(event);
    }
};

// Usage
Subject s;
s.subscribe([](const std::string& e){ std::cout << "A: " << e; });
s.subscribe([](const std::string& e){ std::cout << "B: " << e; });
s.notify("hello");  // both callbacks invoked

// For removal, use a token/ID system or observer objects
// instead of std::function

CRTP (Curiously Recurring Template Pattern)

CRTP (Derived : Base<Derived>) achieves static polymorphism—the base class customizes behavior via the derived type. No virtual function overhead. Used to add functionality (Comparable, Iterable) to derived classes via mixins. Downcasting via static_cast is safe because the template guarantees the derived type. CRTP powers std::enable_shared_from_this, std::iterator, and many policy-based designs.

cpp
// CRTP: class Derived : public Base<Derived>
template <typename Derived>
struct Comparable {
    bool operator==(const Derived& other) const {
        return static_cast<const Derived*>(this)->equalTo(other);
    }
    bool operator!=(const Derived& other) const {
        return !(*this == other);
    }
};

struct Point : Comparable<Point> {
    int x, y;
    bool equalTo(const Point& o) const { return x == o.x && y == o.y; }
};

Point a{1, 2}, b{1, 2};
a == b;  // true (uses Comparable::operator==)
a != b;  // false

// Static polymorphism (no virtual overhead)
template <typename T>
void draw(const T& shape) {
    static_cast<const T&>(shape).drawImpl();
}
21

Smart Pointers Deep

unique_ptr

unique_ptr is sole owner of its object. Cannot be copied, only moved. Automatically deletes when out of scope. make_unique is the preferred way to create. Zero overhead vs raw pointers.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // Error: cannot copy
std::unique_ptr<int> p3 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

shared_ptr

shared_ptr allows multiple owners via reference counting. use_count() shows the number of owners. Thread-safe for the counter but not the object. Use make_shared for efficiency (single allocation).

cpp
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: shared ownership
std::cout << p1.use_count();  // 2
// Reference counting: deleted when count reaches 0
// Thread-safe for reference count, not for the object

weak_ptr

weak_ptr is a non-owning reference to shared_ptr. Prevents circular references (memory leaks). lock() tries to convert to shared_ptr. Use expired() to check if the object still exists. Does not affect reference count.

cpp
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;  // Does not increase count
if (auto locked = weak.lock()) {  // Try to get shared_ptr
    std::cout << *locked;  // Use the object
} else {
    std::cout << "Object deleted";
}

Custom Deleter

Custom deleters allow unique_ptr to manage non-memory resources like file handles, sockets, and C APIs. The deleter type is part of the unique_ptr type. Useful for RAII with C libraries.

cpp
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), fclose);
// Or with lambda
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file2(fopen("test.txt", "r"), deleter);

enable_shared_from_this

enable_shared_from_this allows an object to safely get a shared_ptr to itself. Calling shared_ptr(this) directly would create a second reference count, causing double deletion. Inherit from enable_shared_from_this and use shared_from_this().

cpp
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getPtr() {
        return shared_from_this();  // Safe
        // return std::shared_ptr<Node>(this);  // BUG: double delete
    }
};
22

Move Semantics Deep

Move Constructor

Move constructors steal resources instead of copying. noexcept is important: STL containers only move (not copy) if the move is noexcept. Always leave the moved-from object in a valid state.

cpp
class Buffer {
    int* data; size_t size;
public:
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr; other.size = 0;  // Leave in valid state
    }
    Buffer& operator=(Buffer&& other) noexcept {
        delete[] data;
        data = other.data; size = other.size;
        other.data = nullptr; other.size = 0;
        return *this;
    }
};

std::move

std::move is a cast to rvalue reference. It does not perform the move itself. The move constructor or assignment operator does the actual work. After std::move, the source object is in a valid but unspecified state.

cpp
std::string s1 = "Hello";
std::string s2 = std::move(s1);  // s1 is now empty
// std::move does not move anything!
// It casts to an rvalue reference, enabling move
// The actual move happens in the constructor/assignment

Perfect Forwarding

Perfect forwarding preserves the value category of arguments. T&& in a template is a forwarding reference, binding to both lvalues and rvalues. std::forward<T> casts back to the original category. Essential for factory functions and wrappers.

cpp
template<typename T, typename Arg>
auto make(Arg&& arg) {
    return std::make_shared<T>(std::forward<Arg>(arg));
}
// std::forward preserves value category:
// lvalue stays lvalue, rvalue stays rvalue
// Arg&& is a forwarding reference (not rvalue reference)

RVO & NRVO

RVO (Return Value Optimization) and NRVO (Named RVO) eliminate copies by constructing the object in place. Compilers perform this automatically. Using std::move on a local return value prevents NRVO and may pessimize. Trust the compiler.

cpp
std::string create() {
    std::string s = "Hello";
    return s;  // NRVO: no copy, no move
}
std::string s = create();  // RVO: no copy
// With -O2, compilers eliminate the copy/move entirely
// Do NOT use std::move on return of local variable!

Rule of Five

Rule of Five: if you define any of destructor, copy constructor/assignment, or move constructor/assignment, define all five. This ensures correct resource management. The Rule of Zero is preferred: use RAII types (smart pointers, vectors) to avoid manual management.

cpp
class Resource {
public:
    Resource();  // Constructor
    ~Resource();  // Destructor
    Resource(const Resource&);  // Copy constructor
    Resource& operator=(const Resource&);  // Copy assignment
    Resource(Resource&&) noexcept;  // Move constructor
    Resource& operator=(Resource&&) noexcept;  // Move assignment
};
23

Templates Deep

Variadic Templates

Variadic templates accept any number of arguments. Fold expressions (C++17) simplify unpacking. The recursive approach works in C++11. sizeof...(args) gives the count. Used in tuple, make_shared, and printf replacements.

cpp
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << '\n';  // C++17 fold expression
}
print(1, "hello", 3.14, 'a');  // 1hello3.14a
// Recursive unpacking (pre-C++17)
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first;
    print(rest...);
}

SFINAE

SFINAE (Substitution Failure Is Not An Error) removes invalid overloads from consideration. enable_if adds a condition to the template. If the condition is false, the specialization is ignored. C++17 if constexpr is often cleaner.

cpp
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value) { /* integer version */ }
template<typename T,
         typename = std::enable_if_t<std::is_floating_point_v<T>>,
         typename = void>  // Different signature
void process(T value) { /* float version */ }

if constexpr

if constexpr (C++17) evaluates conditions at compile time. Only the true branch is compiled, the other is discarded. Much cleaner than SFINAE for conditional compilation. Works with auto return type deduction.

cpp
template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;
    else
        return t;
}
// Only the matching branch is compiled
// No SFINAE needed

Concepts (C++20)

Concepts (C++20) constrain template parameters with readable syntax. They provide better error messages than SFINAE. Use existing concepts (integral, floating_point) or define custom ones. Concepts can be combined with && and ||.

cpp
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T add(T a, T b) { return a + b; }
// Or: requires clause
template<typename T> requires Number<T>
T multiply(T a, T b) { return a * b; }

Template Specialization

Full specialization provides a complete implementation for a specific type. Partial specialization customizes for a category (e.g., all pointers). The primary template must be declared first. Specializations must match the interface.

cpp
template<typename T>
class Vector { /* general implementation */ };
template<>
class Vector<bool> {  // Full specialization
    // Bit-packed implementation
};
template<typename T>
class Vector<T*> {  // Partial specialization for pointers
    // Pointer-specific implementation
};
24

STL Algorithms Deep

sort & stable_sort

sort is O(n log n), not stable. stable_sort preserves relative order of equal elements. Use a comparator for custom ordering. C++20 ranges allow: std::ranges::sort(v, {}, &last_digit).

cpp
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());  // 1,1,3,4,5,9
std::sort(v.begin(), v.end(), std::greater<>());  // Descending
// Custom comparator
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a % 10 < b % 10;  // Sort by last digit
});

transform & accumulate

transform applies a function to each element. accumulate folds elements with an operation (default +). The initial value determines the type. Use std::reduce (C++17) for parallel reduction.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
    [](int x) { return x * x; });  // 1,4,9,16
int sum = std::accumulate(v.begin(), v.end(), 0);  // 10
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>());

find & count

find returns an iterator to the first match or end(). count returns the number of matches. find_if uses a predicate. All are O(n). For sorted ranges, use binary_search, lower_bound, upper_bound (O(log n)).

cpp
std::vector<int> v = {1, 2, 3, 2, 1};
auto it = std::find(v.begin(), v.end(), 2);  // First 2
size_t cnt = std::count(v.begin(), v.end(), 2);  // 2
auto it2 = std::find_if(v.begin(), v.end(),
    [](int x) { return x > 2; });  // First > 2

copy & remove

copy_if copies matching elements. back_inserter appends to the destination. remove does not actually remove; it shifts non-matching elements forward and returns a new end. erase removes the leftover. C++20 adds std::erase for containers.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
    [](int x) { return x % 2 == 0; });  // 2,4
// Remove-erase idiom
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// C++20: std::erase(v, 3);

minmax & clamp

minmax returns a pair of (min, max) in one pass. clamp restricts a value to a range. min/max accept initializer lists or two values. All accept custom comparators. Structured bindings (C++17) simplify the result.

cpp
auto [min, max] = std::minmax({3, 1, 4, 1, 5});  // C++17
int value = std::clamp(15, 0, 10);  // 10 (clamped to max)
int m = std::min({1, 2, 3});
int m2 = std::max({1, 2, 3}, [](int a, int b) { return a < b; });
25

Concurrency

std::thread

std::thread creates OS threads. join() waits for completion (blocking). detach() runs independently (may outlive the creator). A thread with neither causes std::terminate. Pass arguments by value or use std::ref for references.

cpp
#include <thread>
void task(int n) { /* ... */ }
std::thread t1(task, 42);
std::thread t2([]() { /* lambda */ });
t1.join();  // Wait for completion
t2.detach();  // Run independently
// Always join or detach before destruction

std::mutex

lock_guard is simple RAII: locks on construction, unlocks on destruction. unique_lock is more flexible: can unlock/relock, used with condition variables. Never unlock manually with lock_guard. Use std::scoped_lock for multiple mutexes.

cpp
std::mutex mtx;
int shared = 0;
void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII
    ++shared;
}  // Auto-unlock
// std::unique_lock for conditional locking
std::unique_lock<std::mutex> ulock(mtx);
ulock.unlock();  // Manual unlock
ulock.lock();    // Re-lock

std::async & futures

std::async runs a function asynchronously, returning a future. get() blocks and retrieves the result. launch::async forces a new thread. launch::deferred runs synchronously on get(). Default may be either. Exceptions are propagated through get().

cpp
#include <future>
std::future<int> f = std::async(std::launch::async, []() {
    return 42;
});
int result = f.get();  // Blocks until ready
// std::launch::async: always new thread
// std::launch::deferred: lazy, runs on get()

Condition Variable

condition_variable synchronizes threads. wait releases the lock and blocks until notified. The predicate handles spurious wakeups. notify_one wakes one waiter, notify_all wakes all. Always modify shared state under the mutex before notifying.

cpp
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Waiter
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return ready; });  // Spurious wakeups handled
// Notifier
{
    std::lock_guard<std::mutex> lk(mtx);
    ready = true;
}
cv.notify_one();  // Or notify_all()

Atomic Operations

atomic provides lock-free thread-safe operations. fetch_add/sub are read-modify-write. compare_exchange implements CAS (compare-and-swap). memory_order_relaxed is fastest but weakest. seq_cst (default) is strongest. Use atomics for simple counters and flags.

cpp
#include <atomic>
std::atomic<int> counter{0};
counter++;  // Atomic increment
counter.fetch_add(1, std::memory_order_relaxed);
bool expected = false;
counter.compare_exchange_strong(expected, true);
// Memory orders: relaxed, acquire, release, seq_cst
26

Modern C++ Features

Structured Bindings

Structured bindings (C++17) decompose pairs, tuples, and structs. auto& for references, auto for copies. Simplifies iteration over maps. Works with any aggregate type. Much cleaner than .first/.second.

cpp
std::pair p = {1, "hello"};
auto [num, str] = p;  // C++17
std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
    std::cout << key << ": " << value;
}

std::optional

optional represents a value that may or may not exist. Better than pointers or sentinel values. has_value() or operator bool checks. value() throws if empty, value_or() provides a default. Use for functions that may fail to produce a result.

cpp
#include <optional>
std::optional<int> find(bool found) {
    if (found) return 42;
    return std::nullopt;
}
auto result = find(true);
if (result) std::cout << *result;
// Or: result.value_or(0)

std::variant

variant is a type-safe union. Holds one of several types. visit applies a visitor (overloaded lambda). index() returns the current type index. get_if<T> safely retrieves. Replaces unions and inheritance for closed type hierarchies.

cpp
#include <variant>
std::variant<int, std::string> v;
v = 42;
v = "hello";
std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int: " << arg;
    else if constexpr (std::is_same_v<T, std::string>)
        std::cout << "string: " << arg;
}, v);

Ranges (C++20)

Ranges (C++20) provide composable, lazy views. The | operator chains operations. No intermediate containers. Views are lazy: computation happens on iteration. Much more readable than nested algorithm calls.

cpp
#include <ranges>
namespace rv = std::ranges::views;
auto result = std::vector{1, 2, 3, 4, 5}
    | rv::filter([](int x) { return x % 2 == 0; })
    | rv::transform([](int x) { return x * x; });
// Lazy evaluation: no computation until iterated
for (int x : result) std::cout << x;  // 4, 16

Coroutines (C++20)

Coroutines (C++20) enable async and generator patterns. co_yield suspends and returns a value. co_await waits for another coroutine. co_return finishes. The compiler transforms coroutines into state machines. Need a return type (Generator, Task) implementation.

cpp
#include <coroutine>
Generator<int> counter() {
    for (int i = 0; ; ++i)
        co_yield i;  // Suspend and yield
}
for (int x : counter()) {
    if (x > 5) break;
    std::cout << x;
}
// co_await: wait for async operation
// co_return: finish coroutine
27

Common Pitfalls

Dangling Pointers

Dangling pointers point to freed memory. Accessing them is undefined behavior. Use smart pointers (unique_ptr, shared_ptr) to avoid manual memory management. If using raw pointers, set to nullptr after delete. Use tools like AddressSanitizer to detect.

cpp
// BUG: dangling pointer
int* p = new int(42);
delete p;
std::cout << *p;  // Undefined behavior
// FIX: use smart pointers
auto p = std::make_unique<int>(42);
// Or set to nullptr after delete
int* p2 = new int(42);
delete p2; p2 = nullptr;

Iterator Invalidation

Vector push_back may reallocate, invalidating all iterators. erase invalidates iterators at and after the point. list iterators are stable except for erased elements. Check iterator invalidation rules for each container. When in doubt, use indices.

cpp
std::vector<int> v = {1, 2, 3};
// BUG: iterator invalidated
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 2) v.push_back(4);  // May invalidate it
}
// FIX: use index
for (size_t i = 0; i < v.size(); ++i) {
    if (v[i] == 2) v.push_back(4);
}

Undefined Behavior

Undefined behavior (UB) allows the compiler to do anything. Uninitialized variables, out-of-bounds access, null dereference, signed overflow are all UB. Compilers optimize assuming no UB, leading to surprising results. Use -fsanitize=undefined to detect.

cpp
int x;  // Uninitialized: UB to read
std::cout << x;  // UB
int arr[5];
arr[5] = 0;  // Out of bounds: UB
int* p = nullptr;
*p;  // Null dereference: UB
// Signed overflow: UB (unsigned wraps)
int big = INT_MAX + 1;  // UB

Slicing

Object slicing occurs when a derived object is copied to a base type. The derived parts are lost. Pass by reference or pointer to avoid slicing. Virtual calls on a sliced object call the base version. Always pass polymorphic types by reference/pointer.

cpp
class Base { public: virtual ~Base() {} };
class Derived : public Base { int extra; };
void take(Base b) { /* copies Base part only */ }
Derived d;
take(d);  // Sliced: extra lost
// FIX: pass by reference or pointer
void take(const Base& b) { /* full object */ }

Static Initialization

Static initialization order across translation units is undefined. One file may use another file not-yet-initialized static. Fix with function-local statics (Meyers Singleton): initialization is lazy and thread-safe (C++11+). Access via function call.

cpp
// Static initialization order fiasco
// File1.cpp
int x = computeX();  // May use y
// File2.cpp
int y = computeY();  // May use x
// Order across files is undefined!
// FIX: function-local static
int& getX() {
    static int x = computeX();  // Lazy, thread-safe
    return x;
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.