Table of Contents

0. Setup & Environment

Install Xcode Command Line Tools (macOS default)

The fastest path to a working C++ compiler on macOS — installs Apple Clang with full toolchain.

xcode-select --install       # opens GUI installer
clang++ --version            # verify: Apple clang 15.x or later
Apple Clang supports C++17 fully, C++20 mostly, and C++23 partially. For complete C++20/23 standard library coverage, use GCC instead.

Alternative: GCC via Homebrew

GCC provides full C++20/23 support and different (often more verbose) diagnostics — useful to have alongside Clang.

brew install gcc
g++-14 --version             # use versioned binary, not plain g++
On macOS, g++ is an alias for Apple Clang — always use the versioned binary (g++-14) when you want actual GCC. Check with which g++ && g++ --version.

C++ Standard Libraries

Two standard library implementations are common; they differ in ABI and occasionally in which C++23 features are available.

Build Tools

make ships with the Xcode CLI tools. CMake is the de facto standard for any non-trivial project.

brew install cmake            # CMake — used by virtually every open-source C++ project
cmake --version

# One-off compilation without a build system:
clang++ -Wall -Wextra -std=c++20 -O2 -o hello hello.cpp

Package Managers

C++ has no single universal package manager, but two options cover most needs:

Editor Setup

Quick Verify

Confirm your toolchain handles C++20 before going further:

mkdir ~/cpp-refresher && cd ~/cpp-refresher
cat > hello.cpp << 'EOF'
#include <iostream>
#include <format>
int main() {
    std::cout << std::format("Hello, C++{}!\n", 20);
    return 0;
}
EOF
clang++ -Wall -std=c++20 -o hello hello.cpp && ./hello
# Expected: Hello, C++20!
std::format requires C++20 and is fully supported on Apple Clang 15+ (Xcode 15+) and GCC 13+. If the compile fails, upgrade your toolchain or add -stdlib=libc++.

1. Program Basics

Compilation Model

C++ uses a multi-phase compilation model: preprocessing, compilation (source to object), and linking (objects to executable). Each .cpp file is a translation unit compiled independently.

# Compile with modern standards and useful warnings
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 -o app main.cpp util.cpp

# Separate compilation — compile then link
g++ -std=c++20 -c main.cpp      # produces main.o
g++ -std=c++20 -c util.cpp      # produces util.o
g++ main.o util.o -o app        # link

# Clang equivalents
clang++ -std=c++20 -stdlib=libc++ -Wall -Wextra -o app main.cpp

# Common useful flags
# -g        debug symbols
# -O0/-O2/-O3  optimization levels
# -fsanitize=address,undefined  enable ASan + UBSan
# -march=native  optimize for host CPU
# -DNDEBUG  disable assert() (release builds)

Namespaces

// Named namespace
namespace mylib {
    void helper() {}

    // Nested namespace (C++17 shorthand)
    namespace detail {
        void impl() {}
    }
}

// C++17 nested shorthand
namespace mylib::detail {
    void impl2() {}
}

// using-declaration: bring one name into scope
using mylib::helper;

// using-directive: bring entire namespace (avoid in headers!)
using namespace std;  // OK in .cpp, NEVER in headers

// Inline namespace: versioning — mylib::v2::foo is also mylib::foo
namespace mylib {
    inline namespace v2 {
        void foo() {}
    }
    namespace v1 {
        void foo() {}  // mylib::v1::foo — not the default
    }
}

// Anonymous namespace: internal linkage (prefer over static for non-functions)
namespace {
    int file_local = 42;  // visible only in this translation unit
}

// Namespace alias
namespace fs = std::filesystem;

Headers vs Modules (C++20)

Modules (C++20)
Modules eliminate header inclusion overhead, remove macro leakage, and make build times faster. Compiler support is maturing — GCC 11+, Clang 16+, MSVC 2019 16.8+.
// === Traditional header approach ===
// math_utils.h
#pragma once
namespace mylib {
    int square(int x);
}

// math_utils.cpp
#include "math_utils.h"
namespace mylib {
    int square(int x) { return x * x; }
}

// === C++20 Module approach ===
// math_utils.cppm (or .ixx on MSVC)
export module mylib.math;

export namespace mylib {
    int square(int x) { return x * x; }
}

// Internal helper — not exported
int detail_helper() { return 0; }

// main.cpp
import mylib.math;
import <iostream>;  // import standard library header unit

int main() {
    std::cout << mylib::square(5) << '\n';
}

// Build with modules (GCC example)
// g++ -std=c++20 -fmodules-ts math_utils.cppm main.cpp -o app

One Definition Rule (ODR)

Every entity used must be declared in every translation unit, but defined exactly once across the entire program (with exceptions for inline functions, templates, and constexpr).

// ODR-compliant: inline allows definition in multiple TUs
// (all definitions must be identical)
inline int max_val(int a, int b) { return a > b ? a : b; }

// Templates are implicitly inline — safe in headers
template<typename T>
T square(T x) { return x * x; }

// C++17: inline variables — safe to define in headers
inline constexpr int MAX_SIZE = 1024;

// C++17: variable templates in headers
template<typename T>
inline constexpr T pi = T(3.14159265358979323846);

2. Type System

Fundamental Types & Type Deduction

TypeSize (typical)Range
bool1 bytetrue/false
char1 byteimplementation-defined signed-ness
int4 bytes±2.1B
long long8 bytes±9.2 × 1018
float4 bytes~7 decimal digits
double8 bytes~15 decimal digits
std::size_t8 bytes (64-bit)0 to ~1.8 × 1019
std::ptrdiff_t8 bytes (64-bit)signed pointer difference
#include <cstdint>   // fixed-width types
#include <cstddef>   // size_t, ptrdiff_t, byte
#include <type_traits>

// Prefer fixed-width types for serialization / cross-platform
int8_t  i8  = -128;
uint8_t u8  = 255;
int32_t i32 = 42;
int64_t i64 = 9'000'000'000LL;  // digit separator (C++14)

// auto — deduced at compile time, strips top-level cv-qualifiers and refs
auto x = 42;          // int
auto y = 3.14;        // double
auto z = "hello";     // const char*
auto& r = x;          // int& (explicit ref needed)
const auto& cr = x;   // const int&

// decltype — exact type including refs and cv-qualifiers
int n = 0;
decltype(n) a;         // int
decltype((n)) b = n;   // int& (parentheses = lvalue expression)

// decltype(auto) — deduce like decltype for the initializer expression
decltype(auto) get_ref(int& v) { return v; }   // returns int&
decltype(auto) get_val(int v)  { return v; }   // returns int

// Type aliases
using Seconds = double;
using IntVec  = std::vector<int>;
using BinFunc = std::function<int(int, int)>;

// Old typedef still works, but using is preferred (especially for templates)
typedef unsigned long ulong;  // harder to read with templates

// Template alias (only possible with using, not typedef)
template<typename T>
using Pair = std::pair<T, T>;

Pair<int> coords{1, 2};

Type Traits

#include <type_traits>

// Query traits at compile time
static_assert(std::is_integral_v<int>);
static_assert(std::is_floating_point_v<double>);
static_assert(std::is_same_v<int, int>);
static_assert(!std::is_same_v<int, unsigned int>);
static_assert(std::is_pointer_v<int*>);
static_assert(std::is_reference_v<int&>);
static_assert(std::is_trivially_copyable_v<int>);

// Transform types
using T = std::remove_const_t<const int>;   // int
using U = std::remove_reference_t<int&>;     // int
using V = std::add_pointer_t<int>;           // int*
using W = std::decay_t<const int[3]>;        // int* (array decay)

// SFINAE with enable_if (C++11/14 style — prefer Concepts in C++20)
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
double_val(T x) { return x * 2; }

// void_t for detecting expression validity
template<typename, 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 {};

static_assert(has_size<std::vector<int>>::value);
static_assert(!has_size<int>::value);

Casts

// static_cast: well-defined conversions, checked at compile time
double d = 3.14;
int i = static_cast<int>(d);          // truncates to 3
void* vp = static_cast<void*>(&i);    // pointer to void and back

// dynamic_cast: polymorphic downcasting with RTTI (runtime check)
struct Base { virtual ~Base() = default; };
struct Derived : Base { void extra() {} };

Base* b = new Derived{};
if (auto* dp = dynamic_cast<Derived*>(b)) {
    dp->extra();  // safe: only entered if cast succeeds
}
// Reference form throws std::bad_cast on failure
try {
    Derived& dr = dynamic_cast<Derived&>(*b);
} catch (const std::bad_cast&) { /* ... */ }

// const_cast: add/remove const (undefined behavior to write through
// a pointer that was originally const)
const int ci = 10;
int* p = const_cast<int*>(&ci);  // dangerous — don't write through p

// reinterpret_cast: low-level bit reinterpretation, mostly UB
// Use memcpy or std::bit_cast (C++20) for type punning instead
float f = 1.0f;
uint32_t bits;
std::memcpy(&bits, &f, sizeof(f));   // safe type punning
// C++20:
uint32_t bits2 = std::bit_cast<uint32_t>(f);  // zero-overhead, safe

// std::byte (C++17): type-safe byte manipulation
#include <cstddef>
std::byte b1{0xFF};
std::byte b2 = b1 & std::byte{0x0F};
int val = std::to_integer<int>(b2);
Narrowing Conversions
Brace initialization {} prevents narrowing — use it to catch bugs at compile time. int x{3.14}; is a compile error, but int x = 3.14; is just a warning.

3. Value Categories

Understanding value categories is essential for reasoning about move semantics and overload resolution.

CategoryHas identity?Movable?Examples
lvalueYesNoNamed variables, *ptr, a[i]
prvalueNoYesLiterals, temporaries, a + b
xvalueYesYesstd::move(x), return from function returning T&&
glvalueYeslvalue or xvalue
rvalueNo or expiringYesprvalue or xvalue
int x = 42;     // x is lvalue; 42 is prvalue
int& lr = x;    // OK: lvalue ref binds to lvalue
// int& bad = 42; // ERROR: non-const lvalue ref can't bind to prvalue

const int& cr = 42;   // OK: const lvalue ref extends lifetime of temporary
int&& rr = 42;         // OK: rvalue ref binds to prvalue

// std::move turns an lvalue into an xvalue (castable rvalue)
int&& rr2 = std::move(x);  // x is now an "expiring" value

// Overload resolution: compilers pick the best match
void f(int&)  { std::cout << "lvalue\n"; }
void f(int&&) { std::cout << "rvalue\n"; }

int n = 5;
f(n);           // calls f(int&)  — lvalue
f(5);           // calls f(int&&) — prvalue (rvalue)
f(std::move(n)); // calls f(int&&) — xvalue (rvalue)

// Guaranteed copy elision (C++17): prvalues are not materialized
// until they are needed — the following involves ZERO copies
std::string make_str() { return std::string(1000, 'x'); }
std::string s = make_str();  // no copy, no move — direct construction
Quick Rule
If you can take the address of an expression (&expr), it's an lvalue. If you can't, it's an rvalue. xvalues have identity but signal "I'm done with this."

4. References & Pointers

References

int x = 10;
int& lr  = x;     // lvalue reference — alias for x
int&& rr = 42;    // rvalue reference — binds to temporaries

// Forwarding reference (universal reference) — T&& in a deduced context
template<typename T>
void wrapper(T&& arg) {           // T&& is a forwarding reference
    use(std::forward<T>(arg));    // preserve value category
}

// const lvalue ref binds to anything — use for "sink" params
void process(const std::string& s);  // no copy for lvalues or temporaries

// Reference-to-temporary lifetime extension
const std::string& s = std::string("hello");  // lifetime extended to scope of s
// std::string&& rs = std::string("hello");   // also works

Smart Pointers

Ownership Rule
Use unique_ptr for exclusive ownership (the default), shared_ptr when ownership is shared, and weak_ptr to break cycles. Pass raw pointers to observe (not own) resources.
#include <memory>

// unique_ptr — exclusive ownership, zero overhead
auto up = std::make_unique<int>(42);
auto up2 = std::move(up);           // transfer ownership
// up is now nullptr
*up2 = 100;

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

// shared_ptr — shared ownership via reference count
auto sp1 = std::make_shared<std::string>("hello");
auto sp2 = sp1;                    // ref count = 2
sp1.reset();                       // ref count = 1
std::cout << sp2.use_count();     // 1

// weak_ptr — non-owning observer, breaks cycles
std::weak_ptr<std::string> wp = sp2;
if (auto locked = wp.lock()) {     // try to get a shared_ptr
    std::cout << *locked;
}
// When sp2 goes out of scope, wp.lock() returns nullptr

// Never use new/delete directly — always make_unique / make_shared
// make_shared is more efficient: allocates object + control block together

// Passing smart pointers:
void take_ownership(std::unique_ptr<int> p);  // sink — caller loses ownership
void share(std::shared_ptr<int> p);           // caller shares ownership
void observe(const std::unique_ptr<int>& p);  // borrow, don't own
void observe_raw(int* p);                     // simplest: just observe

// Prefer raw pointer/ref for observation — smart pointers in function
// signatures communicate ownership semantics, not just access

5. Classes & OOP

Constructors & Special Members

class Buffer {
public:
    // Default constructor
    Buffer() : data_(nullptr), size_(0) {}

    // Parameterized constructor
    explicit Buffer(std::size_t n)
        : data_(new char[n]), size_(n) {}

    // Copy constructor
    Buffer(const Buffer& other)
        : data_(new char[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // Copy assignment
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new char[other.size_];
            size_ = other.size_;
            std::copy(other.data_, other.data_ + size_, data_);
        }
        return *this;
    }

    // Move constructor (steal resources)
    Buffer(Buffer&& other) noexcept
        : data_(std::exchange(other.data_, nullptr)),
          size_(std::exchange(other.size_, 0)) {}

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = std::exchange(other.data_, nullptr);
            size_ = std::exchange(other.size_, 0);
        }
        return *this;
    }

    // Destructor
    ~Buffer() { delete[] data_; }

    // Delegating constructor (C++11)
    explicit Buffer(int n) : Buffer(static_cast<std::size_t>(n)) {}

private:
    char* data_;
    std::size_t size_;
};

// Rule of Zero: prefer composing types that manage resources
class GoodBuffer {
    std::vector<char> data_;  // vector handles all 5 special members
public:
    explicit GoodBuffer(std::size_t n) : data_(n) {}
    // Compiler generates all correct special members automatically
};
Rule of Three / Five / Zero
Rule of Three: if you define destructor, copy ctor, or copy assign — define all three.
Rule of Five: add move ctor and move assign for efficiency.
Rule of Zero: design classes so you need none of them (use RAII members). Zero is the goal.

Inheritance & Virtual Functions

struct Shape {
    // Virtual destructor — always when class has virtual functions
    virtual ~Shape() = default;

    // Pure virtual: must be overridden; Shape is abstract
    virtual double area() const = 0;

    // Virtual with default implementation
    virtual std::string describe() const {
        return "Shape with area=" + std::to_string(area());
    }

    // Non-virtual: not polymorphic
    void draw() const { std::cout << describe() << '\n'; }
};

struct Circle : Shape {
    explicit Circle(double r) : radius(r) {}

    // override: compiler checks it actually overrides a virtual
    double area() const override { return 3.14159 * radius * radius; }

    // final: no further overriding allowed
    std::string describe() const override final {
        return "Circle(r=" + std::to_string(radius) + ")";
    }

private:
    double radius;
};

// final class: cannot be inherited from
struct Point final {
    double x, y;
};

// Virtual inheritance solves the diamond problem
struct Animal { virtual void eat() {} };
struct Mammal : virtual Animal {};
struct Bird   : virtual Animal {};
struct Bat    : Mammal, Bird {};  // single Animal subobject

// Multiple inheritance: use with care, prefer interfaces (pure virtuals)
struct Printable { virtual void print() const = 0; };
struct Serializable { virtual std::string serialize() const = 0; };
struct Widget : Printable, Serializable {
    void print() const override { /* ... */ }
    std::string serialize() const override { /* ... */ }
};

// Object slicing — a common pitfall
Shape s = Circle{5.0};  // WRONG: slices to Shape, loses Circle data
Shape* ps = new Circle{5.0};  // correct: polymorphism via pointer

Aggregate & Designated Initialization

// Aggregate: no user-provided constructors, no private/protected members,
// no virtual functions, no base classes (with C++17 relaxation)
struct Point { double x, y, z = 0.0; };

Point p1 = {1.0, 2.0};       // z defaults to 0.0
Point p2{1.0, 2.0, 3.0};     // all three

// Designated initializers (C++20) — name the members explicitly
Point p3{.x = 1.0, .y = 2.0};      // z = 0.0 (default)
Point p4{.x = 1.0, .z = 5.0};      // y = 0.0 (value-initialized)
// Order must match declaration order
// p4{.z = 5.0, .x = 1.0} — ERROR (wrong order)

// Friend functions/classes
class Wallet {
    int balance_ = 0;
    friend class Bank;              // Bank can access private members
    friend std::ostream& operator<<(std::ostream&, const Wallet&);
};

std::ostream& operator<<(std::ostream& os, const Wallet& w) {
    return os << "Wallet(" << w.balance_ << ")";
}

6. RAII & Resource Management

Resource Acquisition Is Initialization: tie resource lifetimes to object lifetimes. Acquire in constructor, release in destructor. Destructors run deterministically at scope exit — even on exception.

// Custom RAII wrapper
class FileHandle {
public:
    explicit FileHandle(const char* path, const char* mode)
        : file_(fopen(path, mode)) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }
    ~FileHandle() { fclose(file_); }

    // Not copyable
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Movable
    FileHandle(FileHandle&& o) noexcept : file_(std::exchange(o.file_, nullptr)) {}
    FileHandle& operator=(FileHandle&& o) noexcept {
        if (this != &o) { fclose(file_); file_ = std::exchange(o.file_, nullptr); }
        return *this;
    }

    FILE* get() const noexcept { return file_; }

private:
    FILE* file_;
};

// RAII for mutexes
#include <mutex>
std::mutex mtx;

void thread_safe_op() {
    std::lock_guard<std::mutex> lock(mtx);  // unlocks at scope exit
    // ... critical section ...
}

void flexible_op() {
    std::unique_lock<std::mutex> lock(mtx);
    // ... critical section ...
    lock.unlock();   // can manually unlock
    // ... non-critical work ...
    lock.lock();     // re-lock
}

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

// Exception safety levels:
// Basic guarantee:  no leaks, invariants maintained, but state may change
// Strong guarantee: operation succeeds or leaves state unchanged (commit/rollback)
// Nothrow guarantee: operation never throws (destructors, move ops should be noexcept)
//
// Achieving strong guarantee with copy-and-swap:
Buffer& Buffer::operator=(Buffer other) {   // pass by value = copy
    swap(*this, other);                      // swap with copy
    return *this;                            // original destroyed by other's dtor
}

7. Move Semantics

std::move and std::forward

std::move is just a cast
std::move(x) does NOT move anything. It casts x to an rvalue reference (T&&), signalling that the value can be moved from. The actual move happens in the move constructor/assignment that receives it.
#include <utility>

std::vector<int> make_vec() {
    std::vector<int> v = {1, 2, 3};
    return v;  // RVO applies — no copy or move needed
}

std::string s1 = "expensive string";
std::string s2 = std::move(s1);  // s1 is in valid but unspecified state
// Do NOT use s1 after moving from it (except to re-assign or destroy)

// std::forward — preserve the value category of a forwarding reference
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
    // forward preserves lvalue/rvalue nature of each argument
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// Return Value Optimization (RVO / NRVO)
std::string build() {
    std::string result;
    result += "hello";
    result += " world";
    return result;  // NRVO: compiler constructs result directly in caller's space
}

// Force a move when RVO can't apply (e.g., returning a parameter)
std::string process(std::string s) {
    // ... modify s ...
    return std::move(s);  // OK: parameter can't benefit from NRVO
    // Actually: just 'return s;' is fine — compilers treat named
    // local vars in return as rvalues when possible (implicit move)
}

// Move-only types
auto up = std::make_unique<int>(42);
auto up2 = std::move(up);     // unique_ptr is move-only — must use move
// auto up3 = up;              // ERROR: copy deleted

// After a move, the moved-from object is in a valid but unspecified state:
// you can re-assign or destroy it, but don't assume its value
std::vector<int> a{1,2,3};
std::vector<int> b = std::move(a);
// a may be empty, but it's a valid vector — can call a.push_back()
a.push_back(99);  // safe

When Moves Happen Implicitly

// 1. Returning a local variable (implicit move in return statement)
Widget create() {
    Widget w;
    return w;  // move (or RVO) — NOT a copy
}

// 2. Initializing from a prvalue
Widget w = Widget{};  // no copy, no move — guaranteed elision (C++17)

// 3. Passing to a function expecting a value (from a temporary)
void consume(Widget w);
consume(Widget{});  // constructed directly into parameter

// 4. Inserting rvalue into containers
std::vector<std::string> v;
v.push_back("hello");          // temporary string — moved in
v.push_back(std::move(s1));    // explicitly move s1 in

// emplace_back: construct in-place, no move at all
v.emplace_back(5, 'x');        // constructs std::string("xxxxx") in-place

8. Templates

Function & Class Templates

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

max_of(3, 5);           // deduced: T=int
max_of(3.14, 2.72);     // deduced: T=double
max_of<long>(3, 5);    // explicit: T=long

// Non-type template parameters
template<typename T, std::size_t N>
class FixedArray {
    T data_[N];
public:
    std::size_t size() const { return N; }
    T& operator[](std::size_t i) { return data_[i]; }
};

FixedArray<int, 10> arr;

// CTAD — Class Template Argument Deduction (C++17)
std::pair p{1, 3.14};           // std::pair<int, double> — deduced!
std::vector v{1, 2, 3};         // std::vector<int>
std::optional opt{42};          // std::optional<int>

// User-defined deduction guides
template<typename T>
struct Wrapper { T value; };
template<typename T> Wrapper(T) -> Wrapper<T>;  // deduction guide
Wrapper w{42};  // Wrapper<int>

Variadic Templates & Fold Expressions

// Parameter pack
template<typename... Ts>
void print_all(Ts&&... args) {
    (std::cout << ... << args) << '\n';  // fold expression (C++17)
}

print_all(1, " hello ", 3.14, '\n');

// Fold expressions (C++17)
template<typename... Ts>
auto sum(Ts... args) {
    return (... + args);          // left fold: ((a + b) + c)
}

template<typename... Ts>
bool all_positive(Ts... args) {
    return (... && (args > 0));  // left fold with &&
}

// Expanding packs in other contexts
template<typename... Ts>
auto make_tuple_of_vectors(Ts... sizes) {
    return std::make_tuple(std::vector<int>(sizes)...);
}

// Recursive variadic (C++11 style — prefer fold in C++17+)
void print_r() {}  // base case
template<typename T, typename... Rest>
void print_r(T first, Rest... rest) {
    std::cout << first << ' ';
    print_r(rest...);
}

Concepts (C++20)

#include <concepts>

// Using standard concepts
template<std::integral T>
T double_it(T x) { return x * 2; }

template<std::floating_point T>
T sqrt_safe(T x) {
    if (x < 0) throw std::domain_error("negative");
    return std::sqrt(x);
}

// Defining a concept
template<typename T>
concept Printable = requires(T t) {
    { std::cout << t } -> std::same_as<std::ostream&>;
};

template<typename T>
concept Container = requires(T c) {
    c.begin();
    c.end();
    c.size();
    typename T::value_type;
};

// requires clause on function template
template<typename T>
    requires Container<T> && Printable<typename T::value_type>
void print_container(const T& c) {
    for (const auto& item : c) std::cout << item << ' ';
}

// Abbreviated function template (C++20 shorthand)
void print_integral(std::integral auto x) {
    std::cout << x << '\n';
}

// Standard concepts to know:
// std::same_as<T, U>      std::convertible_to<F, T>
// std::integral             std::floating_point
// std::signed_integral      std::unsigned_integral
// std::invocable<F, Args...>  std::predicate<F, Args...>
// std::input_iterator       std::random_access_iterator
// std::ranges::range        std::ranges::sized_range

Template Specialization & if constexpr

// Full specialization
template<typename T>
struct Serializer {
    static std::string serialize(const T& t) {
        return std::to_string(t);
    }
};

template<>  // full specialization for std::string
struct Serializer<std::string> {
    static std::string serialize(const std::string& s) {
        return '"' + s + '"';
    }
};

// Partial specialization (only for class templates)
template<typename T>
struct Serializer<std::vector<T>> {
    static std::string serialize(const std::vector<T>& v) {
        std::string out = "[";
        for (const auto& item : v)
            out += Serializer<T>::serialize(item) + ",";
        return out + "]";
    }
};

// if constexpr (C++17): compile-time branch — dead branch not instantiated
template<typename T>
auto describe(T val) {
    if constexpr (std::is_integral_v<T>) {
        return "integer: " + std::to_string(val);
    } else if constexpr (std::is_floating_point_v<T>) {
        return "float: " + std::to_string(val);
    } else {
        // T must have operator<<
        std::ostringstream oss;
        oss << val;
        return "other: " + oss.str();
    }
}

9. Lambda Expressions

// Basic lambda
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4);  // 7

// Capture modes
int x = 10, y = 20;
auto by_val  = [=]() { return x + y; };    // capture all by copy
auto by_ref  = [&]() { x += y; };          // capture all by reference
auto mixed   = [x, &y]() { y = x; };       // x by copy, y by ref
auto this_cp = [*this]() { /* ... */ };     // capture *this by copy (C++17)
auto this_rf = [this]() { /* ... */ };      // capture this pointer by copy

// Mutable: allow modifying captured copies
auto counter = [n = 0]() mutable { return ++n; };  // init-capture (C++14)
counter();  // 1
counter();  // 2

// Generic lambda (C++14): auto params — actually a template operator()
auto print = [](auto val) { std::cout << val << '\n'; };
print(42);
print("hello");

// Template lambda (C++20): explicit type parameter
auto typed = []<typename T>(std::vector<T>& v) {
    std::sort(v.begin(), v.end());
};

// Immediately invoked lambda
int result = [](int n){ return n * n; }(7);  // 49

// Lambda as function pointer (only when no captures)
int (*fp)(int, int) = [](int a, int b){ return a + b; };

// std::function vs templates
// std::function: type-erased, heap allocation, slower — for runtime storage
std::function<int(int)> callback;
callback = [](int x){ return x * 2; };

// Template parameter: zero overhead, but can't be stored heterogeneously
template<typename F>
void apply(F&& f, int x) { std::cout << f(x) << '\n'; }
// prefer this for performance-sensitive code

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

// C++23: deducing this — no std::function needed for recursion
auto fib23 = [](this auto self, int n) -> int {
    return n <= 1 ? n : self(n-1) + self(n-2);
};

10. Standard Library Containers

ContainerAccessInsert/Remove (middle)Insert (end)Use when
vectorO(1) randomO(n)O(1) amortizedDefault sequence
dequeO(1) randomO(n)O(1) both endsQueue + random access
listO(n)O(1) w/ iteratorO(1)Frequent middle insert
array<T,N>O(1) randomN/A (fixed)N/AFixed-size stack array
set/mapO(log n)O(log n)O(log n)Sorted unique keys
unordered_set/mapO(1) avgO(1) avgO(1) avgHash-based lookup
#include <vector>
#include <array>
#include <map>
#include <unordered_map>
#include <set>
#include <span>
#include <string_view>

// vector — contiguous, cache-friendly
std::vector<int> v{1, 2, 3};
v.reserve(100);                // pre-allocate to avoid reallocations
v.push_back(4);
v.emplace_back(5);             // construct in-place
v.insert(v.begin() + 1, 99);  // O(n) — shifts elements
v.erase(v.begin());            // O(n)
auto it = std::find(v.begin(), v.end(), 3);
v.erase(it);

// std::array — fixed size, stack-allocated, safe replacement for C array
std::array<int, 5> arr{1, 2, 3, 4, 5};
arr.at(2);       // bounds-checked
arr[2];          // no bounds check
arr.data();      // raw pointer for C APIs

// map — ordered by key, O(log n) ops
std::map<std::string, int> scores;
scores["alice"] = 95;
scores.insert({"bob", 88});
scores.emplace("carol", 91);    // prefer emplace

auto it2 = scores.find("alice");
if (it2 != scores.end()) { /* found */ }

// try_emplace (C++17): only inserts if key absent, avoids constructing value
scores.try_emplace("dave", 75);  // no construction if "dave" exists

// insert_or_assign (C++17): always updates
scores.insert_or_assign("alice", 100);

// unordered_map — average O(1), custom hash for user types
struct Point { int x, y; };
struct PointHash {
    std::size_t operator()(const Point& p) const {
        return std::hash<int>{}(p.x) ^ (std::hash<int>{}(p.y) << 32);
    }
};
std::unordered_map<Point, int, PointHash> dist;

// std::string_view (C++17): non-owning view, zero-copy
std::string_view sv = "hello world";
sv.substr(0, 5);   // no allocation — returns another view

// std::span (C++20): non-owning view over contiguous data
std::span<int> sp{v.data(), v.size()};
std::span<int, 3> fixed_sp{arr.data(), 3};  // fixed-extent span
// Use span to avoid passing pointer+size pairs to functions
void process(std::span<const int> data);  // accepts vector, array, C array

11. Iterators & Ranges

Iterator Categories

CategoryOperationsExamples
Input++, *, == (single pass)istream_iterator
Output++, * (write only)ostream_iterator
ForwardInput + multi-passforward_list::iterator
BidirectionalForward + --list::iterator, map::iterator
Random AccessBidirectional + +n, -n, []vector::iterator
ContiguousRandom access + contiguous memoryvector::iterator, pointer
#include <ranges>
#include <algorithm>

// Range-based for (uses begin()/end())
std::vector<int> nums{1, 2, 3, 4, 5, 6};
for (int n : nums) { /* ... */ }
for (auto& n : nums) { n *= 2; }  // modify in place

// C++20 Ranges: lazy, composable, no intermediate containers
namespace rv = std::ranges::views;  // or std::views

// Filter + transform pipeline — lazy evaluation
auto result = nums
    | rv::filter([](int n){ return n % 2 == 0; })
    | rv::transform([](int n){ return n * n; });

// Materialize into a vector (C++23 ranges::to)
// C++20 workaround:
std::vector<int> materialized(result.begin(), result.end());

// Common range views
auto evens  = nums | rv::filter([](int n){ return n % 2 == 0; });
auto doubled = nums | rv::transform([](int n){ return n * 2; });
auto first3  = nums | rv::take(3);
auto last3   = nums | rv::drop(nums.size() - 3);
auto reversed = nums | rv::reverse;
auto indexed  = nums | rv::enumerate;   // C++23: yields (index, value) pairs

// zip two ranges (C++23)
std::vector<std::string> names{"a", "b", "c"};
auto pairs = rv::zip(names, nums);  // C++23

// iota: generate range of integers
auto iota5 = rv::iota(0, 5);   // 0, 1, 2, 3, 4

// ranges algorithms (work on entire ranges, no begin/end boilerplate)
std::vector<int> data{5, 3, 1, 4, 2};
std::ranges::sort(data);            // sorts in place
std::ranges::sort(data, std::greater{});  // descending

// Projection: sort by a member/computed key without a full comparator
struct Person { std::string name; int age; };
std::vector<Person> people;
std::ranges::sort(people, {}, &Person::age);     // sort by age
std::ranges::sort(people, {}, &Person::name);    // sort by name
std::ranges::sort(people, std::greater{}, &Person::age);  // descending

12. Algorithms

#include <algorithm>
#include <numeric>
#include <execution>

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

// Searching
auto it = std::find(v.begin(), v.end(), 5);
auto it2 = std::find_if(v.begin(), v.end(), [](int n){ return n > 4; });
bool has5 = std::binary_search(v.begin(), v.end(), 5);  // requires sorted!
int cnt = std::count_if(v.begin(), v.end(), [](int n){ return n % 2 == 0; });

// Sorting
std::sort(v.begin(), v.end());                    // in-place, O(n log n)
std::stable_sort(v.begin(), v.end());             // preserves equal element order
std::partial_sort(v.begin(), v.begin() + 3, v.end()); // first 3 sorted
std::nth_element(v.begin(), v.begin() + 3, v.end()); // O(n), partition not sort

// Transforming
std::vector<int> out(v.size());
std::transform(v.begin(), v.end(), out.begin(), [](int n){ return n * 2; });
// in-place transform:
std::for_each(v.begin(), v.end(), [](int& n){ n *= 2; });

// Reducing
int total = std::accumulate(v.begin(), v.end(), 0);
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<int>{});
// C++17 parallel reductions:
int psum = std::reduce(std::execution::par, v.begin(), v.end());

// Modifying
std::fill(v.begin(), v.end(), 0);
std::iota(v.begin(), v.end(), 1);         // 1, 2, 3, ...
std::reverse(v.begin(), v.end());
std::rotate(v.begin(), v.begin() + 2, v.end()); // rotate left by 2
std::shuffle(v.begin(), v.end(), std::mt19937{std::random_device{}()});

// Remove-erase idiom (pre-C++20)
v.erase(std::remove_if(v.begin(), v.end(), [](int n){ return n % 2 == 0; }),
        v.end());
// C++20: std::erase_if
std::erase_if(v, [](int n){ return n % 2 == 0; });

// Execution policies (C++17) — parallel execution
std::sort(std::execution::par, v.begin(), v.end());         // parallel
std::sort(std::execution::par_unseq, v.begin(), v.end());   // parallel + vectorize
std::for_each(std::execution::seq, v.begin(), v.end(), [](int& n){ n *= 2; });

// min/max
auto [mn, mx] = std::minmax_element(v.begin(), v.end());
int clamped = std::clamp(15, 0, 10);  // 10

// Ranges algorithms (C++20) — accept range directly
std::ranges::sort(v);
std::ranges::unique(v);
auto pos = std::ranges::find(v, 5);

13. Strings & I/O

#include <string>
#include <string_view>
#include <format>   // C++20
#include <print>    // C++23
#include <sstream>
#include <fstream>

// std::string operations
std::string s = "Hello, World!";
s.size();         // 13
s.length();       // same
s.empty();        // false
s.substr(0, 5);   // "Hello" — copy
s.find("World");  // 7 (std::string::npos if not found)
s.rfind('l');     // 10 — last 'l'
s += " Append";
s.insert(5, " there");
s.erase(5, 6);    // remove 6 chars at pos 5
s.replace(7, 5, "C++"); // replace "World" with "C++"

// String building (streams)
std::ostringstream oss;
oss << "Value: " << 42 << ", pi=" << std::fixed << 3.14;
std::string result = oss.str();

// std::string_view (C++17): non-owning, zero-copy view — prefer for read-only params
void log(std::string_view msg) {  // accepts string, string_view, char*, literal
    std::cout << msg << '\n';
}

// CAUTION: string_view can dangle!
std::string_view sv = std::string("temp");  // DANGER: string destroyed, sv dangling

// std::format (C++20): type-safe, fast formatting
std::string formatted = std::format("Name: {}, Score: {:.2f}", "Alice", 99.5);
std::string padded = std::format("{:>10}", "right");  // right-align width 10
std::string hex = std::format("0x{:08X}", 255);       // 0x000000FF

// std::print / std::println (C++23): format directly to stdout
std::println("Hello, {}!", "World");   // with newline
std::print("Value: {}\n", 42);         // without auto newline

// File I/O
std::ifstream in("data.txt");
if (!in) throw std::runtime_error("Cannot open file");
std::string line;
while (std::getline(in, line)) {
    // process line
}

std::ofstream out("output.txt");
out << "Writing: " << 42 << '\n';

// Binary I/O
std::ofstream bin("data.bin", std::ios::binary);
int n = 42;
bin.write(reinterpret_cast<const char*>(&n), sizeof(n));

std::ifstream bin_in("data.bin", std::ios::binary);
int val;
bin_in.read(reinterpret_cast<char*>(&val), sizeof(val));

// String to number
int i = std::stoi("42");
double d = std::stod("3.14");
// Number to string
std::string s2 = std::to_string(42);
// C++17 from_chars / to_chars: locale-independent, no allocation, fastest
#include <charconv>
char buf[32];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), 12345);
int parsed;
auto [ptr2, ec2] = std::from_chars("42abc", "42abc" + 2, parsed); // parsed=42

14. Error Handling

#include <stdexcept>
#include <optional>
#include <variant>
#include <expected>    // C++23

// Exception hierarchy
// std::exception
//   std::logic_error  — programming errors (invalid_argument, out_of_range, etc.)
//   std::runtime_error — runtime conditions (overflow_error, range_error, etc.)

void parse(const std::string& s) {
    if (s.empty()) throw std::invalid_argument("empty string");
    if (s.size() > 100) throw std::length_error("string too long");
}

try {
    parse("");
} catch (const std::invalid_argument& e) {
    std::cerr << "Invalid: " << e.what() << '\n';
} catch (const std::exception& e) {
    std::cerr << "Error: " << e.what() << '\n';
} catch (...) {
    std::cerr << "Unknown exception\n";
    throw;  // re-throw
}

// Custom exceptions
class NetworkError : public std::runtime_error {
public:
    explicit NetworkError(const std::string& msg, int code)
        : std::runtime_error(msg), code_(code) {}
    int code() const noexcept { return code_; }
private:
    int code_;
};

// noexcept: promise not to throw; enables optimizations (e.g. vector move)
void safe_op() noexcept { /* ... */ }
bool might_throw() noexcept(false) { /* ... */ }

// noexcept as operator: evaluates to bool at compile time
static_assert(noexcept(safe_op()));

// std::optional (C++17): value-or-nothing, no heap allocation
std::optional<int> parse_int(std::string_view s) {
    try { return std::stoi(std::string(s)); }
    catch (...) { return std::nullopt; }
}

if (auto val = parse_int("42")) {
    std::cout << *val;
}
int result = parse_int("bad").value_or(0);  // default if empty

// std::variant for sum types / error union
using Result = std::variant<int, std::string>;  // success or error message

Result divide(int a, int b) {
    if (b == 0) return std::string{"division by zero"};
    return a / b;
}

auto r = divide(10, 2);
std::visit([](auto&& v) {
    using T = std::decay_t<decltype(v)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "Result: " << v;
    else
        std::cout << "Error: " << v;
}, r);

// std::expected (C++23): Rust-style Result type
std::expected<int, std::string> safe_divide(int a, int b) {
    if (b == 0) return std::unexpected("division by zero");
    return a / b;
}

auto res = safe_divide(10, 2);
if (res) std::cout << *res;
else std::cout << res.error();

// Chaining with and_then / or_else (monadic ops, C++23)
auto chained = safe_divide(10, 2)
    .and_then([](int v) -> std::expected<double, std::string> {
        return v * 1.5;
    })
    .or_else([](const std::string& e) -> std::expected<double, std::string> {
        return 0.0;  // fallback
    });
Exceptions vs Error Codes
Use exceptions for truly exceptional conditions (file not found, network failure). Use std::optional or std::expected for expected failures in APIs (parse failures, key-not-found). Error codes are appropriate for performance-critical paths or C interop.

15. Concurrency

Threads

#include <thread>
#include <mutex>
#include <shared_mutex>
#include <condition_variable>
#include <future>
#include <atomic>
#include <semaphore>   // C++20
#include <latch>       // C++20
#include <barrier>     // C++20

// std::thread
std::thread t([](int n){ std::cout << "Thread " << n << '\n'; }, 42);
t.join();    // wait for completion
// t.detach(); // detach — thread lives independently (use sparingly)

// std::jthread (C++20): auto-joins on destruction, supports stop tokens
std::jthread worker([](std::stop_token st) {
    while (!st.stop_requested()) {
        // ... do work ...
    }
});
// worker goes out of scope — automatically joins (no need to call join())

Synchronization Primitives

// Mutex and lock guards
std::mutex mtx;
std::vector<int> shared_vec;

void producer() {
    std::lock_guard guard(mtx);     // CTAD (C++17) — deduces mutex type
    shared_vec.push_back(42);
}

// unique_lock: more flexible (deferred lock, timeout, manual lock/unlock)
void consumer() {
    std::unique_lock lock(mtx, std::defer_lock);
    lock.lock();
    // ... process ...
    lock.unlock();
    // ... do other work ...
}

// shared_mutex (C++14): multiple readers, exclusive writer
std::shared_mutex rw_mutex;
std::string shared_data;

void reader() {
    std::shared_lock lock(rw_mutex);  // many can read simultaneously
    std::cout << shared_data;
}

void writer(std::string_view s) {
    std::unique_lock lock(rw_mutex);  // exclusive
    shared_data = s;
}

// Condition variables
std::mutex cv_mtx;
std::condition_variable cv;
bool ready = false;

void wait_for_data() {
    std::unique_lock lock(cv_mtx);
    cv.wait(lock, []{ return ready; });  // spurious wakeup safe
    // ... process when ready ...
}

void set_ready() {
    { std::lock_guard lock(cv_mtx); ready = true; }
    cv.notify_one();   // or notify_all()
}

// C++20 synchronization primitives
std::latch latch(3);                // countdown latch — one-shot
std::barrier barrier(4, []{ /* completion phase */ });  // reusable

std::counting_semaphore<10> sem(5); // max 10, starts at 5
sem.acquire();   // P()
sem.release();   // V()

Async / Future / Promise

// std::async: run function asynchronously
auto future = std::async(std::launch::async, [](int n){
    return n * n;
}, 10);

int result = future.get();  // blocks until ready

// std::promise / std::future: manual signaling
std::promise<int> promise;
std::future<int> fut = promise.get_future();

std::thread t([&promise]{
    // ... do computation ...
    promise.set_value(42);          // or promise.set_exception(...)
});

int val = fut.get();  // blocks
t.join();

// std::packaged_task: wraps a callable, produces a future
std::packaged_task<int(int)> task([](int n){ return n * 2; });
std::future<int> f2 = task.get_future();
std::thread t2(std::move(task), 21);
t2.join();
std::cout << f2.get();  // 42

Atomics & Memory Order

// std::atomic: lock-free operations on fundamental types
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);  // increment
counter++;   // implicit seq_cst (strongest, slowest)

// Memory orders (weakest to strongest):
// relaxed   — no sync, only atomicity (counters OK)
// acquire   — no reads/writes move before this load
// release   — no reads/writes move after this store
// acq_rel   — both acquire and release (for RMW ops like fetch_add)
// seq_cst   — total order across all threads (default, safest)

// Classic spinlock with atomics
class SpinLock {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock()   { while (flag.test_and_set(std::memory_order_acquire)); }
    void unlock() { flag.clear(std::memory_order_release); }
};

// Compare-and-swap
std::atomic<int> val{0};
int expected = 0;
bool swapped = val.compare_exchange_strong(expected, 42);
// If val == 0 (expected): sets val=42, returns true
// If val != 0: sets expected=val, returns false
Data Race = Undefined Behavior
Any unsynchronized access to shared data where at least one thread writes is a data race — instant undefined behavior. Use std::atomic, mutexes, or thread-local storage. Enable TSan (-fsanitize=thread) to catch races.

16. Memory Management

#include <memory>
#include <memory_resource>  // PMR (C++17)
#include <new>

// Stack vs heap
int stack_var = 42;             // stack: automatic storage, ~fast
int* heap_var = new int(42);    // heap: dynamic, must delete
delete heap_var;                // forget this = memory leak

// Prefer smart pointers — never use new/delete directly in modern C++
auto up = std::make_unique<int[]>(100);  // unique_ptr to array

// Placement new: construct in pre-allocated memory
alignas(Widget) std::byte storage[sizeof(Widget)];
Widget* w = new (storage) Widget{};   // construct in-place
w->~Widget();                          // must destroy manually!
// C++20: std::construct_at / std::destroy_at
std::construct_at(reinterpret_cast<Widget*>(storage));
std::destroy_at(reinterpret_cast<Widget*>(storage));

// Alignment
struct alignas(64) CacheLine { char data[64]; };  // 64-byte aligned
static_assert(alignof(CacheLine) == 64);
static_assert(sizeof(CacheLine) == 64);

// Aligned allocation (C++17)
void* p = ::operator new(128, std::align_val_t{64});
::operator delete(p, std::align_val_t{64});

// PMR (Polymorphic Memory Resource, C++17): swap allocators without changing type
std::array<std::byte, 4096> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
std::pmr::vector<int> pmr_vec{&pool};  // allocates from stack buffer
// When pool is exhausted, falls through to upstream (default: new/delete)

// Stack allocator pattern (common in game/systems programming)
// pmr::unsynchronized_pool_resource: fast, non-thread-safe pool
std::pmr::unsynchronized_pool_resource pool2;
std::pmr::map<int, std::pmr::string> fast_map{&pool2};

17. Modern C++ Features

C++17

// Structured bindings — decompose pairs, tuples, structs, arrays
auto [x, y] = std::make_pair(1, 2.0);
auto [key, val] = *my_map.begin();

struct Point3D { double x, y, z; };
auto [a, b, c] = Point3D{1.0, 2.0, 3.0};

// if with init (same for switch)
if (auto it = m.find(key); it != m.end()) {
    // it is in scope here AND in the else branch
    std::cout << it->second;
} // it goes out of scope here

// std::optional
std::optional<std::string> find_user(int id);
auto user = find_user(42);
if (user) std::cout << *user;
user.value_or("anonymous");

// std::variant: type-safe union
std::variant<int, double, std::string> v = "hello";
std::get<std::string>(v);          // "hello"
std::get_if<int>(&v);              // nullptr (holds string)
std::holds_alternative<std::string>(v);  // true

// std::any: type-erased single value
std::any a = 42;
a = std::string{"hello"};          // can reassign to different type
std::any_cast<std::string>(a);     // throws bad_any_cast if wrong type

// Fold expressions in templates (see Templates section)
// Inline variables (see Program Basics section)
// Class template argument deduction (CTAD) — see Templates section

C++20

// Three-way comparison (spaceship operator)
#include <compare>

struct Version {
    int major, minor, patch;
    auto operator<=>(const Version&) const = default;  // compiler generates all 6
};

Version v1{2, 0, 0}, v2{1, 9, 9};
v1 > v2;   // true
v1 == v2;  // false

// Custom spaceship for partial ordering
struct Float {
    double val;
    auto operator<=>(const Float& o) const {
        return val <=> o.val;  // std::partial_ordering (NaN != NaN)
    }
};
// Return types: strong_ordering, weak_ordering, partial_ordering

// Designated initializers (see Classes section)

// Coroutines (stackless, require library support)
#include <coroutine>

// Generator pattern (simplified — typically use a library like cppcoro)
struct Generator {
    struct promise_type {
        int current_val;
        Generator get_return_object() {
            return {std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(int val) {
            current_val = val; return {};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;
    ~Generator() { if (handle) handle.destroy(); }

    bool next() {
        handle.resume();
        return !handle.done();
    }
    int value() const { return handle.promise().current_val; }
};

Generator range(int from, int to) {
    for (int i = from; i < to; ++i)
        co_yield i;
}

// Usage:
auto gen = range(0, 5);
while (gen.next()) std::cout << gen.value() << ' ';  // 0 1 2 3 4

// std::jthread and stop_token (see Concurrency section)
// Concepts (see Templates section)
// Ranges (see Iterators section)
// Modules (see Program Basics section)

// std::format (see Strings section)
// std::bit_cast
uint32_t bits = std::bit_cast<uint32_t>(1.0f);  // type-safe bit reinterpret

C++23

// std::expected (see Error Handling section)

// std::print / std::println
#include <print>
std::println("Hello, {}!", "World");
std::print(stderr, "Error: {}\n", error_msg);

// Deducing this (explicit object parameter)
struct Widget {
    // 'this' becomes an explicit parameter — enables CRTP without CRTP
    template<typename Self>
    auto&& value(this Self&& self) {
        return std::forward<Self>(self).value_;
    }

    // Recursive lambda without std::function
    auto fib = [](this auto self, int n) -> int {
        return n <= 1 ? n : self(n-1) + self(n-2);
    };

private:
    int value_ = 0;
};

// std::ranges::to (materialize ranges)
#include <ranges>
auto evens = std::views::iota(0, 10)
           | std::views::filter([](int n){ return n % 2 == 0; })
           | std::ranges::to<std::vector>();  // C++23

// Multidimensional operator[]
struct Matrix {
    std::vector<double> data;
    int rows, cols;
    double& operator[](int r, int c) { return data[r * cols + c]; }  // C++23
};

Matrix m{.data=std::vector<double>(4*4), .rows=4, .cols=4};
m[1, 2] = 3.14;

// if consteval — detect whether in constant evaluation context
constexpr int compute(int n) {
    if consteval {
        // evaluated at compile time — can use more expensive exact method
        return n * n;
    } else {
        // runtime path
        return n * n;
    }
}

// std::flat_map / std::flat_set: sorted contiguous associative containers
#include <flat_map>
std::flat_map<std::string, int> fm;  // like map but cache-friendly

18. Build Systems & Tooling

CMake

# Minimum CMake project (modern, target-based)
# CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)  # don't allow GNU extensions

# Create an executable target
add_executable(my_app
    src/main.cpp
    src/util.cpp
)

# Set include paths, link libs — always use target_ commands (NOT include_directories)
target_include_directories(my_app PRIVATE include/)
target_compile_options(my_app PRIVATE -Wall -Wextra -Wpedantic)
target_link_libraries(my_app PRIVATE pthread)

# Create a library target
add_library(mylib STATIC src/mylib.cpp)
target_include_directories(mylib PUBLIC include/)   # PUBLIC propagates to consumers

# Find system packages
find_package(Boost 1.80 REQUIRED COMPONENTS filesystem)
target_link_libraries(my_app PRIVATE Boost::filesystem)

# --- Build commands ---
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j$(nproc)
./my_app

# Debug build with sanitizers
cmake .. -DCMAKE_BUILD_TYPE=Debug \
         -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -g"
cmake --build .

Package Managers

# vcpkg (Microsoft) — integrates with CMake
git clone https://github.com/microsoft/vcpkg
./vcpkg/bootstrap-vcpkg.sh
./vcpkg/vcpkg install nlohmann-json fmt boost-filesystem

# CMake integration (toolchain file)
cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake

# Conan 2 — more features, virtual envs
pip install conan
# conanfile.txt
[requires]
fmt/10.1.1
nlohmann_json/3.11.2
[generators]
CMakeDeps
CMakeToolchain

conan install . --output-folder=build --build=missing
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake

Sanitizers & Static Analysis

# AddressSanitizer: heap/stack/global buffer overflows, use-after-free
g++ -fsanitize=address -fno-omit-frame-pointer -g -o app main.cpp
./app   # ASAN will report violations with stack traces

# UndefinedBehaviorSanitizer: signed overflow, null deref, bad casts, etc.
g++ -fsanitize=undefined -g -o app main.cpp

# ThreadSanitizer: data races
g++ -fsanitize=thread -g -o app main.cpp

# MemorySanitizer: uninitialized reads (Clang only)
clang++ -fsanitize=memory -g -o app main.cpp

# Combine ASan + UBSan for dev builds
g++ -fsanitize=address,undefined -g -O1 -o app main.cpp

# clang-tidy: static analysis and linting
clang-tidy src/main.cpp -- -std=c++20 -I include/
# With checks:
clang-tidy --checks='cppcoreguidelines-*,modernize-*,bugprone-*' src/*.cpp

# cppcheck: static analysis (works without compiling)
cppcheck --enable=all --std=c++20 src/

# Valgrind: memory errors (slower but thorough, no recompile needed)
valgrind --leak-check=full ./app

# perf: profiling (Linux)
perf record ./app
perf report

Compile-Time Computation

// constexpr: can be evaluated at compile time
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int f5 = factorial(5);  // computed at compile time: 120
static_assert(f5 == 120);

// C++20: constexpr functions can contain almost anything now
// (loops, local variables, lambdas, try/catch, etc.)
constexpr std::vector<int> make_squares(int n) {
    std::vector<int> v;
    for (int i = 0; i < n; ++i) v.push_back(i * i);
    return v;
}

// consteval: MUST be evaluated at compile time
consteval int must_be_constexpr(int n) { return n * n; }
// int r = must_be_constexpr(x);  // ERROR if x is not constexpr

// constinit (C++20): ensure static-duration variable is zero-initialized at compile time
// Eliminates the "static initialization order fiasco"
constinit int global_counter = 0;  // guaranteed zero-init at compile time
// constinit int bad = some_runtime_func();  // ERROR

19. Common Patterns & Idioms

CRTP — Curiously Recurring Template Pattern

// CRTP: static polymorphism — no virtual, no vtable, resolved at compile time
template<typename Derived>
struct Serializable {
    std::string serialize() const {
        // Calls Derived::to_string() — resolved statically
        return static_cast<const Derived*>(this)->to_string();
    }
};

struct Point : Serializable<Point> {
    double x, y;
    std::string to_string() const {
        return std::format("({}, {})", x, y);
    }
};

// CRTP for operator== / comparison base
template<typename Derived>
struct Comparable {
    bool operator==(const Derived& o) const {
        return static_cast<const Derived*>(this)->equal_to(o);
    }
    bool operator!=(const Derived& o) const { return !(*this == o); }
};

// C++23 alternative: deducing this achieves CRTP without CRTP
struct Base {
    template<typename Self>
    auto clone(this const Self& self) { return Self(self); }
};

Pimpl — Pointer to Implementation

// widget.h — header only knows about the pointer
class Widget {
public:
    Widget();
    ~Widget();
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
    void do_something();
private:
    struct Impl;                    // forward declaration
    std::unique_ptr<Impl> pimpl_;  // opaque pointer
};

// widget.cpp — implementation details hidden
struct Widget::Impl {
    std::vector<int> data;
    std::string name;
    void heavy_computation() { /* ... */ }
};

Widget::Widget() : pimpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;  // must be in .cpp where Impl is complete
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

void Widget::do_something() {
    pimpl_->heavy_computation();
}

// Benefits: ABI stability, reduced compile-time dependencies, faster builds

Type Erasure

// Type erasure: store heterogeneous types behind a uniform interface
// 1. std::function (simplest)
std::function<void(int)> handler;
handler = [](int n){ std::cout << n; };    // lambda
handler = &some_function;                  // function pointer
handler = MyCallable{};                    // any callable

// 2. Manual type erasure with virtual dispatch
struct Drawable {
    virtual void draw() const = 0;
    virtual ~Drawable() = default;
};
// std::vector<std::unique_ptr<Drawable>> shapes;

// 3. std::any — type-safe void*
std::any val = 42;
val = std::string{"hello"};
try {
    auto& s = std::any_cast<std::string&>(val);
} catch (std::bad_any_cast&) { /* ... */ }

// 4. std::variant — closed set of types
using Shape = std::variant<Circle, Rectangle, Triangle>;
std::vector<Shape> shapes;

// Visitor pattern with std::visit
struct DrawVisitor {
    void operator()(const Circle& c) const { /* draw circle */ }
    void operator()(const Rectangle& r) const { /* draw rect */ }
    void operator()(const Triangle& t) const { /* draw tri */ }
};
for (const auto& shape : shapes)
    std::visit(DrawVisitor{}, shape);

// Overloaded helper for inline visitors (common pattern)
template<typename... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
template<typename... Ts> Overloaded(Ts...) -> Overloaded<Ts...>; // C++17 deduction

std::visit(Overloaded{
    [](const Circle&)    { /* ... */ },
    [](const Rectangle&) { /* ... */ },
    [](const Triangle&)  { /* ... */ }
}, shape);

Other Idioms

// Meyer's Singleton (thread-safe since C++11)
class Config {
public:
    static Config& instance() {
        static Config inst;  // initialized exactly once, thread-safe
        return inst;
    }
    // Delete copy/move
    Config(const Config&) = delete;
    Config& operator=(const Config&) = delete;
private:
    Config() = default;
};

// Tag dispatch: select overload at compile time via empty tag types
struct slow_tag {};
struct fast_tag {};

void process(int n, slow_tag) { /* general case */ }
void process(int n, fast_tag) { /* optimized case */ }

template<typename T>
void process_dispatch(T n) {
    if constexpr (std::is_trivially_copyable_v<T>)
        process(n, fast_tag{});
    else
        process(n, slow_tag{});
}

// Builder pattern
class QueryBuilder {
public:
    QueryBuilder& select(std::string_view col)  { cols_.emplace_back(col); return *this; }
    QueryBuilder& from(std::string_view table)  { table_ = table; return *this; }
    QueryBuilder& where(std::string_view cond)  { conds_.emplace_back(cond); return *this; }
    std::string build() const {
        return std::format("SELECT {} FROM {} WHERE {}",
            join(cols_, ", "), table_, join(conds_, " AND "));
    }
private:
    std::vector<std::string> cols_, conds_;
    std::string table_;
    static std::string join(const std::vector<std::string>& v, std::string_view sep);
};

auto q = QueryBuilder{}
    .select("name")
    .select("score")
    .from("users")
    .where("score > 90")
    .build();

20. Common Pitfalls & Gotchas

Dangling References

Dangling references are silent undefined behavior
The compiler won't always warn you. Enable ASan to catch them at runtime.
// DANGER: string_view into a temporary
std::string make_str() { return "hello"; }
std::string_view sv = make_str();  // dangling! string destroyed immediately
std::cout << sv;                   // UB

// DANGER: reference to local returned
int& bad_ref() {
    int local = 42;
    return local;  // UB: returning reference to destroyed local
}

// DANGER: iterator invalidation
std::vector<int> v{1, 2, 3};
auto it = v.begin();
v.push_back(4);    // may reallocate — it is now invalid!
*it = 10;          // UB

// DANGER: reference inside shared_ptr lambda cycle
struct Node {
    std::shared_ptr<Node> next;
    std::function<void()> callback;
};
auto n = std::make_shared<Node>();
n->callback = [n]{ /* captures n — cycle! */ };  // memory leak
// Fix: capture weak_ptr instead
n->callback = [wn = std::weak_ptr(n)]{
    if (auto sn = wn.lock()) { /* ... */ }
};

Object Slicing

struct Base { int x = 1; virtual int val() const { return x; } };
struct Derived : Base { int y = 2; int val() const override { return x + y; } };

Derived d;
Base b = d;            // SLICING: Derived::y lost, vtable lost
b.val();               // calls Base::val() — 1, not 3!

// Always use pointers or references for polymorphism:
Base& br = d;          // correct: virtual dispatch works
br.val();              // 3
Base* bp = &d;         // correct
bp->val();             // 3

Most Vexing Parse

// Classic MVP: looks like variable declaration, is function declaration
Widget w();    // NOT a Widget variable — declares a function returning Widget!
Widget w{};    // correct: value-initialized Widget (use brace init)
Widget w2(Widget());  // declares function taking function returning Widget
Widget w3(Widget{});  // correct: passes Widget{} as argument

// Why: C++ always parses ambiguous constructs as declarations if possible

Undefined Behavior

// Signed integer overflow (unlike unsigned, which wraps)
int x = INT_MAX;
x += 1;          // UB! (compilers assume this never happens)
// Fix: use unsigned, check beforehand, or use __builtin_add_overflow

// Null pointer dereference
int* p = nullptr;
*p = 5;          // UB — segfault on most systems, but not guaranteed

// Accessing out-of-bounds
int arr[3] = {1, 2, 3};
arr[5] = 42;     // UB — may corrupt memory silently

// Unsequenced modifications (order of evaluation not guaranteed)
int i = 0;
i = i++ + ++i;   // UB — multiple unsequenced writes to i

// Data races (see Concurrency section)

// Strict aliasing: can't access object through pointer of unrelated type
int n = 42;
float* fp = reinterpret_cast<float*>(&n);
*fp = 1.5f;      // UB (violates strict aliasing)
// Safe: std::memcpy or std::bit_cast

// Virtual function in constructor/destructor
struct B {
    B() { init(); }                     // calls B::init(), NOT Derived::init()
    virtual void init() { /* ... */ }   // dangerous expectation
};
struct D : B {
    void init() override { /* ... */ }  // not called from B::B()
};

ODR Violations & ABI Issues

// ODR violation: two definitions of same entity, different content
// Very hard to diagnose — symptoms: crashes, wrong behavior, link errors

// In a.cpp:
struct Foo { int x; };          // 4 bytes
// In b.cpp (included different header version):
struct Foo { int x; double y; }; // 12 bytes — silently different!
// Calling functions that use Foo across TUs = UB

// Prevention:
// - Use #pragma once or include guards consistently
// - Never define non-inline functions in headers
// - Use modules (C++20) — they eliminate most ODR issues

// ABI compatibility: changing class layout breaks binary compatibility
// Common ABI-breaking changes:
// - Adding/removing/reordering non-static data members
// - Adding/removing virtual functions (changes vtable)
// - Changing base classes
// - Changing function signatures
//
// If you ship a shared library (.so/.dll), use Pimpl or stable C interface

Iterator Invalidation

ContainerInvalidates iterators when...
vectorAny insert causes reallocation; erase invalidates from erase point
dequeInsert at ends preserves references but invalidates iterators; insert in middle invalidates all
listOnly iterators to erased elements
map/setOnly iterators to erased nodes
unordered_map/setRehash invalidates all; insert may cause rehash
Defensive Coding Tips
  • Enable -Wall -Wextra -Wpedantic and treat warnings as errors (-Werror) in CI
  • Use sanitizers (ASan, UBSan) in debug/test builds
  • Prefer at() over [] in debug builds for bounds checking
  • Use std::span instead of raw pointer + size pairs
  • Compile with multiple compilers (GCC + Clang) to catch non-portable code
  • Run clang-tidy or cppcheck in CI
  • Never ignore compiler warnings about uninitialized variables or sign comparisons