BACK

Modern C++ Programming

Master systems programming with modern C++: from fundamentals to C++23, including templates, concurrency, and advanced metaprogramming.

Official Documentation

February 2026

Contents

Foundations

  • The C++ Philosophy and Evolution
  • Type System and Declarations
  • Functions and Overloading
  • References and Value Categories

Object-Oriented Programming

  • Classes and Encapsulation
  • Operator Overloading
  • Inheritance and Polymorphism

Generic Programming

  • Templates and Generic Programming

Modern C++ Features

  • Smart Pointers and Memory Management
  • Move Semantics and Perfect Forwarding
  • Lambda Expressions

Standard Template Library

  • STL Containers: Sequence Containers
  • STL Containers: Associative Containers
  • STL Algorithms
  • Iterators and Iterator Categories

Advanced Features

  • Exception Handling
  • Namespaces and Name Lookup

Metaprogramming

  • Type Traits and Compile-Time Programming

Concurrency

  • Multithreading Basics
  • Atomic Operations and Memory Ordering
  • Condition Variables and Futures

Modern C++ Features

  • std::optional and std::variant
  • String View and Span
  • Structured Bindings and Init Statements

Standard Library

  • Filesystem Library

Modern C++20/23

  • Concepts and Constraints (C++20)
  • Ranges Library (C++20)
  • Coroutines Basics (C++20)
  • Modules (C++20)
  • Three-Way Comparison (C++20)
  • Designated Initializers (C++20)
  • constexpr and consteval (C++20/23)
  • std::format (C++20)
  • std::expected (C++23)

Tools and Practices

  • CMake and Build Systems
  • Testing with GoogleTest
  • Performance Optimization
  • Memory Management Deep Dive
  • Best Practices and Idioms
  • The C++ Journey: Past, Present, Future

Foundations

Section Detail

The C++ Philosophy and Evolution

Zero-Overhead Abstraction

C++ is built on a fundamental principle: “What you don’t use, you don’t pay for. And what you do use, you couldn’t hand code any better.” This zero-overhead abstraction principle distinguishes C++ from its contemporaries. Unlike Java or C#, which enforce garbage collection overhead, C++ provides high-level abstractions that compile down to machine code equivalent to hand-optimized C.

The language offers three programming paradigms that coexist seamlessly:

  1. Procedural: Direct manipulation of data structures and algorithms
  2. Object-Oriented: Classes, inheritance, polymorphism, encapsulation
  3. Generic: Templates and compile-time metaprogramming

The Evolution of C++

C++ has evolved dramatically since Bjarne Stroustrup’s original design in 1979. Understanding this evolution is crucial for reading existing codebases and writing idiomatic modern code.

StandardYearKey Features
C++98/031998/2003STL, templates, exceptions, namespaces
C++112011Auto, lambdas, smart pointers, move semantics, concurrency
C++142014Generic lambdas, return type deduction, binary literals
C++172017Structured bindings, std::optional, filesystem, parallel algorithms
C++202020Concepts, ranges, coroutines, modules, three-way comparison
C++232023std::expected, monadic operations, multidimensional subscript

The Compilation Model

Unlike interpreted languages, C++ undergoes a multi-stage compilation process that enables aggressive optimization.

System Diagram
SourcePipelinemain.cppheader.hppPreprocessorCompiler FrontendOptimizerCode GeneratorAssemblerLinkerStandard LibraryExecutableTranslation UnitAbstract Syntax TreeIntermediate RepresentationAssemblyObject File (.o)

Name Mangling and the One Definition Rule

C++ supports function overloading, templates, and namespaces, which requires name mangling: the compiler transforms human-readable names into unique symbols. For example, void foo(int) might become _Z3fooi in the object file.

The One Definition Rule (ODR) states that every entity can have only one definition across all translation units. Violating ODR results in undefined behavior, though linkers often fail to detect violations.

Type System and Static Polymorphism

C++ is statically typed with strong type checking at compile time. However, it also provides mechanisms for compile-time polymorphism through templates, eliminating runtime dispatch overhead.

// Runtime polymorphism (virtual dispatch)
class Animal { virtual void speak() = 0; };
class Dog : public Animal { void speak() override; };

// Compile-time polymorphism (template instantiation)
template<typename T>
void process(T& obj) { obj.speak(); }

The template version generates specialized code for each type, enabling inlining and optimization impossible with virtual functions.

Memory Model

C++ provides direct control over memory layout and lifetime. Unlike garbage-collected languages, C++ uses deterministic destruction through RAII (Resource Acquisition Is Initialization): resources are tied to object lifetimes.

Storage DurationLifetimeExample
AutomaticScope-basedLocal variables
StaticProgram durationGlobal variables, static locals
DynamicManual (new/delete)Heap allocations
ThreadThread durationthread_local variables

Modern C++ heavily favors automatic storage with smart pointers (unique_ptr, shared_ptr) managing dynamic memory automatically.

Interactive Lab

The Standard Entry Point

#include <iostream>

int () {
    std::cout << "Modern C++ initialized\n";
    return 0;
}

Undefined Behavior and the Abstract Machine

Like C, C++ defines behavior in terms of an abstract machine. Operations outside the specification result in undefined behavior (UB), where the compiler may assume such cases never occur and optimize accordingly.

Common sources of UB:

  • Dereferencing null or dangling pointers
  • Data races (concurrent modification without synchronization)
  • Signed integer overflow
  • Accessing objects after their lifetime ends

Modern compilers include sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer) to detect UB at runtime during development.

Runtime Environment

Interactive Lab

1#include <iostream>
2 
3int main() {
4 // Modern C++ with type inference and range-based iteration
5 auto message = "C++ Abstract Machine Ready";
6 std::cout << message << '\n';
7 return 0;
8}
System Console

Waiting for signal...

Section Detail

Type System and Declarations

Fundamental Types and Type Categories

C++ provides a rich type system that balances low-level control with high-level abstractions. Every type belongs to one of several categories, each with distinct properties regarding copying, lifetime, and memory representation.

Built-in Fundamental Types

CategoryTypesGuaranteed Properties
Booleanbooltrue or false, implementation-defined size
Characterchar, wchar_t, char8_t, char16_t, char32_tCharacter encoding representation
Integershort, int, long, long longMinimum sizes specified
Floating-Pointfloat, double, long doubleIEEE 754 typical but not mandated
VoidvoidIncomplete type, used for “no value”

C++ guarantees size relationships: sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long). However, exact sizes are implementation-defined. Use <cstdint> for fixed-width types: int32_t, uint64_t, etc.

Type Deduction with auto

C++11 introduced auto for automatic type deduction, reducing verbosity while maintaining static typing. The compiler deduces types from initializers at compile time—there’s no runtime overhead.

auto x = 42;              // int
auto y = 3.14;            // double
auto z = "hello";         // const char*
auto ptr = new int{5};    // int*

// Complex types become manageable
auto it = vec.begin();    // std::vector<T>::iterator
auto lambda = [](int x) { return x * 2; };  // Closure type

Type Deduction Rules

The deduction follows template argument deduction rules with some exceptions:

  • Reference collapse: auto& preserves lvalue references
  • const stripping: auto removes top-level const (use const auto)
  • Array decay: Arrays decay to pointers unless auto&
Interactive Lab

Auto Type Deduction

int arr[5] = {1,2,3,4,5};
 p = arr;  // Deduces to int*
auto& r = arr;  // Deduces to int(&)[5]

decltype: Inspecting Expression Types

While auto deduces from initializers, decltype queries the type of any expression without evaluating it.

int x = 10;
decltype(x) y = 20;        // y is int

int& getRef();
decltype(getRef()) z = x;  // z is int&

// Combining auto and decltype (C++14 return type deduction)
auto func(int x) -> decltype(x * 2) {
    return x * 2;  // Return type is int
}

C++14 added decltype(auto) for perfect forwarding of return types:

template<typename Container>
decltype(auto) getElement(Container& c, size_t i) {
    return c[i];  // Preserves reference if container returns reference
}

Const Correctness

The const qualifier is fundamental to C++‘s type system, enabling the compiler to enforce immutability guarantees and enable optimizations.

Const Variables

const int max_retries = 3;  // Cannot be modified
// max_retries = 5;  // Compile error

int const alt_syntax = 5;  // Equivalent to const int

Const Pointers vs Pointer-to-Const

The placement of const determines what is immutable:

int value = 10;

const int* ptr1 = &value;  // Pointer to const int (can't modify *ptr1)
int* const ptr2 = &value;  // Const pointer to int (can't modify ptr2)
const int* const ptr3 = &value;  // Both const

*ptr1 = 20;  // Error: can't modify through ptr1
ptr1 = nullptr;  // OK: pointer itself is mutable

*ptr2 = 20;  // OK: can modify through ptr2
ptr2 = nullptr;  // Error: pointer itself is const

Mnemonic: Read right-to-left. const int* is “pointer to const int.”

Const Member Functions

Member functions can be marked const, promising not to modify object state:

class Point {
    int x_, y_;
public:
    int getX() const { return x_; }  // Doesn't modify object
    void setX(int x) { x_ = x; }     // Modifies object
};

const Point p{10, 20};
p.getX();  // OK
// p.setX(5);  // Error: can't call non-const method on const object

Type Aliases and Type Identity

C++11 introduced using for type aliases, superseding typedef:

// Old style (C++98)
typedef std::vector<int> IntVector;
typedef void (*FunctionPtr)(int);

// Modern style (C++11+)
using IntVector = std::vector<int>;
using FunctionPtr = void(*)(int);

// Template aliases (only possible with using)
template<typename T>
using Vec = std::vector<T>;

Vec<int> v;  // Equivalent to std::vector<int>

Signed vs Unsigned: A Design Choice

Integer types can be signed or unsigned. Mixing them in expressions causes implicit conversions that can be surprising:

int signed_val = -1;
unsigned int unsigned_val = 1;

if (signed_val < unsigned_val) {
    // This branch is NOT taken!
    // -1 converts to a large unsigned value
}

Best Practice: Prefer signed types for arithmetic, use unsigned only for bit manipulation or when the domain is truly non-negative (e.g., array indices—though even this is debated).

Conceptual Check

What is the type of `x` in: `const auto& x = 42;`?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3 
4int main() {
5 const auto size = 10;
6 std::vector<int> vec(size, 42);
7
8 // auto deduces iterator type
9 auto it = vec.cbegin();
10
11 // decltype preserves exact type
12 decltype(vec[0]) first = vec[0]; // int&
13
14 std::cout << "Type deduction: " << *it << ", " << first << '\n';
15 return 0;
16}
System Console

Waiting for signal...

Section Detail

Functions and Overloading

Function Overloading and Resolution

Unlike C, C++ allows multiple functions with the same name but different parameter lists. The compiler selects the appropriate overload through overload resolution at compile time based on argument types.

void print(int x) { std::cout << "int: " << x; }
void print(double x) { std::cout << "double: " << x; }
void print(const char* x) { std::cout << "string: " << x; }

print(42);      // Calls print(int)
print(3.14);    // Calls print(double)
print("hello"); // Calls print(const char*)

Overload Resolution Rules

The compiler follows a precise algorithm:

  1. Exact match: Argument type matches parameter exactly
  2. Promotion: Integral promotions (charint) or floatdouble
  3. Standard conversion: Numeric conversions, pointer conversions
  4. User-defined conversion: Via conversion operators or constructors
  5. Ellipsis match: Variadic functions (least preferred)

If multiple functions match at the same level, the call is ambiguous and compilation fails.

void process(int x) { }
void process(double x) { }

process(42L);  // Error: ambiguous (long can convert to int or double)
Interactive Lab

Overload Resolution

void func(int) { }
void func(double) { }
void func(const char*) { }

func();  // Calls func(int) - exact match

Default Arguments

Functions can specify default values for trailing parameters. Defaults are evaluated at call site, not function definition.

void configure(int timeout = 30, bool verbose = false);

configure();           // configure(30, false)
configure(60);         // configure(60, false)
configure(60, true);   // configure(60, true)
// configure(, true);  // Error: can't skip non-trailing arguments

Best Practice: Declare defaults in header files (declarations), not source files (definitions). This ensures callers see the defaults.

// header.hpp
void setup(int port = 8080);

// source.cpp
void setup(int port) {  // No default here
    // implementation
}

Inline Functions and ODR

The inline keyword suggests the compiler should substitute the function body at call sites, eliminating call overhead. Modern compilers ignore this hint and inline based on optimization heuristics.

However, inline has a critical semantic meaning: it relaxes the One Definition Rule, allowing identical definitions in multiple translation units.

// header.hpp
inline int square(int x) {
    return x * x;
}
// Can be included in multiple .cpp files without linker errors

Modern Usage: inline is essential for defining functions in headers, especially templates and constexpr functions (implicitly inline).

Function Templates

Templates enable generic programming by allowing functions to operate on any type satisfying certain requirements.

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

auto x = max(10, 20);        // T = int
auto y = max(3.14, 2.71);    // T = double
auto z = max<long>(5, 10);   // Explicit T = long

Template Argument Deduction

The compiler deduces template parameters from function arguments. Deduction fails if types are ambiguous:

template<typename T>
void process(T a, T b) { }

process(10, 20);     // OK: T = int
process(10, 3.14);   // Error: T is int or double?
process<double>(10, 3.14);  // OK: explicit T = double

Template Specialization

You can provide specialized implementations for specific types:

template<typename T>
T zero() { return T{}; }

template<>
const char* zero<const char*>() { return ""; }

Trailing Return Types

C++11 allows return type after parameter list, useful when return type depends on parameters:

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

C++14 simplified this with return type deduction:

template<typename T, typename U>
auto add(T t, U u) {
    return t + u;  // Type deduced from return statement
}

constexpr Functions

Functions marked constexpr can execute at compile time if arguments are constant expressions:

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

constexpr auto result = factorial(5);  // Computed at compile time
int arr[factorial(4)];  // Array size must be compile-time constant

C++14 relaxed restrictions, allowing loops and multiple statements in constexpr functions.

Conceptual Check

What happens when `inline` is applied to a function?

Runtime Environment

Interactive Lab

1#include <iostream>
2 
3template<typename T>
4constexpr T cube(T x) {
5 return x * x * x;
6}
7 
8int main() {
9 constexpr auto compile_time = cube(3); // Computed during compilation
10 auto runtime = cube(5); // Can also run at runtime
11
12 std::cout << "Compile-time: " << compile_time << ", Runtime: " << runtime << '\n';
13 return 0;
14}
System Console

Waiting for signal...

Section Detail

References and Value Categories

References: Aliases with Semantics

References are aliases to existing objects, unlike pointers which are objects themselves. C++ provides two kinds: lvalue references (T&) and rvalue references (T&&, C++11).

int x = 10;
int& ref = x;   // Lvalue reference to x
ref = 20;       // Modifies x

int* ptr = &x;  // Pointer is a separate object
*ptr = 30;      // Modifies x through pointer

Key Properties of References

  1. Must be initialized: Unlike pointers, references cannot be null or uninitialized
  2. Cannot be rebound: Once bound, a reference always refers to the same object
  3. No separate storage: References typically compile to pointers but have different semantics
  4. No reference to reference: int& & is illegal (but see reference collapsing)
int a = 5;
int& r1 = a;
// int& r2;     // Error: must be initialized
// r1 = b;      // This assigns b's value to a, doesn't rebind r1

Value Categories: lvalues and rvalues

Every C++ expression belongs to a value category. The fundamental distinction:

  • lvalue: Has persistent identity (name, memory address)
  • rvalue: Temporary, about to be destroyed
int x = 10;    // x is an lvalue
int* p = &x;   // OK: can take address of lvalue

// int* p = &10;  // Error: can't take address of rvalue
// int& r = 42;   // Error: lvalue ref can't bind to rvalue

C++11 introduced finer categories:

System Diagram
expressionglvaluervaluelvalueprvaluexvalueHas identitye.g., variable namesPure rvaluee.g., literals, temporarieseXpiring valuee.g., std::move result

Rvalue References and Move Semantics

Rvalue references (T&&) bind to temporaries, enabling move semantics: transferring resources instead of copying.

std::string s1 = "hello";
std::string s2 = s1;              // Copy: s1 still valid
std::string s3 = std::move(s1);   // Move: s1 left in valid but unspecified state

void process(std::string&& s) {
    // s is an rvalue reference (but s itself is an lvalue!)
}

process(std::string("temp"));     // OK: binds to temporary
process(std::move(s2));           // OK: std::move casts to rvalue
// process(s2);                   // Error: lvalue can't bind to &&

The lvalue-rvalue Paradox

Inside a function, an rvalue reference parameter is itself an lvalue (it has a name):

void func(std::string&& s) {
    std::string copy = s;            // Copy, not move (s is lvalue)
    std::string moved = std::move(s); // Move (explicitly cast to rvalue)
}

Const lvalue References: Universal Binding

A const T& can bind to both lvalues and rvalues, making it perfect for read-only parameters:

void print(const std::string& s) {  // Accepts anything
    std::cout << s;
}

std::string str = "hello";
print(str);              // OK: binds to lvalue
print("world");          // OK: binds to temporary
print(str + " world");   // OK: binds to temporary result

This is why pre-C++11 code used const T& everywhere: it’s the only reference type that accepts both lvalues and rvalues.

Reference Collapsing Rules

When references are formed through templates or typedef, C++ applies reference collapsing:

T& &   → T&
T& &&  → T&
T&& &  → T&
T&& && → T&&

Rule: An rvalue reference to an rvalue reference becomes rvalue reference; everything else becomes lvalue reference.

template<typename T>
void wrapper(T&& arg) {  // Forwarding reference (see next section)
    // If T is int&, then T&& becomes int& (reference collapsing)
    // If T is int, then T&& is int&&
}

Forwarding References (Universal References)

In template contexts, T&& has special meaning—it’s a forwarding reference that can bind to anything:

template<typename T>
void forward_wrapper(T&& arg) {  // Forwarding reference
    target(std::forward<T>(arg));  // Perfect forwarding
}

int x = 10;
forward_wrapper(x);        // T = int&, arg is int&
forward_wrapper(10);       // T = int, arg is int&&

std::forward<T> preserves the value category: forwards lvalues as lvalues, rvalues as rvalues.

Interactive Lab

Reference Semantics

int x = 5;
int& r = x;
r = 10;
std::cout << ;  // Outputs 10
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <utility>
3 
4void process(int& x) { std::cout << "lvalue\n"; }
5void process(int&& x) { std::cout << "rvalue\n"; }
6 
7int main() {
8 int a = 10;
9 process(a); // Calls process(int&)
10 process(42); // Calls process(int&&)
11 process(std::move(a)); // Calls process(int&&)
12 return 0;
13}
System Console

Waiting for signal...

Object-Oriented Programming

Section Detail

Classes and Encapsulation

Classes: User-Defined Types

Classes are the foundation of object-oriented programming in C++. They combine data (members) and behavior (methods) into cohesive types with controlled access.

class Point {
private:
    double x_, y_;  // Data members (by convention, trailing underscore)
    
public:
    Point(double x, double y) : x_(x), y_(y) {}  // Constructor
    
    double distance() const {  // Const member function
        return std::sqrt(x_ * x_ + y_ * y_);
    }
    
    void setX(double x) { x_ = x; }  // Mutator
    double getX() const { return x_; }  // Accessor
};

Access Specifiers

C++ provides three access levels:

  • private: Accessible only within the class
  • protected: Accessible within the class and derived classes
  • public: Accessible everywhere

Default access: class defaults to private, struct defaults to public.

class MyClass {
    int private_by_default;
public:
    int explicitly_public;
};

struct MyStruct {
    int public_by_default;
private:
    int explicitly_private;
};

Convention: Use class for types with invariants requiring encapsulation; use struct for passive data containers (POD types).

Constructors and Initialization

Constructors initialize objects. C++ provides several kinds:

Default Constructor

class Widget {
public:
    Widget() : value_(0) {}  // Default constructor
private:
    int value_;
};

Widget w;  // Calls default constructor

Parameterized Constructor

class Point {
public:
    Point(double x, double y) : x_(x), y_(y) {}
private:
    double x_, y_;
};

Point p(3.0, 4.0);  // Direct initialization
Point q = {5.0, 6.0};  // Aggregate initialization (C++11)

Member Initializer Lists

Always prefer initializer lists over assignment in constructor body:

class Container {
    std::string name_;
    std::vector<int> data_;
public:
    // Good: Direct initialization
    Container(std::string n) : name_(n), data_(100) {}
    
    // Bad: Default construction then assignment
    // Container(std::string n) { name_ = n; data_.resize(100); }
};

Reasons:

  1. Initialization is generally more efficient than assignment
  2. Const and reference members must use initializer lists
  3. Base classes and members without default constructors require it

Delegating Constructors (C++11)

Constructors can delegate to other constructors:

class Rectangle {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    Rectangle() : Rectangle(0, 0) {}  // Delegates to above
};

Destructors and RAII

Destructors execute when objects are destroyed (scope exit, delete, exception). They’re the foundation of RAII (Resource Acquisition Is Initialization): tying resource lifetime to object lifetime.

class FileHandle {
    FILE* file_;
public:
    FileHandle(const char* path) : file_(fopen(path, "r")) {
        if (!file_) throw std::runtime_error("Failed to open");
    }
    
    ~FileHandle() {
        if (file_) fclose(file_);  // Always cleanup
    }
    
    // Prevent copying (for now)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};

void process() {
    FileHandle f("data.txt");
    // Use file...
}  // Destructor automatically closes file, even if exceptions thrown

Special Member Functions

The compiler auto-generates certain functions if not user-declared:

  1. Default constructor (if no constructors declared)
  2. Destructor
  3. Copy constructor: T(const T&)
  4. Copy assignment: T& operator=(const T&)
  5. Move constructor: T(T&&) (C++11)
  6. Move assignment: T& operator=(T&&) (C++11)

The Rule of Zero

Best Practice: If you can, don’t declare any of the special members. Let the compiler generate them. Use standard library types that manage resources correctly.

class Good {
    std::string name_;
    std::vector<int> data_;
    // Compiler-generated special members are correct
};

The Rule of Three/Five

If you declare any of destructor, copy constructor, or copy assignment, you should probably declare all three (Rule of Three). In modern C++, also declare move constructor and move assignment (Rule of Five).

class Buffer {
    char* data_;
    size_t size_;
public:
    // Constructor
    Buffer(size_t s) : data_(new char[s]), size_(s) {}
    
    // Destructor
    ~Buffer() { delete[] data_; }
    
    // 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_;
            size_ = other.size_;
            data_ = new char[size_];
            std::copy(other.data_, other.data_ + size_, data_);
        }
        return *this;
    }
    
    // Move constructor
    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;
    }
};

However, prefer std::vector<char> and Rule of Zero!

Conceptual Check

Why prefer member initializer lists over assignment in constructor bodies?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <string>
3 
4class Person {
5 std::string name_;
6 int age_;
7public:
8 Person(std::string n, int a) : name_(std::move(n)), age_(a) {}
9
10 void display() const {
11 std::cout << name_ << " is " << age_ << " years old\n";
12 }
13};
14 
15int main() {
16 Person p("Alice", 30);
17 p.display();
18 return 0;
19}
System Console

Waiting for signal...

Section Detail

Operator Overloading

Operator Overloading: Custom Type Semantics

C++ allows redefining operators for user-defined types, enabling natural syntax for custom abstractions. Operators are functions with special names: operator+, operator[], operator<<, etc.

class Complex {
    double real_, imag_;
public:
    Complex(double r = 0, double i = 0) : real_(r), imag_(i) {}
    
    // Member operator
    Complex operator+(const Complex& other) const {
        return Complex(real_ + other.real_, imag_ + other.imag_);
    }
    
    Complex& operator+=(const Complex& other) {
        real_ += other.real_;
        imag_ += other.imag_;
        return *this;
    }
};

Complex a(1, 2), b(3, 4);
Complex c = a + b;  // Calls a.operator+(b)

Member vs Non-Member Operators

Member operators: Left operand is *this

class Vector {
public:
    Vector operator+(const Vector& rhs) const;  // this + rhs
};

Non-member operators: Both operands are parameters

Vector operator+(const Vector& lhs, const Vector& rhs);

When to use each:

  • Member: Unary operators (-, ++, *), compound assignment (+=, *=), subscript ([]), call (()), member access (->)
  • Non-member: Symmetric binary operators (+, -, *), stream operators (<<, >>)
  • Friend: Non-member needing private access
class Rational {
    int num_, den_;
    friend Rational operator+(const Rational& lhs, const Rational& rhs);
public:
    Rational(int n, int d = 1) : num_(n), den_(d) {}
};

// Non-member allows: 2 + Rational(3,4) via implicit conversion
Rational operator+(const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.num_ * rhs.den_ + rhs.num_ * lhs.den_,
                    lhs.den_ * rhs.den_);
}

Canonical Operator Patterns

Arithmetic Operators

Implement compound assignment (+=), then derive binary operator (+):

class Number {
    int value_;
public:
    Number& operator+=(const Number& rhs) {
        value_ += rhs.value_;
        return *this;
    }
};

// Non-member, defined in terms of +=
Number operator+(Number lhs, const Number& rhs) {
    lhs += rhs;  // lhs is a copy, so modify it
    return lhs;
}

Comparison Operators

C++20 introduced the spaceship operator (<=>), but pre-C++20:

class Point {
    int x_, y_;
public:
    bool operator==(const Point& other) const {
        return x_ == other.x_ && y_ == other.y_;
    }
    
    bool operator!=(const Point& other) const {
        return !(*this == other);  // Implement in terms of ==
    }
    
    bool operator<(const Point& other) const {
        return (x_ < other.x_) || (x_ == other.x_ && y_ < other.y_);
    }
    
    // Define >, <=, >= in terms of < and ==
};

C++20 spaceship simplifies this:

class Point {
    int x_, y_;
public:
    auto operator<=>(const Point&) const = default;  // Generates all comparisons
};

Increment/Decrement

Prefix returns reference; postfix returns copy:

class Counter {
    int count_;
public:
    // Prefix: ++c
    Counter& operator++() {
        ++count_;
        return *this;
    }
    
    // Postfix: c++
    Counter operator++(int) {  // Dummy int parameter
        Counter temp = *this;
        ++(*this);  // Use prefix version
        return temp;
    }
};

Subscript Operator

class Array {
    int* data_;
    size_t size_;
public:
    int& operator[](size_t index) {
        return data_[index];
    }
    
    const int& operator[](size_t index) const {
        return data_[index];
    }
};

C++23 allows multidimensional subscript:

class Matrix {
public:
    double& operator[](size_t row, size_t col);  // C++23
};

Stream Insertion/Extraction

Always non-member to allow std::cout << obj:

class Person {
    std::string name_;
    int age_;
    friend std::ostream& operator<<(std::ostream&, const Person&);
public:
    Person(std::string n, int a) : name_(n), age_(a) {}
};

std::ostream& operator<<(std::ostream& os, const Person& p) {
    return os << p.name_ << " (" << p.age_ << ")";
}

Conversion Operators

Allow implicit or explicit conversion to other types:

class Fraction {
    int num_, den_;
public:
    // Implicit conversion to double
    operator double() const {
        return static_cast<double>(num_) / den_;
    }
    
    // Explicit conversion to bool (C++11)
    explicit operator bool() const {
        return num_ != 0;
    }
};

Fraction f(3, 4);
double d = f;  // Implicit: calls operator double()
// bool b = f;  // Error: explicit conversion required
bool b = static_cast<bool>(f);  // OK
if (f) { }  // OK: contextual conversion to bool
Conceptual Check

Why implement operator+ as non-member in terms of operator+=?

Runtime Environment

Interactive Lab

1#include <iostream>
2 
3class Vector2D {
4 double x_, y_;
5public:
6 Vector2D(double x, double y) : x_(x), y_(y) {}
7
8 Vector2D& operator+=(const Vector2D& rhs) {
9 x_ += rhs.x_;
10 y_ += rhs.y_;
11 return *this;
12 }
13
14 friend Vector2D operator+(Vector2D lhs, const Vector2D& rhs) {
15 return lhs += rhs;
16 }
17
18 friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
19 return os << "(" << v.x_ << ", " << v.y_ << ")";
20 }
21};
22 
23int main() {
24 Vector2D v1(1, 2), v2(3, 4);
25 std::cout << v1 + v2 << '\n';
26 return 0;
27}
System Console

Waiting for signal...

Section Detail

Inheritance and Polymorphism

Inheritance: The Is-A Relationship

Inheritance models taxonomic relationships where a derived class “is a” specialized version of a base class. C++ supports single and multiple inheritance with three access modes.

class Shape {
protected:
    std::string color_;
public:
    Shape(std::string c) : color_(c) {}
    virtual double area() const = 0;  // Pure virtual
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double radius_;
public:
    Circle(std::string c, double r) : Shape(c), radius_(r) {}
    
    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
};

Inheritance Access Modes

DerivationBase public → DerivedBase protected → DerivedBase private → Derived
publicpublicprotectedinaccessible
protectedprotectedprotectedinaccessible
privateprivateprivateinaccessible

Public inheritance models “is-a”: Circle is-a Shape. Most common. Protected inheritance models “implemented-in-terms-of” (rare). Private inheritance models “implemented-in-terms-of” (prefer composition).

class Stack : private std::vector<int> {  // Private inheritance
public:
    void push(int x) { push_back(x); }
    int pop() { int x = back(); pop_back(); return x; }
};
// Stack is NOT a std::vector publicly

Virtual Functions and Dynamic Dispatch

Virtual functions enable runtime polymorphism: the called function depends on the dynamic type of the object, not the static type of the pointer/reference.

class Animal {
public:
    virtual void speak() const {
        std::cout << "Some sound\\n";
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() const override {  // override keyword: C++11
        std::cout << "Woof!\\n";
    }
};

void makeNoise(const Animal& a) {
    a.speak();  // Dynamic dispatch
}

Dog d;
makeNoise(d);  // Outputs: Woof!

The Virtual Table Mechanism

Virtual functions are implemented via a virtual table (vtable): a compile-time array of function pointers for each polymorphic class. Each object contains a hidden vptr pointing to its class’s vtable.

System Diagram
Dog Objectvptr →data members...Dog vtable[0]: Dog::speak()[1]: Dog::~Dog()

Cost: One pointer per object, one indirection per virtual call. This is the “overhead” of polymorphism.

Abstract Classes and Pure Virtual Functions

A pure virtual function has no implementation and is declared with = 0. Classes with pure virtuals are abstract: cannot be instantiated.

class Drawable {
public:
    virtual void draw() const = 0;  // Pure virtual
    virtual ~Drawable() = default;
};

class Rectangle : public Drawable {
public:
    void draw() const override {
        // Implementation
    }
};

// Drawable d;  // Error: cannot instantiate abstract class
Rectangle r;    // OK: Rectangle is concrete

Interface idiom: Abstract class with only pure virtuals and no data members.

The Override and Final Specifiers

C++11 added override and final to catch errors:

class Base {
public:
    virtual void foo(int x);
    virtual void bar() const;
};

class Derived : public Base {
public:
    void foo(double x) override;  // Error: doesn't override (different signature)
    void bar() override;          // Error: doesn't override (missing const)
};

final prevents further overriding or derivation:

class Base {
public:
    virtual void method() final;  // Cannot be overridden
};

class Derived final : public Base {  // Cannot be further derived
};

Virtual Destructors: A Critical Rule

If a class has virtual functions, its destructor must be virtual. Otherwise, deleting a derived object through a base pointer causes undefined behavior:

class Base {
public:
    ~Base() { std::cout << "Base destroyed\\n"; }
};

class Derived : public Base {
    int* data_;
public:
    Derived() : data_(new int[100]) {}
    ~Derived() { delete[] data_; }  // CRITICAL: must run
};

Base* p = new Derived;
delete p;  // UB: Only Base destructor runs, memory leak!

Solution: Make base destructor virtual:

class Base {
public:
    virtual ~Base() { }  // Virtual destructor
};

Slicing Problem

Assigning a derived object to a base object slices off the derived parts:

class Base { int x_; };
class Derived : public Base { int y_; };

Derived d;
Base b = d;  // Slicing: y_ is lost

Base& ref = d;  // OK: Reference preserves dynamic type
Base* ptr = &d;  // OK: Pointer preserves dynamic type

Rule: Pass polymorphic types by pointer or reference, never by value.

Conceptual Check

What happens if a base class destructor is NOT virtual?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <memory>
3 
4class Animal {
5public:
6 virtual void speak() const { std::cout << "...\n"; }
7 virtual ~Animal() = default;
8};
9 
10class Cat : public Animal {
11public:
12 void speak() const override { std::cout << "Meow!\n"; }
13};
14 
15void communicate(const Animal& a) {
16 a.speak(); // Dynamic dispatch
17}
18 
19int main() {
20 Cat c;
21 communicate(c);
22 return 0;
23}
System Console

Waiting for signal...

Generic Programming

Section Detail

Templates and Generic Programming

Templates: Compile-Time Polymorphism

Templates enable writing code that works with any type satisfying certain requirements, with full type checking at compile time and zero runtime overhead. Unlike runtime polymorphism (virtual functions), templates generate specialized code for each type used.

Function Templates

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

auto x = max(10, 20);         // T = int
auto y = max(3.14, 2.71);     // T = double
auto z = max<long>(5, 10);    // Explicit T = long

Template Argument Deduction

The compiler deduces template parameters from function arguments. Deduction follows strict rules:

template<typename T>
void process(T value, T* ptr);

int x = 5;
process(x, &x);  // OK: T = int

double d = 3.14;
// process(x, &d);  // Error: T is int or double?

Class Templates

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

Stack<int> intStack;
Stack<std::string> stringStack;

Member Function Templates

Templates can appear at multiple levels:

template<typename T>
class Container {
public:
    // Member function template
    template<typename U>
    void insert(U&& value) {
        // ...
    }
};

Container<int> c;
c.insert(42);       // U = int
c.insert("hello");  // U = const char*

Template Specialization

Full Specialization

Provide a completely different implementation for a specific type:

template<typename T>
class TypeName {
public:
    static const char* get() { return "unknown"; }
};

template<>
class TypeName<int> {
public:
    static const char* get() { return "int"; }
};

template<>
class TypeName<double> {
public:
    static const char* get() { return "double"; }
};

Partial Specialization (Class Templates Only)

Specialize for a subset of template parameters:

template<typename T, typename U>
class Pair {
    T first_;
    U second_;
};

// Partial specialization: both types the same
template<typename T>
class Pair<T, T> {
    T first_, second_;
public:
    bool equal() const { return first_ == second_; }
};

// Partial specialization: pointer types
template<typename T>
class Pair<T*, T*> {
    // Special implementation for pointers
};

Non-Type Template Parameters

Templates can take compile-time constant values:

template<typename T, size_t N>
class Array {
    T data_[N];
public:
    constexpr size_t size() const { return N; }
    
    T& operator[](size_t i) { return data_[i]; }
    const T& operator[](size_t i) const { return data_[i]; }
};

Array<int, 10> arr1;      // Different type from...
Array<int, 20> arr2;      // ...this

Non-type parameters can be: integers, enums, pointers, references, nullptr_t, and (C++20) floating-point and class types.

Variadic Templates (C++11)

Templates accepting arbitrary numbers of arguments:

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

print(1, 2.5, "hello", 'x');  // Any number/types of arguments

Parameter Pack Expansion

template<typename... Ts>
class Tuple;  // Declaration

// Recursive inheritance
template<typename T, typename... Ts>
class Tuple<T, Ts...> : public Tuple<Ts...> {
    T value_;
public:
    Tuple(T v, Ts... vs) : Tuple<Ts...>(vs...), value_(v) {}
};

template<>
class Tuple<> {  // Base case
};

SFINAE: Substitution Failure Is Not An Error

When template substitution fails, the candidate is removed from overload set instead of causing error:

template<typename T>
typename T::value_type getValue(T container) {  // Only if T has value_type
    return container[0];
}

template<typename T>
T getValue(T value) {  // Fallback
    return value;
}

std::vector<int> vec{1, 2, 3};
auto x = getValue(vec);  // Calls first overload
auto y = getValue(42);   // Calls second overload (first SFINAE'd out)

Modern C++ uses std::enable_if for SFINAE:

template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
increment(T value) {
    return value + 1;
}

increment(10);    // OK
// increment(3.14);  // Error: no matching function

C++20 concepts provide cleaner syntax:

template<std::integral T>
T increment(T value) {
    return value + 1;
}
Conceptual Check

What happens when you instantiate Array<int, 5> and Array<int, 10>?

Runtime Environment

Interactive Lab

1#include <iostream>
2 
3template<typename T>
4T add(T a, T b) {
5 return a + b;
6}
7 
8template<typename T, size_t N>
9class FixedArray {
10 T data_[N];
11public:
12 constexpr size_t size() const { return N; }
13};
14 
15int main() {
16 std::cout << add(5, 10) << '\n';
17 std::cout << add(1.5, 2.5) << '\n';
18
19 FixedArray<int, 5> arr;
20 std::cout << "Array size: " << arr.size() << '\n';
21 return 0;
22}
System Console

Waiting for signal...

Modern C++ Features

Section Detail

Smart Pointers and Memory Management

Smart Pointers: Automated Memory Management

Raw pointers require manual memory management with new/delete, leading to leaks, double-deletes, and dangling pointers. Smart pointers from <memory> automate lifetime management through RAII.

unique_ptr: Exclusive Ownership

unique_ptr<T> represents exclusive ownership: exactly one owner, non-copyable but movable. The pointed-to object is destroyed when the unique_ptr is destroyed.

#include <memory>

std::unique_ptr<int> p1(new 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

auto p3 = std::make_unique<int>(100);  // Preferred (C++14)

Why make_unique?

// Problematic:
process(std::unique_ptr<T>(new T), might_throw());
// If might_throw() throws after new but before unique_ptr construction, leak!

// Safe:
process(std::make_unique<T>(), might_throw());
// make_unique is atomic: allocation and unique_ptr construction happen together

unique_ptr for Arrays

std::unique_ptr<int[]> arr(new int[10]);
arr[0] = 5;  // Operator[] available for array version

// Prefer std::vector or std::array
std::vector<int> vec(10);  // Better

Custom Deleters

struct FileCloser {
    void operator()(FILE* fp) const {
        if (fp) fclose(fp);
    }
};

std::unique_ptr<FILE, FileCloser> file(fopen("data.txt", "r"));
// File automatically closed when unique_ptr destroyed

shared_ptr: Shared Ownership

shared_ptr<T> uses reference counting: the object is destroyed when the last shared_ptr owning it is destroyed. Copyable and movable.

std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::shared_ptr<int> p2 = p1;  // Both own the object
// Reference count: 2

p1.reset();  // p1 releases ownership, count = 1
// p2 still valid, object alive

Control Block Architecture

Each shared_ptr has two pointers:

  1. Pointer to the managed object
  2. Pointer to the control block (reference counts, deleter)
Interactive Lab

Creating shared_ptr

// Preferred: single allocation for object + control block
auto p1 = <int>(42);

// Avoid: two allocations
std::shared_ptr<int> p2(new int(42));

Cyclic References Problem

struct Node {
    std::shared_ptr<Node> next;
};

std::shared_ptr<Node> a = std::make_shared<Node>();
std::shared_ptr<Node> b = std::make_shared<Node>();

a->next = b;  // a owns b, count = 2
b->next = a;  // b owns a, count = 2
// Neither can be destroyed: memory leak!

weak_ptr: Breaking Cycles

weak_ptr<T> observes a shared_ptr without affecting reference count. Used to break cycles and check if object still exists.

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Doesn't own
};

std::shared_ptr<Node> a = std::make_shared<Node>();
std::shared_ptr<Node> b = std::make_shared<Node>();

a->next = b;
b->prev = a;  // Weak reference: no cycle

Using weak_ptr

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;

if (auto locked = wp.lock()) {  // Attempt to get shared_ptr
    std::cout << *locked << '\\n';  // Object still exists
} else {
    std::cout << "Object destroyed\\n";
}

Comparison: unique_ptr vs shared_ptr

Aspectunique_ptrshared_ptr
OwnershipExclusiveShared
CopyableNoYes
SizeSize of pointer2 pointers
OverheadZeroReference counting (atomic for thread safety)
Use WhenClear single ownerUnclear ownership, need sharing

Rule of Thumb: Default to unique_ptr. Use shared_ptr only when ownership truly must be shared.

Returning Smart Pointers from Functions

// Return unique_ptr when transferring ownership
std::unique_ptr<Widget> createWidget() {
    return std::make_unique<Widget>();
}

auto w = createWidget();  // Ownership transferred via move

// Prefer returning by value for copyable types
Widget createWidget2() {
    return Widget{};  // Copy elision / move
}

Smart Pointers and Polymorphism

class Base { 
public:
    virtual ~Base() = default; 
};
class Derived : public Base {};

std::unique_ptr<Base> p = std::make_unique<Derived>();
// Polymorphism works: Derived destructor called when p destroyed

Common Pitfalls

Constructing shared_ptr from Same Raw Pointer Twice

int* raw = new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw);  // DISASTER: double-delete!

// Each shared_ptr has independent control block

Never create multiple shared_ptr from the same raw pointer. Use make_shared or copy existing shared_ptr.

Conceptual Check

What happens when you copy a unique_ptr?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <memory>
3 
4class Resource {
5 int id_;
6public:
7 Resource(int id) : id_(id) { std::cout << "Resource " << id_ << " created\n"; }
8 ~Resource() { std::cout << "Resource " << id_ << " destroyed\n"; }
9 int id() const { return id_; }
10};
11 
12int main() {
13 {
14 auto p1 = std::make_unique<Resource>(1);
15 auto p2 = std::make_shared<Resource>(2);
16 auto p3 = p2; // Shared ownership
17 std::cout << "Ref count: " << p2.use_count() << '\n';
18 } // All destroyed here
19 std::cout << "Scope exited\n";
20 return 0;
21}
System Console

Waiting for signal...

Section Detail

Move Semantics and Perfect Forwarding

Move Semantics: Avoiding Expensive Copies

Before C++11, returning or passing large objects by value meant expensive copies. Move semantics allow transferring resources from temporary objects instead of copying them.

class Buffer {
    char* data_;
    size_t size_;
public:
    // Constructor
    Buffer(size_t s) : data_(new char[s]), size_(s) {}
    
    // Copy constructor (deep copy)
    Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    
    // Move constructor (transfer ownership)
    Buffer(Buffer&& other) noexcept 
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    
    ~Buffer() { delete[] data_; }
};

Move Constructor and Move Assignment

Move operations should be noexcept when possible—this enables optimizations in standard containers:

class Widget {
    std::string name_;
    std::vector<int> data_;
public:
    // Move constructor
    Widget(Widget&& other) noexcept
        : name_(std::move(other.name_)),
          data_(std::move(other.data_)) {
    }
    
    // Move assignment
    Widget& operator=(Widget&& other) noexcept {
        if (this != &other) {
            name_ = std::move(other.name_);
            data_ = std::move(other.data_);
        }
        return *this;
    }
};

Note: std::move on member variables is necessary because a named rvalue reference (like other) is itself an lvalue!

std::move: Unconditional Cast to Rvalue

std::move doesn’t move anything—it just casts its argument to an rvalue reference, enabling move semantics:

std::string s1 = "hello";
std::string s2 = std::move(s1);  // s1's data moved to s2
// s1 is now in "valid but unspecified" state

After std::move: The object remains alive and destructible, but its value is unspecified. Only reassign or destroy it.

std::string s = "hello";
std::string t = std::move(s);

// s.clear();  // OK: Reset to known state
// s = "new";  // OK: Reassign
// auto len = s.size();  // DANGEROUS: Value unspecified

Move-Only Types

Some types cannot be copied, only moved: unique_ptr, thread, iostream, etc.

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: move

std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(10));  // Move into vector

Return Value Optimization (RVO) and NRVO

Compilers can elide copies/moves when returning by value:

Widget createWidget() {
    return Widget{};  // RVO: No move/copy, constructed directly in caller
}

Widget createWidget2() {
    Widget w;
    // ...
    return w;  // NRVO: Named Return Value Optimization (maybe)
}

Don’t std::move return values—it inhibits RVO:

Widget createWidget() {
    Widget w;
    return std::move(w);  // BAD: Prevents NRVO, forces move
}

Perfect Forwarding with std::forward

When writing wrapper functions, you want to forward arguments exactly as received—preserving lvalue/rvalue-ness. This requires forwarding references (a.k.a. universal references).

template<typename T>
void wrapper(T&& arg) {  // Forwarding reference
    target(std::forward<T>(arg));  // Perfect forwarding
}

int x = 10;
wrapper(x);        // T = int&, arg forwarded as lvalue
wrapper(10);       // T = int, arg forwarded as rvalue
wrapper(std::move(x));  // T = int, arg forwarded as rvalue

Forwarding Reference Rules

T&& is a forwarding reference only when:

  1. T is a template type parameter
  2. Type deduction occurs
template<typename T>
void func(T&& x);  // Forwarding reference

template<typename T>
void func(std::vector<T>&& x);  // NOT forwarding reference (no deduction for &&)

void func(int&& x);  // NOT forwarding reference (not a template)

Variadic Template Forwarding

Combining variadic templates with perfect forwarding:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

auto p = make_unique<std::pair<int, std::string>>(42, "hello");

When to Use Move vs Forward

  • std::move: When you know you have an rvalue or want to treat something as rvalue
  • std::forward: In templates, when you want to preserve the value category
template<typename T>
void process(T&& arg) {
    // Use arg multiple times...
    
    target(std::forward<T>(arg));  // Forward on last use
}

Move Semantics and Exception Safety

Move operations should be noexcept when possible. Standard containers use move only if noexcept:

class Widget {
public:
    Widget(Widget&& other) noexcept { /* ... */ }
    Widget& operator=(Widget&& other) noexcept { /* ... */ }
};

std::vector<Widget> vec;
vec.push_back(Widget{});  // Uses move (noexcept)

// If move is not noexcept, vector uses copy (for strong exception guarantee)
Conceptual Check

What does std::move actually do?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <utility>
3#include <string>
4 
5class Holder {
6 std::string data_;
7public:
8 Holder(std::string s) : data_(std::move(s)) {}
9
10 Holder(const Holder&) { std::cout << "Copied\n"; }
11 Holder(Holder&&) noexcept { std::cout << "Moved\n"; }
12
13 Holder& operator=(const Holder&) { std::cout << "Copy-assigned\n"; return *this; }
14 Holder& operator=(Holder&&) noexcept { std::cout << "Move-assigned\n"; return *this; }
15};
16 
17int main() {
18 Holder h1("hello");
19 Holder h2 = h1; // Copy
20 Holder h3 = std::move(h1); // Move
21 h2 = h3; // Copy assign
22 h2 = std::move(h3); // Move assign
23 return 0;
24}
System Console

Waiting for signal...

Section Detail

Lambda Expressions

Lambda Expressions: Anonymous Functions

C++11 introduced lambdas: inline anonymous function objects with concise syntax. Lambdas are extensively used with algorithms, callbacks, and asynchronous operations.

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

std::vector<int> v = {4, 2, 5, 1, 3};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
// v is now {5, 4, 3, 2, 1}

Lambda Syntax

[captures](parameters) specifiers -> return_type { body }
  • captures: Variables from enclosing scope
  • parameters: Function parameters (optional, default ())
  • specifiers: mutable, constexpr, noexcept
  • return_type: Can be deduced or explicit
  • body: Function implementation

Minimal Lambda

auto f = [] { return 42; };  // No parameters, deduced return type

Capture Modes

Lambdas can capture variables from their enclosing scope:

Capture by Value

int x = 10;
auto lambda = [x]() { return x * 2; };  // Captures x by value
x = 20;
std::cout << lambda();  // Outputs: 20 (captured value at lambda creation)

Capture by Reference

int x = 10;
auto lambda = [&x]() { return x * 2; };  // Captures x by reference
x = 20;
std::cout << lambda();  // Outputs: 40 (current value of x)

Mixed Captures

int x = 1, y = 2, z = 3;
auto lambda = [x, &y, z]() {
    // x and z by value, y by reference
};

Default Captures

int a = 1, b = 2;

auto lambda1 = [=]() { return a + b; };  // Capture all by value
auto lambda2 = [&]() { return a + b; };  // Capture all by reference

auto lambda3 = [=, &b]() { return a + b; };  // All by value except b
auto lambda4 = [&, a]() { return a + b; };   // All by reference except a

Warning: Capturing [=] doesn’t capture this in member functions (C++17 changed this). Prefer explicit captures.

Mutable Lambdas

By default, value-captured variables are const. Use mutable to modify them:

int x = 0;
auto counter = [x]() mutable {
    return ++x;  // Modifies lambda's copy of x
};

std::cout << counter();  // 1
std::cout << counter();  // 2
std::cout << x;          // 0 (original x unchanged)

Generic Lambdas (C++14)

Parameters can use auto for generic types:

auto print = [](const auto& x) {
    std::cout << x << '\\n';
};

print(42);       // Works with int
print(3.14);     // Works with double
print("hello");  // Works with const char*

Equivalent to a template function:

struct Lambda {
    template<typename T>
    void operator()(const T& x) const {
        std::cout << x << '\\n';
    }
};

Init Captures (C++14)

Capture with initialization, enabling move-only types:

auto ptr = std::make_unique<int>(42);

auto lambda = [p = std::move(ptr)]() {
    return *p;
};

// ptr is now nullptr, lambda owns the unique_ptr

Rename and transform during capture:

int x = 5;
auto lambda = [y = x * 2]() {
    return y;  // y is 10
};

Return Type Deduction

Usually automatic, but can be explicit:

auto lambda1 = [](int x) { return x * 2; };  // Deduced: int

auto lambda2 = [](int x) -> double {  // Explicit: double
    return x * 2.5;
};

Required when return type is ambiguous:

auto lambda = [](bool flag) {
    if (flag)
        return 1;      // int
    else
        return 2.5;    // double - ERROR: inconsistent types
};

// Fix with explicit return type:
auto lambda = [](bool flag) -> double {
    if (flag) return 1;
    else return 2.5;
};

Lambda Type and std::function

Each lambda has a unique compiler-generated type. Use auto to store:

auto lambda = [](int x) { return x * 2; };

std::function provides type erasure but has overhead:

#include <functional>

std::function<int(int)> func = [](int x) { return x * 2; };
// More flexible but slower than auto

Prefer auto unless you need type erasure or reassignment.

Immediately Invoked Lambda Expression (IIFE)

Useful for complex initialization:

const auto value = [&]() {
    if (condition1) return compute1();
    else if (condition2) return compute2();
    else return default_value();
}();  // Immediately invoked

Lambdas with Algorithms

Classic use case: predicates and projections

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

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

// Count elements > 3
auto count = std::count_if(v.begin(), v.end(),
                           [](int x) { return x > 3; });
Conceptual Check

What does [=] capture in a member function (C++17+)?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <algorithm>
4 
5int main() {
6 std::vector<int> v = {1, 2, 3, 4, 5};
7 int threshold = 3;
8
9 // Generic lambda with capture
10 auto filter = [threshold](const auto& x) {
11 return x > threshold;
12 };
13
14 auto count = std::count_if(v.begin(), v.end(), filter);
15 std::cout << "Elements > " << threshold << ": " << count << '\n';
16
17 // IIFE for initialization
18 const auto sum = [&v]() {
19 int total = 0;
20 for (auto x : v) total += x;
21 return total;
22 }();
23
24 std::cout << "Sum: " << sum << '\n';
25 return 0;
26}
System Console

Waiting for signal...

Standard Template Library

Section Detail

STL Containers: Sequence Containers

Sequence Containers: Ordered Collections

The STL provides several sequence containers, each with different performance characteristics. Choose based on access patterns and operation frequencies.

std::vector: Dynamic Array

Contiguous memory, random access, amortized O(1) push_back:

std::vector<int> v;
v.push_back(10);
v.push_back(20);
v.emplace_back(30);  // Construct in-place

v[0] = 15;  // O(1) random access
v.resize(100);
v.reserve(1000);  // Pre-allocate capacity

for (const auto& elem : v) {
    std::cout << elem << ' ';
}

Capacity vs Size

  • size(): Number of elements
  • capacity(): Allocated storage
  • reserve(n): Ensure capacity ≥ n (doesn’t change size)
  • resize(n): Change size (may construct/destroy elements)
std::vector<int> v;
v.reserve(100);  // capacity = 100, size = 0
v.resize(50);    // capacity = 100, size = 50 (elements default-constructed)

Invalidation Rules

  • push_back: Invalidates all if reallocation occurs
  • insert/erase: Invalidates from point of modification onward
  • clear: Invalidates all iterators, size = 0, capacity unchanged

When to Use vector

  • Default choice for sequence container
  • Need random access
  • Frequent access to elements
  • Size relatively stable or grows at end

std::deque: Double-Ended Queue

Like vector but efficient insertion/deletion at both ends:

std::deque<int> d;
d.push_front(10);  // O(1)
d.push_back(20);   // O(1)
d[1] = 25;         // O(1) random access

d.pop_front();     // O(1)
d.pop_back();      // O(1)

Implementation

Not contiguous memory—typically a sequence of fixed-size arrays (chunks). Random access is O(1) but slightly slower than vector.

When to Use deque

  • Need push/pop at both ends
  • Random access required
  • Don’t need pointer stability
  • Used by std::stack and std::queue

std::list: Doubly-Linked List

Bidirectional list, O(1) insertion/deletion anywhere (if you have iterator):

std::list<int> lst = {1, 2, 3, 4, 5};

auto it = std::find(lst.begin(), lst.end(), 3);
lst.insert(it, 10);  // Insert before 3: {1, 2, 10, 3, 4, 5}
lst.erase(it);       // Remove 3: {1, 2, 10, 4, 5}

lst.push_front(0);
lst.push_back(6);

Splice Operations

Move elements between lists in O(1):

std::list<int> l1 = {1, 2, 3};
std::list<int> l2 = {4, 5, 6};

l1.splice(l1.end(), l2);  // Move all of l2 to end of l1
// l1: {1, 2, 3, 4, 5, 6}, l2: {}

When to Use list

  • Frequent insertion/deletion in middle
  • No need for random access
  • Need iterator stability (iterators never invalidated except for erased elements)
  • Rarely used in practice (cache-unfriendly)

std::forward_list: Singly-Linked List

Like list but single direction, smaller memory footprint:

std::forward_list<int> fwd = {1, 2, 3};

fwd.push_front(0);  // O(1)
// No push_back (would be O(n))

auto it = fwd.before_begin();
fwd.insert_after(it, 10);  // Insert after position

When to Use forward_list

  • Minimizing memory overhead matters
  • Only need forward iteration
  • Very rare in practice

std::array: Fixed-Size Array

Compile-time fixed size, no dynamic allocation:

std::array<int, 5> arr = {1, 2, 3, 4, 5};

arr[0] = 10;  // O(1) access
arr.at(2) = 30;  // Bounds-checked access

// Size is part of type
// std::array<int, 5> != std::array<int, 6>

array vs C Array

int c_array[5];          // No bounds checking, decays to pointer
std::array<int, 5> arr;  // Knows size, range-based for, STL algorithms

When to Use array

  • Fixed size known at compile time
  • Want STL container interface
  • Need to pass to functions without decay

Performance Comparison

Operationvectordequelistforward_list
Random AccessO(1)O(1)O(n)O(n)
Insert/Delete FrontO(n)O(1)O(1)O(1)
Insert/Delete BackO(1)†O(1)O(1)O(n)
Insert/Delete MiddleO(n)O(n)O(1)‡O(1)‡
MemoryContiguousChunkedNodeNode

† Amortized
‡ If you already have an iterator

Conceptual Check

Why is std::vector the default choice for sequence containers?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <deque>
4#include <list>
5 
6int main() {
7 std::vector<int> v = {1, 2, 3};
8 v.push_back(4);
9 std::cout << "vector: ";
10 for (auto x : v) std::cout << x << ' ';
11 std::cout << '\n';
12
13 std::deque<int> d = {1, 2, 3};
14 d.push_front(0);
15 d.push_back(4);
16 std::cout << "deque: ";
17 for (auto x : d) std::cout << x << ' ';
18 std::cout << '\n';
19
20 std::list<int> l = {1, 2, 3};
21 l.push_front(0);
22 l.push_back(4);
23 std::cout << "list: ";
24 for (auto x : l) std::cout << x << ' ';
25 std::cout << '\n';
26
27 return 0;
28}
System Console

Waiting for signal...

Section Detail

STL Containers: Associative Containers

Associative Containers: Key-Based Lookup

Associative containers organize elements by key for fast lookup. Two families: ordered (red-black trees) and unordered (hash tables).

std::set: Unique Sorted Keys

Stores unique elements in sorted order:

std::set<int> s = {5, 2, 8, 2, 1};  // {1, 2, 5, 8} - duplicates removed

s.insert(3);  // O(log n)
s.erase(2);   // O(log n)

if (s.count(5)) {  // O(log n), returns 0 or 1
    std::cout << "Found 5\\n";
}

auto it = s.find(8);  // O(log n), returns iterator
if (it != s.end()) {
    std::cout << *it << '\\n';
}

Iteration Order

Elements are always sorted:

std::set<int> s = {5, 1, 3, 2, 4};
for (auto x : s) {
    std::cout << x << ' ';  // Outputs: 1 2 3 4 5
}

Custom Comparison

struct Person {
    std::string name;
    int age;
};

struct CompareByAge {
    bool operator()(const Person& a, const Person& b) const {
        return a.age < b.age;
    }
};

std::set<Person, CompareByAge> people;

std::map: Key-Value Pairs

Associates keys with values, sorted by key:

std::map<std::string, int> ages;
ages["Alice"] = 30;
ages["Bob"] = 25;
ages.insert({"Charlie", 35});
ages.emplace("David", 28);

std::cout << ages["Alice"];  // 30

// operator[] creates element if missing!
std::cout << ages["Unknown"];  // Creates entry with value 0

// Use find to avoid creation:
if (auto it = ages.find("Eve"); it != ages.end()) {
    std::cout << it->second;
} else {
    std::cout << "Not found\\n";
}

Structured Bindings (C++17)

for (const auto& [name, age] : ages) {
    std::cout << name << ": " << age << '\\n';
}

std::multiset and std::multimap

Allow duplicate keys:

std::multiset<int> ms = {1, 2, 2, 3, 3, 3};
std::cout << ms.count(3);  // 3

std::multimap<std::string, int> mm;
mm.insert({"Alice", 90});
mm.insert({"Alice", 85});  // Same key, different value

auto range = mm.equal_range("Alice");
for (auto it = range.first; it != range.second; ++it) {
    std::cout << it->second << ' ';  // 90 85
}

Unordered Associative Containers (C++11)

Hash table based: O(1) average, no ordering:

std::unordered_set<int> us = {5, 2, 8, 1};
us.insert(3);  // O(1) average

// Iteration order is arbitrary
for (auto x : us) {
    std::cout << x << ' ';  // Unpredictable order
}

std::unordered_map<std::string, int> um;
um["key"] = 42;  // O(1) average

Custom Hash Function

struct Person {
    std::string name;
    int age;
};

struct PersonHash {
    size_t operator()(const Person& p) const {
        return std::hash<std::string>{}(p.name) ^ std::hash<int>{}(p.age);
    }
};

struct PersonEqual {
    bool operator()(const Person& a, const Person& b) const {
        return a.name == b.name && a.age == b.age;
    }
};

std::unordered_set<Person, PersonHash, PersonEqual> people;

Ordered vs Unordered: When to Use

AspectOrdered (set/map)Unordered (unordered_set/map)
LookupO(log n)O(1) average, O(n) worst
InsertionO(log n)O(1) average
IterationSorted orderArbitrary order
MemoryLess overheadMore overhead (buckets)
Use WhenNeed sorted order, range queriesOnly need fast lookup

Default choice: unordered_map/unordered_set unless you need ordering.

Range-Based Operations

Ordered containers support efficient range queries:

std::set<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9};

auto lower = s.lower_bound(3);  // First element >= 3
auto upper = s.upper_bound(7);  // First element > 7

for (auto it = lower; it != upper; ++it) {
    std::cout << *it << ' ';  // 3 4 5 6 7
}

Transparent Comparators (C++14)

Avoid creating temporary objects for lookup:

std::set<std::string, std::less<>> s;  // Note: std::less<> not std::less<std::string>

s.find("hello");  // Doesn't create std::string temporary
Conceptual Check

What happens when you use operator[] on a map with a non-existent key?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <map>
3#include <unordered_map>
4#include <string>
5 
6int main() {
7 std::map<std::string, int> ordered;
8 ordered["zebra"] = 1;
9 ordered["apple"] = 2;
10 ordered["mango"] = 3;
11
12 std::cout << "Ordered map: ";
13 for (const auto& [key, val] : ordered) {
14 std::cout << key << " ";
15 }
16 std::cout << '\n';
17
18 std::unordered_map<std::string, int> unordered;
19 unordered["zebra"] = 1;
20 unordered["apple"] = 2;
21 unordered["mango"] = 3;
22
23 std::cout << "Unordered map: ";
24 for (const auto& [key, val] : unordered) {
25 std::cout << key << " ";
26 }
27 std::cout << '\n';
28
29 return 0;
30}
System Console

Waiting for signal...

Section Detail

STL Algorithms

STL Algorithms: Iterator-Based Operations

The <algorithm> header provides ~100 algorithms operating on iterator ranges, enabling generic operations on any container.

Non-Modifying Algorithms

find, find_if, find_if_not

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

auto it = std::find(v.begin(), v.end(), 3);
if (it != v.end()) {
    std::cout << "Found: " << *it << '\\n';
}

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

count, count_if

auto n = std::count(v.begin(), v.end(), 3);
auto even_count = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });

all_of, any_of, none_of

bool all_positive = std::all_of(v.begin(), v.end(), [](int x) { return x > 0; });
bool has_even = std::any_of(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
bool no_negatives = std::none_of(v.begin(), v.end(), [](int x) { return x < 0; });

Modifying Algorithms

copy, copy_if

std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(src.size());

std::copy(src.begin(), src.end(), dst.begin());

std::vector<int> evens;
std::copy_if(src.begin(), src.end(), std::back_inserter(evens),
             [](int x) { return x % 2 == 0; });

transform

Apply function to each element:

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

std::transform(v.begin(), v.end(), squared.begin(),
               [](int x) { return x * x; });
// squared = {1, 4, 9, 16, 25}

// Binary transform
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {4, 5, 6};
std::vector<int> sum(3);

std::transform(a.begin(), a.end(), b.begin(), sum.begin(),
               [](int x, int y) { return x + y; });
// sum = {5, 7, 9}

fill, generate

std::vector<int> v(10);
std::fill(v.begin(), v.end(), 42);  // All elements = 42

int n = 0;
std::generate(v.begin(), v.end(), [&n]() { return n++; });
// v = {0, 1, 2, ..., 9}

remove, remove_if

Careful: Doesn’t actually remove, just moves elements to end:

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

auto new_end = std::remove_if(v.begin(), v.end(),
                               [](int x) { return x % 2 == 0; });
v.erase(new_end, v.end());  // Actually erase
// v = {1, 3, 5}

// Idiomatic erase-remove:
v.erase(std::remove_if(v.begin(), v.end(),
                       [](int x) { return x < 3; }),
        v.end());

C++20 added std::erase_if for convenience:

std::erase_if(v, [](int x) { return x < 3; });

Sorting and Partitioning

sort, stable_sort, partial_sort

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

std::sort(v.begin(), v.end());  // O(n log n), unstable
// v = {1, 2, 5, 8, 9}

std::sort(v.begin(), v.end(), std::greater<>());  // Descending
// v = {9, 8, 5, 2, 1}

std::stable_sort(v.begin(), v.end());  // Preserves relative order of equal elements

nth_element

Partially sorts so that nth element is in correct position:

std::vector<int> v = {5, 2, 8, 1, 9, 3, 7};
std::nth_element(v.begin(), v.begin() + 3, v.end());

// v[3] is the 4th smallest element
// Elements before v[3] are <= v[3]
// Elements after v[3] are >= v[3]
// Relative order not specified

partition

std::vector<int> v = {1, 2, 3, 4, 5, 6};
auto pivot = std::partition(v.begin(), v.end(),
                            [](int x) { return x % 2 == 0; });
// Even numbers before pivot, odd after

Binary Search (on sorted ranges)

lower_bound, upper_bound, equal_range

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

auto lower = std::lower_bound(v.begin(), v.end(), 2);  // First >= 2
auto upper = std::upper_bound(v.begin(), v.end(), 2);  // First > 2

std::cout << std::distance(lower, upper);  // Count of 2's: 3

auto [first, last] = std::equal_range(v.begin(), v.end(), 2);  // C++17
bool found = std::binary_search(v.begin(), v.end(), 3);  // O(log n)

Set Operations (on sorted ranges)

std::vector<int> a = {1, 2, 3, 4, 5};
std::vector<int> b = {3, 4, 5, 6, 7};
std::vector<int> result;

std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
                      std::back_inserter(result));
// result = {3, 4, 5}

result.clear();
std::set_union(a.begin(), a.end(), b.begin(), b.end(),
               std::back_inserter(result));
// result = {1, 2, 3, 4, 5, 6, 7}

result.clear();
std::set_difference(a.begin(), a.end(), b.begin(), b.end(),
                    std::back_inserter(result));
// result = {1, 2}

Numeric Algorithms (<numeric>)

accumulate, reduce

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

// C++17 reduce (parallel)
int sum2 = std::reduce(std::execution::par, v.begin(), v.end());

partial_sum, adjacent_difference

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

std::partial_sum(v.begin(), v.end(), result.begin());
// result = {1, 3, 6, 10, 15}  (cumulative sum)

std::adjacent_difference(v.begin(), v.end(), result.begin());
// result = {1, 1, 1, 1, 1}  (differences)
Conceptual Check

What does std::remove_if actually do?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <numeric>
5 
6int main() {
7 std::vector<int> v = {5, 2, 8, 1, 9, 3};
8
9 std::sort(v.begin(), v.end());
10 std::cout << "Sorted: ";
11 for (auto x : v) std::cout << x << ' ';
12 std::cout << '\n';
13
14 auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 5; });
15 if (it != v.end()) std::cout << "First > 5: " << *it << '\n';
16
17 int sum = std::accumulate(v.begin(), v.end(), 0);
18 std::cout << "Sum: " << sum << '\n';
19
20 return 0;
21}
System Console

Waiting for signal...

Section Detail

Iterators and Iterator Categories

Iterators: The Glue Between Containers and Algorithms

Iterators abstract the concept of position within a sequence, enabling algorithms to work with any container. They’re the foundation of generic programming in C++.

Iterator Categories

Iterators form a hierarchy based on operations they support:

System Diagram
Input IteratorForward IteratorBidirectional IteratorRandom Access IteratorOutput IteratorContiguous Iterator (C++20)

Input Iterator

Read-only, single-pass:

  • *it (read)
  • ++it, it++
  • ==, !=

Example: std::istream_iterator

Output Iterator

Write-only, single-pass:

  • *it = value (write)
  • ++it, it++

Example: std::ostream_iterator, std::back_inserter

Forward Iterator

Multi-pass read/write:

  • All input iterator operations
  • *it = value
  • Multi-pass guarantee

Example: std::forward_list::iterator

Bidirectional Iterator

Forward + backward:

  • All forward iterator operations
  • --it, it--

Example: std::list::iterator, std::set::iterator

Random Access Iterator

Bidirectional + arithmetic:

  • All bidirectional operations
  • it + n, it - n
  • it[n]
  • <, >, <=, >=

Example: std::vector::iterator, std::deque::iterator

Contiguous Iterator (C++20)

Random access + contiguous memory:

  • Guarantees elements are in contiguous memory
  • Enables pointer arithmetic optimizations

Example: std::vector::iterator, std::array::iterator

Basic Iterator Operations

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

auto it = v.begin();  // Iterator to first element
auto end = v.end();   // Past-the-end iterator

while (it != end) {
    std::cout << *it << ' ';  // Dereference
    ++it;  // Advance
}

// Range-based for is syntactic sugar for iterators
for (auto x : v) {
    std::cout << x << ' ';
}

Iterator Adapters

back_inserter, front_inserter, inserter

Convert containers into output iterators:

std::vector<int> v;
std::fill_n(std::back_inserter(v), 5, 42);  // Pushes 5 copies of 42

std::list<int> lst;
std::fill_n(std::front_inserter(lst), 3, 10);  // Pushes to front

std::set<int> s;
std::copy(v.begin(), v.end(), std::inserter(s, s.begin()));

reverse_iterator

Iterate backwards:

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

for (auto it = v.rbegin(); it != v.rend(); ++it) {
    std::cout << *it << ' ';  // 5 4 3 2 1
}

move_iterator

Causes dereference to return rvalue reference:

std::vector<std::unique_ptr<int>> v1;
v1.push_back(std::make_unique<int>(1));
v1.push_back(std::make_unique<int>(2));

std::vector<std::unique_ptr<int>> v2(
    std::make_move_iterator(v1.begin()),
    std::make_move_iterator(v1.end())
);
// v1's unique_ptrs moved to v2

Stream Iterators

istream_iterator

Read from input stream:

std::istringstream iss("1 2 3 4 5");
std::vector<int> v(std::istream_iterator<int>(iss),
                   std::istream_iterator<int>());
// v = {1, 2, 3, 4, 5}

ostream_iterator

Write to output stream:

std::vector<int> v = {1, 2, 3, 4, 5};
std::copy(v.begin(), v.end(),
          std::ostream_iterator<int>(std::cout, " "));
// Outputs: 1 2 3 4 5

Iterator Utilities

std::advance

Move iterator by n positions:

std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
std::advance(it, 3);  // Now points to 4

std::distance

Count elements between iterators:

auto dist = std::distance(lst.begin(), lst.end());  // 5

std::next, std::prev

Return advanced iterator without modifying original:

auto it = v.begin();
auto next = std::next(it, 2);  // Points 2 positions ahead
auto prev = std::prev(it, 1);  // Points 1 position back

Custom Iterators

Creating custom iterators requires defining specific operations based on category:

class Range {
    int current_;
    int end_;
public:
    class Iterator {
        int value_;
    public:
        using iterator_category = std::forward_iterator_tag;
        using value_type = int;
        using difference_type = int;
        using pointer = int*;
        using reference = int&;
        
        Iterator(int v) : value_(v) {}
        
        int operator*() const { return value_; }
        Iterator& operator++() { ++value_; return *this; }
        Iterator operator++(int) { auto temp = *this; ++value_; return temp; }
        
        bool operator==(const Iterator& other) const { return value_ == other.value_; }
        bool operator!=(const Iterator& other) const { return !(*this == other); }
    };
    
    Range(int start, int end) : current_(start), end_(end) {}
    
    Iterator begin() const { return Iterator(current_); }
    Iterator end() const { return Iterator(end_); }
};

// Usage:
for (auto x : Range(1, 6)) {
    std::cout << x << ' ';  // 1 2 3 4 5
}

Iterator Invalidation

Different operations invalidate iterators differently:

ContainerOperationInvalidation
vectorpush_backAll if reallocation
vectorinsert/eraseFrom point onward
dequepush_back/frontAll (except end)
listinsert/eraseOnly erased iterators
map/setinsert/eraseOnly erased iterators
Conceptual Check

Which iterator category do std::list iterators belong to?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <iterator>
4#include <algorithm>
5 
6int main() {
7 std::vector<int> src = {1, 2, 3, 4, 5};
8 std::vector<int> dst;
9
10 // Using back_inserter
11 std::copy(src.begin(), src.end(), std::back_inserter(dst));
12
13 std::cout << "Forward: ";
14 for (auto it = dst.begin(); it != dst.end(); ++it) {
15 std::cout << *it << ' ';
16 }
17 std::cout << '\n';
18
19 std::cout << "Reverse: ";
20 for (auto it = dst.rbegin(); it != dst.rend(); ++it) {
21 std::cout << *it << ' ';
22 }
23 std::cout << '\n';
24
25 return 0;
26}
System Console

Waiting for signal...

Advanced Features

Section Detail

Exception Handling

Exception Handling: Error Propagation Mechanism

Exceptions provide a structured way to handle errors, separating error handling from normal flow. When an exception is thrown, stack unwinding occurs: all local objects are destroyed until a matching catch handler is found.

Basic Syntax

#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) {
        throw std::runtime_error("Division by zero");
    }
    return a / b;
}

try {
    double result = divide(10, 0);
} catch (const std::runtime_error& e) {
    std::cerr << "Error: " << e.what() << '\\n';
} catch (const std::exception& e) {
    std::cerr << "Exception: " << e.what() << '\\n';
} catch (...) {
    std::cerr << "Unknown exception\\n";
}

Standard Exception Hierarchy

All standard exceptions derive from std::exception:

std::exception
├── std::logic_error
│   ├── std::invalid_argument
│   ├── std::domain_error
│   ├── std::length_error
│   └── std::out_of_range
├── std::runtime_error
│   ├── std::range_error
│   ├── std::overflow_error
│   └── std::underflow_error
└── std::bad_alloc
throw std::invalid_argument("Invalid input");
throw std::out_of_range("Index out of bounds");
throw std::bad_alloc();  // Memory allocation failed

Custom Exceptions

class FileError : public std::runtime_error {
    std::string filename_;
public:
    FileError(const std::string& file, const std::string& msg)
        : std::runtime_error(msg), filename_(file) {}
    
    const std::string& filename() const { return filename_; }
};

void openFile(const std::string& name) {
    // ...
    throw FileError(name, "Failed to open file");
}

try {
    openFile("data.txt");
} catch (const FileError& e) {
    std::cerr << "File error in " << e.filename() 
              << ": " << e.what() << '\\n';
}

Exception Safety Guarantees

Code can provide different levels of exception safety:

1. No-Throw Guarantee

Operation never throws (or only throws if program terminates):

void swap(T& a, T& b) noexcept {
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

2. Strong Exception Safety (Transactional)

If an exception is thrown, program state is unchanged:

void push_back(const T& value) {
    if (size_ == capacity_) {
        // Allocate new buffer
        T* new_data = new T[capacity_ * 2];
        
        // Copy old data (might throw)
        std::uninitialized_copy(data_, data_ + size_, new_data);
        
        // Only now commit changes (no-throw operations)
        delete[] data_;
        data_ = new_data;
        capacity_ *= 2;
    }
    data_[size_++] = value;
}

3. Basic Exception Safety

Invariants are preserved, no resources leak, but state may change:

void process() {
    auto ptr = new Resource;  // Potential leak if next line throws
    risky_operation();
    delete ptr;
}

// Better with RAII:
void process() {
    auto ptr = std::make_unique<Resource>();  // Automatic cleanup
    risky_operation();
}

4. No Exception Safety

Undefined behavior or resource leaks if exception thrown.

RAII and Exception Safety

RAII (Resource Acquisition Is Initialization) ensures cleanup even during exceptions:

class FileHandle {
    FILE* file_;
public:
    FileHandle(const char* name) : file_(fopen(name, "r")) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }
    
    ~FileHandle() {
        if (file_) fclose(file_);  // Always called during stack unwinding
    }
    
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};

void process() {
    FileHandle f("data.txt");
    // Use file...
    throw std::runtime_error("Error");  // File automatically closed
}

The noexcept Specifier

Declares that a function won’t throw exceptions:

void guaranteed() noexcept {
    // If an exception escapes, std::terminate is called
}

template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
    // noexcept if T's move constructor is noexcept
}

Importance: noexcept enables optimizations. Standard containers use move operations only if noexcept.

class Widget {
public:
    Widget(Widget&&) noexcept;  // vector will use move
    // Widget(Widget&&);  // vector will use copy instead (for safety)
};

Conditional noexcept

template<typename T>
class Stack {
public:
    void push(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) {
        // noexcept if T is nothrow copyable
    }
};

Rethrowing Exceptions

try {
    risky_operation();
} catch (const std::exception& e) {
    log_error(e.what());
    throw;  // Rethrow same exception (preserves type)
}

// DON'T do this:
catch (const std::exception& e) {
    throw e;  // Slices exception to std::exception!
}

Function try Blocks

Handle exceptions in constructor initialization:

class Widget {
    Resource r_;
public:
    Widget(const std::string& name)
    try : r_(name) {  // Function try block
        // Constructor body
    }
    catch (const std::exception& e) {
        // Handle exception from r_ construction
        // Note: Exception is always rethrown after this catch block
    }
};

Exception Specifications (Deprecated)

C++98 dynamic exception specifications are deprecated:

void func() throw(std::runtime_error);  // Deprecated, don't use

void func() noexcept;  // Modern C++
Conceptual Check

What happens when an exception is thrown but not caught?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <stdexcept>
3#include <memory>
4 
5class Resource {
6public:
7 Resource() { std::cout << "Resource acquired\n"; }
8 ~Resource() { std::cout << "Resource released\n"; }
9};
10 
11void dangerous() {
12 auto r = std::make_unique<Resource>();
13 throw std::runtime_error("Error!");
14}
15 
16int main() {
17 try {
18 dangerous();
19 } catch (const std::exception& e) {
20 std::cout << "Caught: " << e.what() << '\n';
21 }
22 std::cout << "Program continues\n";
23 return 0;
24}
System Console

Waiting for signal...

Section Detail

Namespaces and Name Lookup

Namespaces: Organizing Code and Avoiding Collisions

Namespaces prevent name collisions in large codebases by grouping related declarations under a common name.

Defining Namespaces

namespace graphics {
    class Shape {};
    void draw(const Shape& s) {}
}

namespace physics {
    class Shape {};  // Different from graphics::Shape
    void simulate(const Shape& s) {}
}

graphics::Shape s1;
physics::Shape s2;

Nested Namespaces

namespace company {
    namespace graphics {
        namespace renderer {
            class Pipeline {};
        }
    }
}

// C++17 shorthand:
namespace company::graphics::renderer {
    class Pipeline {};
}

Using Declarations and Directives

using Declaration

Introduces specific name:

using std::cout;
using std::endl;

cout << "Hello" << endl;  // No std:: prefix needed

using Directive

Imports entire namespace (use sparingly):

using namespace std;  // BAD: Pollutes global namespace

// OK in implementation files with limited scope:
void func() {
    using namespace std::chrono;  // Limited to function scope
    auto now = system_clock::now();
}

Best Practice: Avoid using namespace in headers—it forces pollution on all includers.

Argument-Dependent Lookup (ADL)

Also called “Koenig Lookup”: when calling a function, the compiler searches namespaces of argument types:

namespace lib {
    struct Widget {};
    void process(const Widget& w) {}
}

lib::Widget w;
process(w);  // Finds lib::process via ADL (no lib:: needed!)

ADL Examples

std::vector<int> v;
std::sort(v.begin(), v.end());  // ADL finds std::sort

// Also works:
sort(v.begin(), v.end());  // ADL looks in std:: because iterators are in std::

ADL Pitfalls

namespace lib {
    template<typename T>
    void swap(T& a, T& b) {
        T temp = a;
        a = b;
        b = temp;
    }
}

int main() {
    using std::swap;  // Bring std::swap into scope
    
    int a = 1, b = 2;
    swap(a, b);  // Calls std::swap (ADL + using declaration)
    
    lib::CustomType x, y;
    swap(x, y);  // Calls lib::swap via ADL
}

Anonymous Namespaces

Provide internal linkage (like static in C):

namespace {  // Anonymous namespace
    int internal_helper() {
        return 42;
    }
}

// internal_helper is only visible in this translation unit

Use: Replace file-scope static:

// Old style:
static void helper() {}

// Modern C++:
namespace {
    void helper() {}
}

Inline Namespaces (C++11)

Members of inline namespace are also members of enclosing namespace:

namespace lib {
    inline namespace v2 {
        void func() {}
    }
    namespace v1 {
        void func() {}
    }
}

lib::func();     // Calls lib::v2::func (inline namespace)
lib::v1::func(); // Explicitly call v1 version

Use Case: Versioning libraries while maintaining backward compatibility:

namespace mylib {
    inline namespace v3 {
        class Widget {};  // Current version
    }
    namespace v2 {
        class Widget {};  // Old version still available
    }
}

mylib::Widget w;     // Uses v3
mylib::v2::Widget w2; // Uses v2

Namespace Aliases

Shorten long namespace names:

namespace fs = std::filesystem;
namespace chrono = std::chrono;

fs::path p = "/home/user";
auto now = chrono::system_clock::now();

Using in Class Scope

class Derived : public Base {
public:
    using Base::Base;  // Inherit constructors
    using Base::method;  // Bring method into Derived scope
};

The Global Namespace

Prefix :: refers to global namespace:

int value = 10;  // Global

namespace lib {
    int value = 20;
    
    void func() {
        std::cout << value;    // 20 (lib::value)
        std::cout << ::value;  // 10 (global value)
    }
}

Best Practices

  1. Never using namespace in headers
  2. Prefer using declarations over directives
  3. Use anonymous namespaces instead of file-scope static
  4. Namespace names should be lowercase
  5. Avoid deeply nested namespaces (>3 levels)
  6. Use namespace aliases for long names
Conceptual Check

What is Argument-Dependent Lookup (ADL)?

Runtime Environment

Interactive Lab

1#include <iostream>
2 
3namespace math {
4 struct Point { double x, y; };
5
6 double distance(const Point& p) {
7 return std::sqrt(p.x * p.x + p.y * p.y);
8 }
9}
10 
11namespace {
12 void internal_helper() {
13 std::cout << "Internal function\n";
14 }
15}
16 
17int main() {
18 math::Point p{3.0, 4.0};
19
20 // ADL: finds math::distance because p is math::Point
21 std::cout << "Distance: " << distance(p) << '\n';
22
23 internal_helper();
24
25 return 0;
26}
System Console

Waiting for signal...

Metaprogramming

Section Detail

Type Traits and Compile-Time Programming

Type Traits: Compile-Time Type Information

The <type_traits> header provides utilities for querying and transforming types at compile time, enabling sophisticated template metaprogramming.

Type Predicates

Query type properties at compile time:

#include <type_traits>

static_assert(std::is_integral_v<int>);
static_assert(!std::is_integral_v<double>);

static_assert(std::is_floating_point_v<float>);
static_assert(std::is_pointer_v<int*>);
static_assert(std::is_array_v<int[10]>);
static_assert(std::is_class_v<std::string>);

Common Type Predicates

TraitChecks
is_voidType is void
is_integralInteger types
is_floating_pointfloat, double, long double
is_arithmeticIntegral or floating-point
is_pointerPointer type
is_referenceLvalue or rvalue reference
is_constConst-qualified
is_classClass or struct
is_enumEnumeration type

Type Relationships

static_assert(std::is_same_v<int, int>);
static_assert(!std::is_same_v<int, long>);

static_assert(std::is_base_of_v<Base, Derived>);
static_assert(std::is_convertible_v<Derived*, Base*>);

Type Transformations

Modify types at compile time:

// Remove const/volatile
using T1 = std::remove_const_t<const int>;  // int
using T2 = std::remove_cv_t<const volatile int>;  // int

// Add const/volatile
using T3 = std::add_const_t<int>;  // const int

// Remove reference
using T4 = std::remove_reference_t<int&>;  // int
using T5 = std::remove_reference_t<int&&>;  // int

// Remove pointer
using T6 = std::remove_pointer_t<int*>;  // int

// Decay (array/function to pointer, remove cv/reference)
using T7 = std::decay_t<int[10]>;  // int*
using T8 = std::decay_t<const int&>;  // int

Compile-Time Conditionals

std::conditional

Choose type based on condition:

template<typename T>
using StorageType = std::conditional_t<
    sizeof(T) <= 8,
    T,
    T*
>;

StorageType<int> x;      // int (small)
StorageType<BigClass> y; // BigClass* (large)

std::enable_if (SFINAE)

Enable function/class only if condition is true:

// Only enable for integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
square(T value) {
    return value * value;
}

square(5);     // OK
// square(3.14);  // Error: SFINAE'd out

Constructibility and Assignability

static_assert(std::is_default_constructible_v<std::string>);
static_assert(std::is_copy_constructible_v<int>);
static_assert(std::is_move_constructible_v<std::unique_ptr<int>>);
static_assert(!std::is_copy_assignable_v<std::unique_ptr<int>>);

// Nothrow versions
static_assert(std::is_nothrow_move_constructible_v<std::string>);

Practical Example: Generic Swap

template<typename T>
void my_swap(T& a, T& b) noexcept(std::is_nothrow_move_constructible_v<T> &&
                                   std::is_nothrow_move_assignable_v<T>) {
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

if constexpr (C++17)

Compile-time conditional branches:

template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // Dereference pointer
    } else {
        return t;   // Return value directly
    }
}

int x = 42;
auto v1 = get_value(&x);  // Dereferences, returns 42
auto v2 = get_value(x);   // Returns 42 directly

Discarded Branches

Code in discarded branches doesn’t need to be valid:

template<typename T>
void process(T value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << value * 2;  // Only instantiated for integral types
    } else {
        std::cout << value.toString();  // Only instantiated for types with toString()
    }
}

Tag Dispatching

Alternative to SFINAE using type tags:

template<typename Iterator>
void advance_impl(Iterator& it, int n, std::random_access_iterator_tag) {
    it += n;  // O(1) for random access
}

template<typename Iterator>
void advance_impl(Iterator& it, int n, std::input_iterator_tag) {
    while (n--) ++it;  // O(n) for input iterators
}

template<typename Iterator>
void advance(Iterator& it, int n) {
    advance_impl(it, n, typename std::iterator_traits<Iterator>::iterator_category{});
}

Compile-Time Assertions

template<typename T>
class Buffer {
    static_assert(std::is_trivially_copyable_v<T>,
                  "T must be trivially copyable");
    static_assert(sizeof(T) <= 64,
                  "T is too large for inline storage");
    T data_[100];
};

void_t: Detecting Members

template<typename, typename = void>
struct has_value_type : std::false_type {};

template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};

static_assert(has_value_type<std::vector<int>>::value);
static_assert(!has_value_type<int>::value);
Conceptual Check

What does std::decay_t do to a type?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <type_traits>
3 
4template<typename T>
5void check_type(T value) {
6 if constexpr (std::is_integral_v<T>) {
7 std::cout << "Integral: " << value * 2 << '\n';
8 } else if constexpr (std::is_floating_point_v<T>) {
9 std::cout << "Floating: " << value / 2 << '\n';
10 } else {
11 std::cout << "Other type\n";
12 }
13}
14 
15int main() {
16 check_type(42);
17 check_type(3.14);
18 check_type("hello");
19
20 static_assert(std::is_same_v<std::decay_t<int&>, int>);
21 std::cout << "All assertions passed\n";
22
23 return 0;
24}
System Console

Waiting for signal...

Concurrency

Section Detail

Multithreading Basics

Multithreading: Concurrent Execution

C++11 introduced a standard threading library (<thread>), enabling portable concurrent programming without platform-specific APIs.

Creating Threads

#include <thread>
#include <iostream>

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

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);
    
    t1.join();  // Wait for t1 to finish
    t2.join();  // Wait for t2 to finish
    
    return 0;
}

Thread Operations

Join vs Detach

std::thread t(task);

// Option 1: Join (wait for completion)
t.join();  // Blocks until thread completes

// Option 2: Detach (independent execution)
t.detach();  // Thread runs independently
// Cannot join() after detach()

Critical: Every thread must be either joined or detached before destruction, otherwise std::terminate is called.

{
    std::thread t(task);
}  // CRASH: thread destructor called without join/detach

Thread Guard RAII

class thread_guard {
    std::thread& t_;
public:
    explicit thread_guard(std::thread& t) : t_(t) {}
    ~thread_guard() {
        if (t_.joinable()) t_.join();
    }
    thread_guard(const thread_guard&) = delete;
    thread_guard& operator=(const thread_guard&) = delete;
};

void func() {
    std::thread t(task);
    thread_guard g(t);
    // Even if exception thrown, thread is joined
}

Passing Arguments

void task(int x, const std::string& s) {
    std::cout << x << ": " << s << '\\n';
}

std::thread t(task, 42, "hello");  // Args passed by value

Reference Arguments

void modify(int& x) {
    x = 100;
}

int value = 0;
std::thread t(modify, std::ref(value));  // Must use std::ref
t.join();
std::cout << value;  // 100

Data Races and Undefined Behavior

Concurrent modification of shared data without synchronization causes data races (undefined behavior):

int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;  // DATA RACE!
    }
}

std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();

std::cout << counter;  // Unpredictable result (< 200000)

Mutexes: Mutual Exclusion

std::mutex provides mutual exclusion:

#include <mutex>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();
        ++counter;
        mtx.unlock();
    }
}

Problem: If exception thrown between lock/unlock, mutex stays locked (deadlock).

Lock Guards: RAII for Mutexes

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // Locks mutex
        ++counter;
    }  // Automatically unlocks when lock goes out of scope
}

unique_lock: Flexible Locking

More flexible than lock_guard:

std::mutex mtx;

void task() {
    std::unique_lock<std::mutex> lock(mtx);
    
    // Do work...
    
    lock.unlock();  // Manually unlock
    
    // Do work without lock...
    
    lock.lock();  // Relock
    
    // More work...
}  // Automatically unlocks if locked

Deferred Locking

std::unique_lock<std::mutex> lock(mtx, std::defer_lock);  // Don't lock yet
// ...
lock.lock();  // Lock when ready

Avoiding Deadlocks

Deadlock Example

std::mutex m1, m2;

void thread1() {
    std::lock_guard<std::mutex> lock1(m1);
    std::lock_guard<std::mutex> lock2(m2);  // Potential deadlock
}

void thread2() {
    std::lock_guard<std::mutex> lock2(m2);
    std::lock_guard<std::mutex> lock1(m1);  // Opposite order
}

Solution: std::lock

void thread1() {
    std::unique_lock<std::mutex> lock1(m1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(m2, std::defer_lock);
    std::lock(lock1, lock2);  // Atomically locks both, deadlock-free
}

C++17 std::scoped_lock simplifies this:

void thread1() {
    std::scoped_lock lock(m1, m2);  // Locks both, unlocks in destructor
}

Thread Identification

std::thread::id main_thread_id = std::this_thread::get_id();

void task() {
    auto id = std::this_thread::get_id();
    std::cout << "Thread ID: " << id << '\\n';
}

Hardware Concurrency

unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware supports " << n << " concurrent threads\\n";
Conceptual Check

What happens if a thread object is destroyed without calling join() or detach()?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <thread>
3#include <mutex>
4#include <vector>
5 
6std::mutex mtx;
7int counter = 0;
8 
9void increment(int count) {
10 for (int i = 0; i < count; ++i) {
11 std::lock_guard<std::mutex> lock(mtx);
12 ++counter;
13 }
14}
15 
16int main() {
17 const int iterations = 10000;
18 std::vector<std::thread> threads;
19
20 for (int i = 0; i < 4; ++i) {
21 threads.emplace_back(increment, iterations);
22 }
23
24 for (auto& t : threads) {
25 t.join();
26 }
27
28 std::cout << "Counter: " << counter << '\n';
29 std::cout << "Expected: " << (4 * iterations) << '\n';
30
31 return 0;
32}
System Console

Waiting for signal...

Section Detail

Atomic Operations and Memory Ordering

Atomic Operations: Lock-Free Synchronization

The <atomic> header provides atomic operations—indivisible operations that appear to occur instantaneously to other threads, without requiring mutexes.

std::atomic Basics

#include <atomic>

std::atomic<int> counter{0};

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;  // Atomic increment (no mutex needed)
    }
}

std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();

std::cout << counter;  // Guaranteed to be 200000

Atomic Operations

std::atomic<int> x{10};

x.store(20);           // Atomic write
int val = x.load();    // Atomic read
int old = x.exchange(30);  // Atomic swap

// Compare-and-swap
int expected = 30;
bool success = x.compare_exchange_strong(expected, 40);
// If x == expected, sets x = 40 and returns true
// Otherwise, sets expected = x and returns false

Lock-Free Property

std::atomic<int> counter{0};

if (counter.is_lock_free()) {
    std::cout << "True atomic (no locks)\\n";
} else {
    std::cout << "Uses mutex internally\\n";
}

// Compile-time check (C++17)
static_assert(std::atomic<int>::is_always_lock_free);

Most standard types are lock-free on modern architectures:

  • atomic<int>, atomic<long>: Usually lock-free
  • atomic<std::string>: Not lock-free (too large)
  • atomic<bool>, atomic<char>: Always lock-free

Memory Ordering

The C++ memory model defines how operations on different threads are ordered. Six memory ordering modes:

1. memory_order_relaxed

No synchronization, only atomicity:

std::atomic<int> x{0}, y{0};

// Thread 1
x.store(1, std::memory_order_relaxed);
y.store(2, std::memory_order_relaxed);

// Thread 2
int a = y.load(std::memory_order_relaxed);
int b = x.load(std::memory_order_relaxed);

// a=2, b=0 is possible! (reordering allowed)

Use: Independent counters, statistics

2. memory_order_acquire and memory_order_release

Establishes happens-before relationship:

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

// Producer thread
data = 42;  // Non-atomic write
ready.store(true, std::memory_order_release);  // Release

// Consumer thread
while (!ready.load(std::memory_order_acquire)) {}  // Acquire
std::cout << data;  // Guaranteed to see 42

Release-acquire pair synchronizes: all writes before release are visible after acquire.

3. memory_order_acq_rel

Both acquire and release:

std::atomic<int> x{0};

int prev = x.fetch_add(1, std::memory_order_acq_rel);
// Acquires previous value, releases new value

4. memory_order_seq_cst (Default)

Sequentially consistent: total global order:

std::atomic<int> x{0}, y{0};

// Thread 1
x.store(1);  // Default: memory_order_seq_cst
y.store(2);

// Thread 2
int a = y.load();
int b = x.load();

// If a == 2, then b must == 1 (total order guaranteed)

Most expensive but easiest to reason about. Default for most operations.

5. memory_order_consume (Deprecated)

Like acquire but weaker (rarely used, complex semantics).

Compare-Exchange

Fundamental building block for lock-free algorithms:

std::atomic<int> value{0};

void increment() {
    int expected = value.load();
    while (!value.compare_exchange_weak(expected, expected + 1)) {
        // expected was updated to current value, retry
    }
}

Strong vs Weak CAS

// Strong: Only fails if value != expected
compare_exchange_strong(expected, desired);

// Weak: May spuriously fail even if value == expected
// (Faster on some architectures)
compare_exchange_weak(expected, desired);

Use _weak in loops (spurious failures don’t matter).

Atomic Flags

Simplest atomic type, guaranteed lock-free:

std::atomic_flag flag = ATOMIC_FLAG_INIT;

bool was_set = flag.test_and_set();  // Atomically set to true, return old value
flag.clear();  // Set to false

Spinlock Implementation

class spinlock {
    std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (flag_.test_and_set(std::memory_order_acquire)) {
            // Spin until lock acquired
        }
    }
    
    void unlock() {
        flag_.clear(std::memory_order_release);
    }
};

Warning: Spinlocks waste CPU cycles. Use for very short critical sections only.

Lock-Free Stack Example

template<typename T>
class lock_free_stack {
    struct Node {
        T data;
        Node* next;
    };
    
    std::atomic<Node*> head_{nullptr};
    
public:
    void push(const T& value) {
        Node* new_node = new Node{value, head_.load()};
        
        while (!head_.compare_exchange_weak(new_node->next, new_node)) {
            // Retry with updated head
        }
    }
    
    bool pop(T& result) {
        Node* old_head = head_.load();
        
        while (old_head && !head_.compare_exchange_weak(old_head, old_head->next)) {
            // Retry
        }
        
        if (old_head) {
            result = old_head->data;
            delete old_head;
            return true;
        }
        return false;
    }
};

Caution: Lock-free programming is extremely difficult. Prefer mutexes unless profiling shows they’re a bottleneck.

When to Use Atomics vs Mutexes

Use Atomics WhenUse Mutexes When
Single variableMultiple variables
Simple operationsComplex critical sections
Performance criticalSimplicity matters
Lock-free requiredStandard case
Conceptual Check

What is the default memory ordering for atomic operations?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <atomic>
3#include <thread>
4#include <vector>
5 
6std::atomic<long> counter{0};
7 
8void increment(int count) {
9 for (int i = 0; i < count; ++i) {
10 counter.fetch_add(1, std::memory_order_relaxed);
11 }
12}
13 
14int main() {
15 std::cout << "atomic<long> is lock-free: "
16 << std::boolalpha << counter.is_lock_free() << '\n';
17
18 std::vector<std::thread> threads;
19 for (int i = 0; i < 4; ++i) {
20 threads.emplace_back(increment, 25000);
21 }
22
23 for (auto& t : threads) t.join();
24
25 std::cout << "Counter: " << counter << '\n';
26 return 0;
27}
System Console

Waiting for signal...

Section Detail

Condition Variables and Futures

Advanced Thread Synchronization

Beyond mutexes and atomics, C++ provides higher-level synchronization primitives: condition variables for signaling between threads, and futures/promises for asynchronous result retrieval.

Condition Variables

Condition variables enable threads to wait for specific conditions without busy-waiting:

#include <condition_variable>
#include <mutex>
#include <queue>

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

void producer() {
    for (int i = 0; i < 10; ++i) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            queue.push(i);
        }
        cv.notify_one();  // Wake one waiting thread
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !queue.empty(); });  // Wait until queue not empty
        
        int value = queue.front();
        queue.pop();
        lock.unlock();
        
        std::cout << "Consumed: " << value << '\\n';
    }
}

Wait Mechanics

std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock);  // Atomically unlocks mtx and sleeps
                // When woken, reacquires mtx

// Equivalent to:
while (!predicate()) {
    cv.wait(lock);
}

Spurious Wakeups

Condition variables can wake spuriously (without notify). Always use predicate form:

// Bad: No protection against spurious wakeups
cv.wait(lock);
if (!queue.empty()) { /* ... */ }

// Good: Handles spurious wakeups
cv.wait(lock, []{ return !queue.empty(); });

notify_one vs notify_all

cv.notify_one();   // Wakes one waiting thread
cv.notify_all();   // Wakes all waiting threads

Use notify_all when multiple threads might need to act on the condition.

Timed Waits

using namespace std::chrono_literals;

std::unique_lock<std::mutex> lock(mtx);

if (cv.wait_for(lock, 1s, []{ return ready; })) {
    // Condition met within 1 second
} else {
    // Timeout
}

auto deadline = std::chrono::system_clock::now() + 5s;
if (cv.wait_until(lock, deadline, []{ return ready; })) {
    // Condition met before deadline
}

std::future and std::promise

Futures provide a mechanism to retrieve results from asynchronous operations:

promise-future Pair

#include <future>

void compute(std::promise<int> prom) {
    // Do expensive computation
    int result = 42;
    prom.set_value(result);  // Fulfill promise
}

std::promise<int> prom;
std::future<int> fut = prom.get_future();

std::thread t(compute, std::move(prom));

int result = fut.get();  // Blocks until promise fulfilled
std::cout << "Result: " << result << '\\n';

t.join();

Exception Propagation

void task(std::promise<int> prom) {
    try {
        throw std::runtime_error("Error!");
    } catch (...) {
        prom.set_exception(std::current_exception());
    }
}

std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(task, std::move(prom));

try {
    int result = fut.get();  // Rethrows exception
} catch (const std::exception& e) {
    std::cout << "Caught: " << e.what() << '\\n';
}

t.join();

std::async

Higher-level alternative to manual thread + promise:

std::future<int> fut = std::async(std::launch::async, []{
    // Runs in separate thread
    return 42;
});

int result = fut.get();  // Retrieve result

Launch Policies

// Force async execution (new thread)
auto f1 = std::async(std::launch::async, task);

// Defer execution until get() called (lazy)
auto f2 = std::async(std::launch::deferred, task);

// Implementation chooses (default)
auto f3 = std::async(task);

shared_future

Multiple threads can wait on the same result:

std::shared_future<int> sf = std::async(task).share();

auto lambda = [sf]{ return sf.get(); };

std::thread t1(lambda);
std::thread t2(lambda);
// Both can call get()

t1.join();
t2.join();

packaged_task

Wraps callable for future retrieval:

std::packaged_task<int(int, int)> task([](int a, int b) {
    return a + b;
});

std::future<int> result = task.get_future();

std::thread t(std::move(task), 3, 4);

std::cout << result.get();  // 7

t.join();

Producer-Consumer with Condition Variables

class ThreadSafeQueue {
    std::queue<int> queue_;
    mutable std::mutex mtx_;
    std::condition_variable cv_;
    
public:
    void push(int value) {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            queue_.push(value);
        }
        cv_.notify_one();
    }
    
    int pop() {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_.wait(lock, [this]{ return !queue_.empty(); });
        int value = queue_.front();
        queue_.pop();
        return value;
    }
    
    bool empty() const {
        std::lock_guard<std::mutex> lock(mtx_);
        return queue_.empty();
    }
};
Conceptual Check

Why must condition_variable::wait take a unique_lock instead of lock_guard?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <future>
3#include <thread>
4#include <chrono>
5 
6int compute(int x) {
7 std::this_thread::sleep_for(std::chrono::milliseconds(100));
8 return x * x;
9}
10 
11int main() {
12 // Async execution
13 auto fut1 = std::async(std::launch::async, compute, 5);
14 auto fut2 = std::async(std::launch::async, compute, 10);
15
16 std::cout << "Computing...\n";
17
18 std::cout << "Result 1: " << fut1.get() << '\n';
19 std::cout << "Result 2: " << fut2.get() << '\n';
20
21 return 0;
22}
System Console

Waiting for signal...

Modern C++ Features

Section Detail

std::optional and std::variant

std::optional: Representing Optional Values

C++17’s std::optional<T> represents a value that may or may not be present, avoiding special sentinel values or pointers.

Basic Usage

#include <optional>

std::optional<int> parse_int(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;  // No value
    }
}

auto result = parse_int("42");
if (result) {  // or result.has_value()
    std::cout << *result << '\\n';  // Dereference to get value
} else {
    std::cout << "Parse failed\\n";
}

Accessing Values

std::optional<int> opt = 42;

// Safe access with default
int value = opt.value_or(0);  // Returns 42, or 0 if empty

// Direct access (throws if empty)
int v1 = opt.value();  // Throws std::bad_optional_access if empty

// Unsafe access (UB if empty)
int v2 = *opt;  // Like pointer dereference

// Check before access
if (opt.has_value()) {
    std::cout << opt.value();
}

Creating Optionals

std::optional<int> empty;  // No value
std::optional<int> zero{0};  // Contains 0
std::optional<int> forty_two = 42;
std::optional<int> none = std::nullopt;

// In-place construction
std::optional<std::string> str{std::in_place, 10, 'x'};  // "xxxxxxxxxx"

Modifying Optionals

std::optional<int> opt;

opt = 42;  // Assign value
opt.reset();  // Clear value (now empty)
opt.emplace(100);  // Construct value in-place

Monadic Operations (C++23)

std::optional<int> opt = 42;

// and_then: Apply function returning optional
auto result = opt.and_then([](int x) -> std::optional<int> {
    return x > 0 ? std::optional{x * 2} : std::nullopt;
});

// transform: Apply function returning value
auto doubled = opt.transform([](int x) { return x * 2; });

// or_else: Provide alternative
auto val = std::optional<int>{}.or_else([]{ return std::optional{42}; });

Use Cases

class Config {
    std::optional<std::string> username_;
    std::optional<int> port_;
    
public:
    void set_username(const std::string& name) { username_ = name; }
    std::optional<std::string> get_username() const { return username_; }
    
    int get_port() const { return port_.value_or(8080); }  // Default port
};

std::variant: Type-Safe Unions

std::variant is a type-safe union that can hold one of several types at any time.

Basic Usage

#include <variant>

std::variant<int, double, std::string> value;

value = 42;  // Holds int
value = 3.14;  // Now holds double
value = "hello";  // Now holds string

// Access by index
int i = std::get<0>(value);  // Throws if not int

// Access by type (must be unique)
std::string s = std::get<std::string>(value);

// Safe access
if (auto ptr = std::get_if<std::string>(&value)) {
    std::cout << *ptr;
}

Visiting Variants

std::variant<int, double, std::string> v = 42;

std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "int: " << arg << '\\n';
    } else if constexpr (std::is_same_v<T, double>) {
        std::cout << "double: " << arg << '\\n';
    } else {
        std::cout << "string: " << arg << '\\n';
    }
}, v);

Overloaded Pattern

Helper for multiple lambdas:

template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };

template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;  // Deduction guide

std::variant<int, double, std::string> v = "hello";

std::visit(overloaded{
    [](int x) { std::cout << "int: " << x; },
    [](double x) { std::cout << "double: " << x; },
    [](const std::string& x) { std::cout << "string: " << x; }
}, v);

Variant State

std::variant<int, double> v = 3.14;

std::cout << v.index();  // 1 (double is second alternative)

if (std::holds_alternative<double>(v)) {
    std::cout << "Holds double\\n";
}

Error Handling with Variant

Alternative to exceptions:

struct Error {
    std::string message;
};

std::variant<int, Error> divide(int a, int b) {
    if (b == 0) {
        return Error{"Division by zero"};
    }
    return a / b;
}

auto result = divide(10, 2);

std::visit(overloaded{
    [](int value) { std::cout << "Result: " << value; },
    [](const Error& e) { std::cerr << "Error: " << e.message; }
}, result);

variant vs optional vs union

Featureoptionalvariantunion
Type SafetyYesYesNo
Knows TypeYesYesNo
Empty StateYesNo*N/A
Non-Trivial TypesYesYesNo

*variant has std::monostate for empty state

std::variant<std::monostate, int, double> v;  // Default: monostate (empty)
Conceptual Check

What's the key advantage of std::optional over using nullptr or -1 as sentinel values?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <optional>
3#include <variant>
4#include <string>
5 
6std::optional<int> safe_divide(int a, int b) {
7 if (b == 0) return std::nullopt;
8 return a / b;
9}
10 
11int main() {
12 auto r1 = safe_divide(10, 2);
13 std::cout << "10/2 = " << r1.value_or(-1) << '\n';
14
15 auto r2 = safe_divide(10, 0);
16 std::cout << "10/0 = " << r2.value_or(-1) << '\n';
17
18 std::variant<int, double, std::string> v = 42;
19 std::cout << "Variant holds index " << v.index() << '\n';
20
21 v = "hello";
22 std::visit([](auto&& arg) {
23 std::cout << "Value: " << arg << '\n';
24 }, v);
25
26 return 0;
27}
System Console

Waiting for signal...

Section Detail

String View and Span

Non-Owning Views: Efficient Abstractions

C++17/20 introduced view types that refer to existing data without owning it, avoiding unnecessary copies.

std::string_view (C++17)

Lightweight non-owning reference to a string:

#include <string_view>

void print(std::string_view sv) {  // No copy!
    std::cout << sv << '\\n';
}

std::string str = "hello";
const char* cstr = "world";
print(str);   // OK: string converts to string_view
print(cstr);  // OK: const char* converts to string_view
print("literal");  // OK: no temporary std::string created

Performance Advantage

// Bad: Creates temporary string for each call
void process(const std::string& s);
process("literal");  // Allocates memory!

// Good: No allocation
void process(std::string_view s);
process("literal");  // Just pointer + length

string_view Operations

std::string_view sv = "Hello, World!";

sv.size();     // 13
sv.length();   // 13
sv.empty();    // false
sv.data();     // Pointer to first character

sv[0];         // 'H'
sv.front();    // 'H'
sv.back();     // '!'

sv.substr(7, 5);  // "World"
sv.remove_prefix(7);  // sv = "World!"
sv.remove_suffix(1);  // sv = "World"

Lifetime Hazards

Critical: string_view doesn’t own data—ensure underlying data outlives the view:

std::string_view get_view() {
    std::string temp = "temporary";
    return temp;  // DANGER: Returns view to destroyed string!
}

auto sv = get_view();
std::cout << sv;  // UB: Dangling reference!

Safe usage:

std::string str = "persistent";
std::string_view sv = str;  // OK: str outlives sv

void func(std::string_view sv) {
    // OK: sv only used during function
}

std::span (C++20)

Non-owning view over contiguous sequence:

#include <span>

void process(std::span<int> data) {
    for (int& x : data) {
        x *= 2;
    }
}

std::vector<int> vec = {1, 2, 3, 4, 5};
int arr[] = {1, 2, 3, 4, 5};

process(vec);  // Works with vector
process(arr);  // Works with array

Compile-Time vs Runtime Extent

// Dynamic extent (size at runtime)
std::span<int> dynamic_span = vec;

// Fixed extent (size at compile time)
std::span<int, 5> fixed_span = arr;

// fixed_span.size() is constexpr
static_assert(fixed_span.size() == 5);

span Operations

std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp = vec;

sp.size();       // 5
sp.size_bytes(); // 5 * sizeof(int)
sp.empty();      // false
sp.data();       // Pointer to first element

sp[0] = 10;      // Modify through span
sp.front() = 20;
sp.back() = 30;

// Subspans
auto first3 = sp.first(3);   // First 3 elements
auto last2 = sp.last(2);     // Last 2 elements
auto middle = sp.subspan(1, 3);  // 3 elements starting at index 1

Read-Only Spans

void print(std::span<const int> data) {  // const elements
    for (int x : data) {
        std::cout << x << ' ';
    }
}

std::vector<int> vec = {1, 2, 3};
print(vec);  // OK: converts to span<const int>

Use Cases

Replacing Multiple Overloads

Before:

void process(const std::vector<int>& v);
void process(const std::array<int, 10>& a);
void process(const int* data, size_t size);

After:

void process(std::span<const int> data);  // Handles all cases

Safer C-Style Arrays

// Old: Easy to mismatch size
void process(const int* data, size_t size);

// New: Size is part of the span
void process(std::span<const int> data);

string_view vs const string&

Aspectstring_viewconst string&
Copy CostCheap (2 pointers)Depends (reference)
Temporary CreationNoYes (for literals)
Null-TerminationNot guaranteedGuaranteed
SubstringO(1)O(n) (copies)
Modify OriginalNoNo

Guideline: Use string_view for read-only string parameters, especially when substrings are common.

span vs vector&

Aspectspanvector&
TypeViewContainer
OwnershipNoYes
Works WithAny contiguousOnly vector
ResizeNoYes

Guideline: Use span for functions operating on contiguous data regardless of container type.

Conceptual Check

Why is std::string_view potentially dangerous?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <string_view>
3#include <span>
4#include <vector>
5 
6void print_sv(std::string_view sv) {
7 std::cout << "string_view: " << sv << " (size: " << sv.size() << ")\n";
8}
9 
10void double_values(std::span<int> data) {
11 for (int& x : data) {
12 x *= 2;
13 }
14}
15 
16int main() {
17 // string_view examples
18 print_sv("literal");
19 std::string str = "hello";
20 print_sv(str);
21
22 // span examples
23 std::vector<int> vec = {1, 2, 3, 4, 5};
24 double_values(vec);
25
26 std::cout << "After doubling: ";
27 for (int x : vec) std::cout << x << ' ';
28 std::cout << '\n';
29
30 return 0;
31}
System Console

Waiting for signal...

Section Detail

Structured Bindings and Init Statements

Structured Bindings: Decomposing Objects

C++17 structured bindings provide a concise syntax for unpacking tuples, pairs, arrays, and structs into individual variables.

Basic Syntax

std::pair<int, std::string> get_person() {
    return {42, "Alice"};
}

auto [id, name] = get_person();  // Structured binding
std::cout << id << ": " << name;  // 42: Alice

Binding Pairs and Tuples

std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};

for (const auto& [name, score] : scores) {
    std::cout << name << ": " << score << '\\n';
}

std::tuple<int, double, std::string> data{42, 3.14, "hello"};
auto [i, d, s] = data;

Binding Arrays

int arr[] = {1, 2, 3};
auto [a, b, c] = arr;  // a=1, b=2, c=3

// Number of bindings must match array size
// auto [x, y] = arr;  // Error: 3 elements, 2 bindings

Binding Structs

Works with any struct/class where all members are public:

struct Point {
    double x, y;
};

Point p{3.0, 4.0};
auto [x, y] = p;  // x=3.0, y=4.0

double distance = std::sqrt(x*x + y*y);

Reference Bindings

std::map<std::string, int> map = {{"key", 42}};

// Reference binding: can modify original
for (auto& [key, value] : map) {
    value *= 2;  // Modifies map
}

// Const reference: read-only
for (const auto& [key, value] : map) {
    std::cout << key << ": " << value << '\\n';
}

Ignoring Values

C++20 allows placeholder with [[maybe_unused]]:

auto [a, [[maybe_unused]] b, c] = std::tuple{1, 2, 3};
// b is not used but no warning

C++26 will support _ as placeholder:

auto [a, _, c] = std::tuple{1, 2, 3};  // C++26

Custom Types

Make custom types decomposable:

class Rectangle {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    
    // Tuple-like interface
    template<size_t I>
    double get() const {
        if constexpr (I == 0) return width_;
        else if constexpr (I == 1) return height_;
    }
};

// Specializations for std::tuple_size and std::tuple_element
namespace std {
    template<>
    struct tuple_size<Rectangle> : integral_constant<size_t, 2> {};
    
    template<size_t I>
    struct tuple_element<I, Rectangle> { using type = double; };
}

Rectangle r{3.0, 4.0};
auto [w, h] = r;  // w=3.0, h=4.0

Init Statements in if and switch (C++17)

Declare and initialize variables in the condition statement with tighter scope.

if with Init Statement

if (auto it = map.find(key); it != map.end()) {
    std::cout << it->second;
    // it is in scope here
}
// it is NOT in scope here

Before C++17:

auto it = map.find(key);  // it pollutes outer scope
if (it != map.end()) {
    std::cout << it->second;
}

switch with Init Statement

switch (auto status = get_status(); status) {
    case Status::OK:
        // use status
        break;
    case Status::ERROR:
        // use status
        break;
}
// status not in scope

Structured Binding in if

Combine init statement with structured binding:

if (auto [iter, inserted] = map.insert({key, value}); inserted) {
    std::cout << "Inserted: " << iter->first << '\\n';
} else {
    std::cout << "Already exists\\n";
}

Range-Based for Init Statement (C++20)

for (auto v = init_value(); auto& elem : container) {
    // v and elem both in scope
    process(v, elem);
}

Use Cases

Lock and Check

if (std::lock_guard lock(mutex); condition()) {
    // Protected section
}
// Lock released

Optional Chaining

if (auto opt = get_optional(); opt.has_value()) {
    std::cout << *opt;
}

Error Handling

if (auto [result, error] = perform_operation(); error) {
    handle_error(error);
} else {
    use_result(result);
}
Conceptual Check

What's the main benefit of init statements in if/switch?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <map>
3#include <tuple>
4 
5struct Point { double x, y; };
6 
7int main() {
8 // Structured bindings with tuple
9 auto [a, b, c] = std::tuple{1, 2.5, "hello"};
10 std::cout << "Tuple: " << a << ", " << b << ", " << c << '\n';
11
12 // Structured bindings with struct
13 Point p{3.0, 4.0};
14 auto [x, y] = p;
15 std::cout << "Point: (" << x << ", " << y << ")\n";
16
17 // Init statement in if
18 std::map<std::string, int> scores{{"Alice", 95}};
19
20 if (auto it = scores.find("Alice"); it != scores.end()) {
21 std::cout << "Alice's score: " << it->second << '\n';
22 }
23
24 // Structured binding in loop
25 for (const auto& [name, score] : scores) {
26 std::cout << name << " scored " << score << '\n';
27 }
28
29 return 0;
30}
System Console

Waiting for signal...

Standard Library

Section Detail

Filesystem Library

std::filesystem: Portable File Operations

C++17 introduced <filesystem> for portable, type-safe file and directory operations, replacing platform-specific APIs.

Path Manipulation

#include <filesystem>
namespace fs = std::filesystem;

fs::path p = "/home/user/document.txt";

p.filename();     // "document.txt"
p.stem();         // "document"
p.extension();    // ".txt"
p.parent_path();  // "/home/user"
p.root_path();    // "/"

// Path concatenation
fs::path dir = "/home/user";
fs::path file = "document.txt";
fs::path full = dir / file;  // "/home/user/document.txt"

Path Components

fs::path p = "/home/user/docs/file.txt";

for (const auto& part : p) {
    std::cout << part << ' ';
}
// Output: "/" "home" "user" "docs" "file.txt"

Checking File Status

fs::path p = "some/path";

fs::exists(p);          // Does path exist?
fs::is_regular_file(p); // Is it a regular file?
fs::is_directory(p);    // Is it a directory?
fs::is_symlink(p);      // Is it a symbolic link?
fs::is_empty(p);        // Is file empty or directory empty?

// File size
auto size = fs::file_size(p);  // In bytes

// Last modification time
auto time = fs::last_write_time(p);

Directory Iteration

// Iterate over directory contents
for (const auto& entry : fs::directory_iterator("/path/to/dir")) {
    std::cout << entry.path() << '\n';
}

// Recursive iteration
for (const auto& entry : fs::recursive_directory_iterator("/path/to/dir")) {
    if (fs::is_regular_file(entry)) {
        std::cout << entry.path() << ": " << fs::file_size(entry) << " bytes\n";
    }
}

File Operations

// Copy file
fs::copy("source.txt", "dest.txt");

// Copy with options
fs::copy("source.txt", "dest.txt", 
         fs::copy_options::overwrite_existing);

// Rename/move
fs::rename("old_name.txt", "new_name.txt");

// Remove file
fs::remove("file.txt");

// Remove directory and contents
fs::remove_all("directory");

// Create directory
fs::create_directory("new_dir");

// Create directory and parents
fs::create_directories("path/to/deep/dir");

File Permissions

fs::path p = "file.txt";

// Get permissions
auto perms = fs::status(p).permissions();

// Set permissions
fs::permissions(p, fs::perms::owner_read | fs::perms::owner_write);

// Add permissions
fs::permissions(p, fs::perms::others_read, fs::perm_options::add);

// Remove permissions
fs::permissions(p, fs::perms::others_write, fs::perm_options::remove);

Space Information

fs::space_info info = fs::space("/");

std::cout << "Capacity: " << info.capacity << " bytes\n";
std::cout << "Free: " << info.free << " bytes\n";
std::cout << "Available: " << info.available << " bytes\n";

Current Path

// Get current working directory
fs::path cwd = fs::current_path();

// Set current working directory
fs::current_path("/new/path");

Temporary Directory

fs::path temp = fs::temp_directory_path();
fs::path temp_file = temp / "my_temp_file.txt";

Error Handling

// Exception-based
try {
    fs::copy("source.txt", "dest.txt");
} catch (const fs::filesystem_error& e) {
    std::cerr << e.what() << '\n';
    std::cerr << "Path1: " << e.path1() << '\n';
    std::cerr << "Path2: " << e.path2() << '\n';
}

// Error code-based
std::error_code ec;
fs::copy("source.txt", "dest.txt", ec);
if (ec) {
    std::cerr << "Error: " << ec.message() << '\n';
}

Practical Examples

Find All Files with Extension

std::vector<fs::path> find_files(const fs::path& dir, const std::string& ext) {
    std::vector<fs::path> result;
    
    for (const auto& entry : fs::recursive_directory_iterator(dir)) {
        if (entry.path().extension() == ext) {
            result.push_back(entry.path());
        }
    }
    
    return result;
}

auto cpp_files = find_files("/project", ".cpp");

Calculate Directory Size

uintmax_t directory_size(const fs::path& dir) {
    uintmax_t size = 0;
    
    for (const auto& entry : fs::recursive_directory_iterator(dir)) {
        if (fs::is_regular_file(entry)) {
            size += fs::file_size(entry);
        }
    }
    
    return size;
}

Safe File Backup

void backup_file(const fs::path& source) {
    if (!fs::exists(source)) return;
    
    fs::path backup = source;
    backup += ".bak";
    
    if (fs::exists(backup)) {
        fs::remove(backup);
    }
    
    fs::copy(source, backup);
}
Conceptual Check

What operator is used to concatenate filesystem paths?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <filesystem>
3 
4namespace fs = std::filesystem;
5 
6int main() {
7 fs::path p = "/home/user/document.txt";
8
9 std::cout << "Full path: " << p << '\n';
10 std::cout << "Filename: " << p.filename() << '\n';
11 std::cout << "Stem: " << p.stem() << '\n';
12 std::cout << "Extension: " << p.extension() << '\n';
13 std::cout << "Parent: " << p.parent_path() << '\n';
14
15 // Path concatenation
16 fs::path dir = "/tmp";
17 fs::path file = "test.txt";
18 fs::path full = dir / file;
19 std::cout << "Concatenated: " << full << '\n';
20
21 // Get temp directory
22 std::cout << "Temp directory: " << fs::temp_directory_path() << '\n';
23
24 return 0;
25}
System Console

Waiting for signal...

Modern C++20/23

Section Detail

Concepts and Constraints (C++20)

Concepts: Constraining Templates

C++20 concepts provide a way to specify requirements on template parameters, replacing complex SFINAE with readable constraints.

Basic Concept Syntax

#include <concepts>

template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

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

add(5, 10);      // OK: int is Numeric
add(3.14, 2.71); // OK: double is Numeric
// add("hello", "world");  // Error: const char* is not Numeric

Standard Library Concepts

#include <concepts>

// Fundamental concepts
std::integral<int>;           // Integer types
std::floating_point<double>;  // Floating-point types
std::signed_integral<int>;    // Signed integers
std::unsigned_integral<size_t>;  // Unsigned integers

// Comparison concepts
std::equality_comparable<std::string>;
std::totally_ordered<int>;  // Has <, >, <=, >=, ==, !=

// Object concepts
std::movable<std::unique_ptr<int>>;
std::copyable<std::string>;
std::semiregular<int>;  // Default constructible and copyable
std::regular<int>;      // Semiregular and equality comparable

Defining Custom Concepts

template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;  // Must support +
};

template<typename T>
concept Container = requires(T t) {
    typename T::value_type;    // Must have value_type
    { t.begin() };              // Must have begin()
    { t.end() };                // Must have end()
    { t.size() } -> std::convertible_to<size_t>;  // size() returns size_t-like
};

template<Container C>
void process(const C& container) {
    for (const auto& elem : container) {
        // ...
    }
}

requires Clauses

Trailing requires

template<typename T>
T max(T a, T b) requires std::totally_ordered<T> {
    return (a > b) ? a : b;
}

Constrained auto

std::integral auto square(std::integral auto x) {
    return x * x;
}

square(5);    // OK
// square(3.14);  // Error: double is not integral

Combining Concepts

template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;

template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;

template<typename T>
concept Range = requires(T t) {
    t.begin();
    t.end();
};

requires Expression

template<typename T>
concept Drawable = requires(T t) {
    { t.draw() } -> std::same_as<void>;  // Must have void draw()
    { t.position() } -> std::convertible_to<std::pair<int, int>>;
    t.visible;  // Must have visible member
};

template<typename T>
concept Arithmetic = requires(T a, T b) {
    a + b;
    a - b;
    a * b;
    a / b;
};

Nested Requirements

template<typename T>
concept Complex = requires(T t) {
    requires std::regular<T>;  // Must be regular
    requires sizeof(T) >= 8;   // Size requirement
    { t.real() } -> std::floating_point;
    { t.imag() } -> std::floating_point;
};

Subsumption

More constrained concept is preferred:

template<typename T>
concept Integral = std::is_integral_v<T>;

template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;

void process(Integral auto x) {
    std::cout << "Integral\\n";
}

void process(SignedIntegral auto x) {  // More constrained
    std::cout << "Signed integral\\n";
}

process(5);    // Calls SignedIntegral version
process(5u);   // Calls Integral version

Concept-Based Overloading

void print(std::integral auto x) {
    std::cout << "Integer: " << x << '\\n';
}

void print(std::floating_point auto x) {
    std::cout << "Float: " << x << '\\n';
}

print(42);    // Calls integral version
print(3.14);  // Calls floating_point version

Debugging Concept Failures

template<typename T>
concept Sortable = requires(T t) {
    { t.begin() } -> std::random_access_iterator;
    { t.end() } -> std::random_access_iterator;
};

std::list<int> lst;
// std::sort(lst.begin(), lst.end());  
// Clear error: list iterators are not random_access_iterator

Abbreviated Function Templates

// Before concepts
template<typename T>
void func(T x);

// With concepts (C++20)
void func(auto x);  // Unconstrained

void func(std::integral auto x);  // Constrained

// Multiple parameters
void func2(std::integral auto x, std::floating_point auto y);

Practical Example: Generic Algorithm

template<typename Range, typename Pred>
    requires std::ranges::range<Range> && 
             std::predicate<Pred, std::ranges::range_value_t<Range>>
auto filter(Range&& r, Pred p) {
    std::vector<std::ranges::range_value_t<Range>> result;
    for (auto&& elem : r) {
        if (p(elem)) {
            result.push_back(elem);
        }
    }
    return result;
}
Conceptual Check

What advantage do concepts have over SFINAE?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <concepts>
3#include <vector>
4 
5template<typename T>
6concept Numeric = std::integral<T> || std::floating_point<T>;
7 
8template<Numeric T>
9T multiply(T a, T b) {
10 return a * b;
11}
12 
13// Constrained auto
14auto add(std::integral auto a, std::integral auto b) {
15 return a + b;
16}
17 
18int main() {
19 std::cout << multiply(5, 10) << '\n';
20 std::cout << multiply(3.14, 2.0) << '\n';
21 std::cout << add(100, 200) << '\n';
22
23 // Compile-time check
24 static_assert(Numeric<int>);
25 static_assert(Numeric<double>);
26 static_assert(!Numeric<std::string>);
27
28 std::cout << "All concept checks passed\n";
29
30 return 0;
31}
System Console

Waiting for signal...

Section Detail

Ranges Library (C++20)

Ranges: Composable Algorithms

C++20 ranges revolutionize how we work with sequences, enabling composable, lazy-evaluated operations with cleaner syntax.

Range-Based Algorithms

#include <ranges>
#include <algorithm>
namespace rng = std::ranges;

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

// Old style
std::sort(vec.begin(), vec.end());

// Range style
rng::sort(vec);  // Operates on entire range

// With projection
struct Person { std::string name; int age; };
std::vector<Person> people = /*...*/;

rng::sort(people, {}, &Person::age);  // Sort by age

Views: Lazy Evaluation

Views are lightweight range adaptors that don’t own data and evaluate lazily:

#include <ranges>
namespace views = std::views;

std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// filter: Select elements matching predicate
auto even = vec | views::filter([](int x) { return x % 2 == 0; });
// No computation yet - lazy!

// transform: Apply function to each element
auto squared = vec | views::transform([](int x) { return x * x; });

// take: First n elements
auto first_three = vec | views::take(3);

// drop: Skip first n elements
auto skip_two = vec | views::drop(2);

Composing Views

Views compose using the pipe operator:

auto result = vec
    | views::filter([](int x) { return x % 2 == 0; })
    | views::transform([](int x) { return x * x; })
    | views::take(3);

for (int x : result) {
    std::cout << x << ' ';  // 4 16 36
}
// Computation happens here during iteration

Common Views

views::iota

Generate sequence of numbers:

// Infinite sequence: 1, 2, 3, ...
for (int i : views::iota(1) | views::take(5)) {
    std::cout << i << ' ';  // 1 2 3 4 5
}

// Bounded sequence
for (int i : views::iota(1, 6)) {
    std::cout << i << ' ';  // 1 2 3 4 5
}

views::reverse

std::vector<int> vec = {1, 2, 3, 4, 5};

for (int x : vec | views::reverse) {
    std::cout << x << ' ';  // 5 4 3 2 1
}

views::keys and views::values

For associative containers:

std::map<std::string, int> map = {{"a", 1}, {"b", 2}, {"c", 3}};

for (const auto& key : map | views::keys) {
    std::cout << key << ' ';  // a b c
}

for (int value : map | views::values) {
    std::cout << value << ' ';  // 1 2 3
}

views::split

std::string str = "hello,world,test";

for (auto word : str | views::split(',')) {
    for (char c : word) {
        std::cout << c;
    }
    std::cout << '\\n';
}
// hello
// world
// test

views::join

Flatten range of ranges:

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

for (int x : nested | views::join) {
    std::cout << x << ' ';  // 1 2 3 4 5 6
}

views::zip (C++23)

Combine multiple ranges:

std::vector<int> nums = {1, 2, 3};
std::vector<std::string> words = {"one", "two", "three"};

for (auto [num, word] : views::zip(nums, words)) {
    std::cout << num << ": " << word << '\\n';
}
// 1: one
// 2: two
// 3: three

Materializing Views

Convert views to containers:

auto view = vec 
    | views::filter([](int x) { return x % 2 == 0; })
    | views::transform([](int x) { return x * 2; });

// Convert to vector
std::vector<int> result(view.begin(), view.end());

// Or use ranges::to (C++23)
auto result2 = view | ranges::to<std::vector>();

Owning Views

views::all

Create view of existing range:

std::vector<int> vec = {1, 2, 3, 4, 5};
auto v = views::all(vec);

ranges::ref_view

Non-owning view (reference):

std::vector<int> vec = {1, 2, 3};
ranges::ref_view view(vec);

ranges::owning_view

Takes ownership of range:

auto owned = ranges::owning_view(std::vector{1, 2, 3});

Range Adaptors

Filtering and Transforming

std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Filter odd numbers, square them, take first 3
auto result = numbers
    | views::filter([](int x) { return x % 2 == 1; })
    | views::transform([](int x) { return x * x; })
    | views::take(3);

// Result: 1, 9, 25

Reverse and Drop

auto result = numbers
    | views::reverse
    | views::drop(2)
    | views::take(3);

// Start from end, skip 2, take 3: 8, 7, 6

Projection Support

Range algorithms accept projections:

struct Person {
    std::string name;
    int age;
};

std::vector<Person> people = {
    {"Alice", 30},
    {"Bob", 25},
    {"Charlie", 35}
};

// Find person with age > 30
auto it = rng::find_if(people, [](int age) { return age > 30; }, &Person::age);

// Sort by name
rng::sort(people, {}, &Person::name);

Practical Examples

Processing Text Lines

std::string text = "line1\\nline2\\nline3\\nline4";

for (auto line : text | views::split('\\n') | views::take(2)) {
    for (char c : line) std::cout << c;
    std::cout << '\\n';
}

Generating Fibonacci Sequence

auto fibonacci = views::iota(0)
    | views::transform([](int n) {
        // ... compute nth fibonacci
    })
    | views::take(10);
Conceptual Check

What is the key advantage of ranges views over eager evaluation?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <ranges>
4 
5namespace views = std::views;
6 
7int main() {
8 std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
9
10 // Compose operations
11 auto result = nums
12 | views::filter([](int x) { return x % 2 == 0; })
13 | views::transform([](int x) { return x * x; })
14 | views::take(3);
15
16 std::cout << "Even squares (first 3): ";
17 for (int x : result) {
18 std::cout << x << ' ';
19 }
20 std::cout << '\n';
21
22 // iota with reverse
23 std::cout << "Countdown: ";
24 for (int x : views::iota(1, 6) | views::reverse) {
25 std::cout << x << ' ';
26 }
27 std::cout << '\n';
28
29 return 0;
30}
System Console

Waiting for signal...

Section Detail

Coroutines Basics (C++20)

Coroutines: Suspendable Functions

C++20 coroutines are functions that can suspend execution and resume later, enabling asynchronous programming and lazy generators without callbacks.

Coroutine Keywords

A function is a coroutine if it contains any of:

  • co_await: Suspend until expression completes
  • co_yield: Suspend and produce a value
  • co_return: Complete and optionally return a value
generator<int> counter() {
    for (int i = 0; i < 5; ++i) {
        co_yield i;  // Suspend and produce value
    }
}

Basic Generator Example

Simple generator coroutine:

#include <coroutine>
#include <iostream>

template<typename T>
struct generator {
    struct promise_type {
        T current_value;
        
        generator get_return_object() {
            return generator{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(T value) {
            current_value = value;
            return {};
        }
        
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    
    std::coroutine_handle<promise_type> coro;
    
    generator(std::coroutine_handle<promise_type> h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }
    
    bool next() {
        coro.resume();
        return !coro.done();
    }
    
    T value() {
        return coro.promise().current_value;
    }
};

generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

// Usage:
auto gen = range(0, 5);
while (gen.next()) {
    std::cout << gen.value() << '\\n';
}

Coroutine Mechanics

Promise Type

Every coroutine requires a promise_type that defines behavior:

struct promise_type {
    // Called to create the coroutine's return object
    ReturnType get_return_object();
    
    // Should coroutine suspend before executing body?
    auto initial_suspend();  // Returns suspend_always or suspend_never
    
    // Should coroutine suspend after completing?
    auto final_suspend() noexcept;
    
    // Handle co_yield value
    auto yield_value(T value);
    
    // Handle co_return
    void return_void();
    // OR
    void return_value(T value);
    
    // Exception handling
    void unhandled_exception();
};

Coroutine Handle

Manages coroutine state:

std::coroutine_handle<PromiseType> handle;

handle.resume();   // Resume coroutine execution
handle.done();     // Check if coroutine completed
handle.destroy();  // Destroy coroutine state
handle.promise();  // Access promise object

co_await Expression

Suspend until awaitable completes:

Task async_operation() {
    auto result = co_await some_async_call();
    // Resumed when async_call completes
    co_return result;
}

Awaitable Type

struct awaitable {
    bool await_ready();  // Can we skip suspension?
    void await_suspend(std::coroutine_handle<>);  // Called when suspending
    T await_resume();    // Called when resuming, returns value
};

Infinite Generator

generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

auto fib = fibonacci();
for (int i = 0; i < 10; ++i) {
    fib.next();
    std::cout << fib.value() << ' ';
}
// 0 1 1 2 3 5 8 13 21 34

Lazy Evaluation

Coroutines enable lazy evaluation—values computed on demand:

generator<int> lazy_transform(const std::vector<int>& vec) {
    for (int x : vec) {
        co_yield x * x;  // Computed only when requested
    }
}

Async Task Pattern

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

Task async_work() {
    co_await some_operation();
    co_await another_operation();
    co_return;
}

Range-Based for with Coroutines

Make generator iterable:

template<typename T>
struct generator {
    // ... previous code ...
    
    struct iterator {
        std::coroutine_handle<promise_type> coro;
        
        iterator& operator++() {
            coro.resume();
            return *this;
        }
        
        bool operator!=(const iterator&) const {
            return !coro.done();
        }
        
        T operator*() const {
            return coro.promise().current_value;
        }
    };
    
    iterator begin() {
        coro.resume();
        return {coro};
    }
    
    iterator end() {
        return {nullptr};
    }
};

// Now works with range-based for:
for (int x : range(0, 5)) {
    std::cout << x << ' ';
}

Coroutine State

Coroutine state is heap-allocated (can be optimized away):

// State includes:
// - Parameters
// - Local variables
// - Promise object
// - Suspension point information

Compared to Traditional Approaches

ApproachCallbacksState MachinesCoroutines
ReadabilityPoorMediumExcellent
ComplexityHighHighLow
Error HandlingDifficultComplexNatural
PerformanceFastFastFast
Conceptual Check

What makes a function a coroutine?

Runtime Environment

Interactive Lab

1#include <iostream>
2 
3// Simplified generator for demonstration
4struct SimpleGen {
5 int current = 0;
6 int end;
7
8 SimpleGen(int e) : end(e) {}
9
10 bool next() {
11 if (current < end) {
12 ++current;
13 return true;
14 }
15 return false;
16 }
17
18 int value() const { return current - 1; }
19};
20 
21// Simulates generator behavior without actual coroutines
22SimpleGen simulate_range(int start, int end) {
23 return SimpleGen(end - start);
24}
25 
26int main() {
27 std::cout << "Simulated coroutine generator:\n";
28 auto gen = simulate_range(0, 5);
29
30 while (gen.next()) {
31 std::cout << gen.value() << ' ';
32 }
33 std::cout << '\n';
34
35 return 0;
36}
System Console

Waiting for signal...

Section Detail

Modules (C++20)

Modules: Modern Code Organization

C++20 modules replace the preprocessor-based #include system with a semantic import mechanism, offering faster compilation and better encapsulation.

Basic Module Syntax

Module Interface

// math.cppm (module interface file)
export module math;

export int add(int a, int b) {
    return a + b;
}

export int subtract(int a, int b) {
    return a - b;
}

Importing a Module

// main.cpp
import math;

int main() {
    auto result = add(5, 3);  // Uses exported add()
    return 0;
}

Export Declarations

Exporting Functions

export module geometry;

export namespace geometry {
    double circle_area(double radius);
    double square_area(double side);
}

Exporting Classes

export module shapes;

export class Circle {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area() const;
};

// Implementation can be in same file or separate
double Circle::area() const {
    return 3.14159 * radius_ * radius_;
}

Exporting Templates

export module containers;

export template<typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(const T& value) { data_.push_back(value); }
    T pop();
};

Module Partitions

Split large modules into partitions:

// math-basics.cppm (partition interface)
export module math:basics;

export int add(int a, int b) {
    return a + b;
}

// math-advanced.cppm (partition interface)
export module math:advanced;

export double sqrt(double x) {
    // implementation
}

// math.cppm (primary module interface)
export module math;

export import :basics;    // Re-export partition
export import :advanced;

Implementation Units

Separate interface from implementation:

// widget.cppm (module interface)
export module widget;

export class Widget {
public:
    void process();
private:
    void internal_helper();
};

// widget.cpp (module implementation)
module widget;  // No 'export'

void Widget::process() {
    internal_helper();
}

void Widget::internal_helper() {
    // Implementation details
}

Module Linkage

Exported vs Non-Exported

export module utils;

// Exported: Visible to importers
export void public_function();

// Not exported: Module-internal only
void private_function();

namespace {
    // Internal linkage
    void internal_function();
}

Global Module Fragment

Import legacy headers:

module;  // Global module fragment

#include <vector>
#include <string>

export module mymodule;

export class MyClass {
    std::vector<std::string> data_;  // Uses std types
};

Private Module Fragment

Hide implementation details completely:

export module widget;

export class Widget {
public:
    void process();
};

module :private;  // Private fragment

// Implementation hidden from importers
void Widget::process() {
    // ...
}

// Helper functions not visible
void helper() {
    // ...
}

Advantages Over Headers

1. Faster Compilation

// With headers: Reparse every #include in every TU
#include <vector>  // Parsed in every .cpp file

// With modules: Parse once, import everywhere
import std;  // Pre-compiled module

2. No Macro Pollution

// Header leaks macros
#define MAX(a,b) ((a)>(b)?(a):(b))

// Module doesn't leak implementation details
export module math;
// Macros don't leak to importers

3. Order Independence

// Headers: Order matters
#include "b.h"  // Must come before a.h sometimes
#include "a.h"

// Modules: Order doesn't matter
import a;
import b;

4. Better Encapsulation

// Module interface
export module database;

export class Connection {
    // Only public interface exported
};

// Private implementation not exposed

Importing Standard Library

import std;  // Import entire standard library (C++23)

import std.core;  // Core utilities
import std.io;    // I/O facilities
import std.regex; // Regular expressions

Module vs Header Comparison

AspectHeadersModules
ParsingEvery TUOnce
MacrosLeakDon’t leak
OrderMattersIndependent
ODR ViolationsPossiblePrevented
Compile TimeSlowFast

Migration Strategy

Gradual adoption:

// 1. Start with new code
export module new_feature;

// 2. Wrap existing headers
export module legacy_wrapper;
export {
    #include "old_header.h"
}

// 3. Eventually refactor to pure modules

Best Practices

  1. One module per library: Don’t create too many small modules
  2. Use partitions for large modules
  3. Export minimal interface: Keep internals private
  4. Avoid global module fragment when possible
  5. Prefer module imports over header includes

Compilation Model

# Compile module interface (generates BMI - Binary Module Interface)
g++ -std=c++20 -c math.cppm -o math.o

# Compile code that imports module
g++ -std=c++20 -c main.cpp -o main.o

# Link
g++ math.o main.o -o program
Conceptual Check

What is the primary advantage of modules over headers?

Interactive Lab

Module Export

 module math;

export int multiply(int a, int b) {
    return a * b;
}
Section Detail

Three-Way Comparison (C++20)

The Spaceship Operator (C++20)

C++20 introduces the three-way comparison operator <=> (nicknamed “spaceship”), dramatically simplifying comparison operator implementation.

Basic Syntax

#include <compare>

class Point {
    int x_, y_;
public:
    Point(int x, int y) : x_(x), y_(y) {}
    
    // Default three-way comparison
    auto operator<=>(const Point&) const = default;
};

Point p1{1, 2}, p2{3, 4};

p1 < p2;   // Automatically works
p1 <= p2;  // Automatically works
p1 > p2;   // Automatically works
p1 >= p2;  // Automatically works
p1 == p2;  // Requires separate == or also defaulted
p1 != p2;  // Generated from ==

Comparison Categories

The spaceship operator returns one of three ordering types:

1. strong_ordering

Total order with substitutability:

std::strong_ordering::less
std::strong_ordering::equal
std::strong_ordering::greater
std::strong_ordering::equivalent  // Same as equal

Use when equal values are truly identical:

class Integer {
    int value_;
public:
    std::strong_ordering operator<=>(const Integer& other) const {
        return value_ <=> other.value_;
    }
};

2. weak_ordering

Total order without substitutability:

std::weak_ordering::less
std::weak_ordering::equivalent
std::weak_ordering::greater

Use when equal values may not be substitutable (e.g., case-insensitive strings):

class CaseInsensitiveString {
    std::string value_;
public:
    std::weak_ordering operator<=>(const CaseInsensitiveString& other) const {
        // Compare ignoring case
        // "Hello" and "hello" are equivalent but not equal
    }
};

3. partial_ordering

May have incomparable values:

std::partial_ordering::less
std::partial_ordering::equivalent
std::partial_ordering::greater
std::partial_ordering::unordered  // Not comparable

Use for types like floating-point (NaN is unordered):

double a = std::nan(""), b = 1.0;
auto cmp = a <=> b;  // Returns partial_ordering::unordered

Defaulted Spaceship

The compiler can generate <=> for you:

struct Person {
    std::string name;
    int age;
    
    auto operator<=>(const Person&) const = default;
    // Compares members lexicographically: first name, then age
};

Member-wise Comparison Order

Comparison happens in declaration order:

struct Record {
    int priority;     // Compared first
    std::string name; // Compared second if priority equal
    double value;     // Compared third if name equal
    
    auto operator<=>(const Record&) const = default;
};

Custom Spaceship

Implement custom logic:

class Version {
    int major_, minor_, patch_;
public:
    std::strong_ordering operator<=>(const Version& other) const {
        if (auto cmp = major_ <=> other.major_; cmp != 0)
            return cmp;
        if (auto cmp = minor_ <=> other.minor_; cmp != 0)
            return cmp;
        return patch_ <=> other.patch_;
    }
};

Equality vs Spaceship

For efficiency, you might want to define == separately:

class String {
    std::string data_;
public:
    // Fast equality check
    bool operator==(const String& other) const {
        return data_ == other.data_;
    }
    
    // Three-way comparison for ordering
    std::strong_ordering operator<=>(const String& other) const {
        return data_ <=> other.data_;
    }
};

Default Both

Common pattern: default both operators:

struct Point {
    int x, y;
    
    bool operator==(const Point&) const = default;
    auto operator<=>(const Point&) const = default;
};

Heterogeneous Comparisons

Compare different types:

class Temperature {
    double celsius_;
public:
    explicit Temperature(double c) : celsius_(c) {}
    
    std::partial_ordering operator<=>(double fahrenheit) const {
        double c = (fahrenheit - 32) * 5/9;
        return celsius_ <=> c;
    }
};

Temperature t(100);
t < 212.0;  // Compare Celsius with Fahrenheit

Compiler-Generated Operators

With defaulted <=>, you get all six comparison operators:

struct Data {
    int value;
    auto operator<=>(const Data&) const = default;
};

// Generated automatically:
// <, <=, >, >=
// Also need == for these:
bool operator==(const Data&) const = default;
// Then != is also generated

Pre-C++20 vs C++20

Before C++20

class Point {
    int x_, y_;
public:
    bool operator==(const Point& other) const {
        return x_ == other.x_ && y_ == other.y_;
    }
    bool operator!=(const Point& other) const {
        return !(*this == other);
    }
    bool operator<(const Point& other) const {
        return (x_ < other.x_) || (x_ == other.x_ && y_ < other.y_);
    }
    bool operator<=(const Point& other) const {
        return !(other < *this);
    }
    bool operator>(const Point& other) const {
        return other < *this;
    }
    bool operator>=(const Point& other) const {
        return !(*this < other);
    }
};

With C++20

class Point {
    int x_, y_;
public:
    auto operator<=>(const Point&) const = default;
    bool operator==(const Point&) const = default;
};
// Done! All six operators generated

Special Cases

Comparison with std::tie

Before C++20 idiom:

bool operator<(const Point& other) const {
    return std::tie(x_, y_) < std::tie(other.x_, other.y_);
}

C++20 makes this unnecessary:

auto operator<=>(const Point&) const = default;
Conceptual Check

What does the spaceship operator return?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <compare>
3 
4struct Point {
5 int x, y;
6
7 auto operator<=>(const Point&) const = default;
8 bool operator==(const Point&) const = default;
9};
10 
11int main() {
12 Point p1{1, 2}, p2{3, 4}, p3{1, 2};
13
14 std::cout << std::boolalpha;
15 std::cout << "p1 < p2: " << (p1 < p2) << '\n';
16 std::cout << "p1 == p3: " << (p1 == p3) << '\n';
17 std::cout << "p2 > p1: " << (p2 > p1) << '\n';
18 std::cout << "p1 != p2: " << (p1 != p2) << '\n';
19
20 return 0;
21}
System Console

Waiting for signal...

Section Detail

Designated Initializers (C++20)

Designated Initializers

C++20 introduces designated initializers from C99, allowing you to initialize struct members by name. This improves clarity and maintainability, especially for structs with many members.

Basic Syntax

struct Config {
    int width;
    int height;
    bool fullscreen;
    double scale;
};

// Traditional initialization
Config c1{1920, 1080, true, 2.0};  // What does each value mean?

// C++20 designated initializers
Config c2{
    .width = 1920,
    .height = 1080,
    .fullscreen = true,
    .scale = 2.0
};  // Crystal clear!

Partial Initialization

You can initialize only specific members:

struct Settings {
    int timeout = 30;       // Default value
    bool debug = false;
    std::string host = "localhost";
};

Settings s{.debug = true};  // Only set debug, others use defaults
// timeout=30, debug=true, host="localhost"

Order Requirement

C++ requires designated initializers to match declaration order:

struct Point {
    int x;
    int y;
    int z;
};

Point p1{.x = 1, .y = 2, .z = 3};  // OK: follows declaration order
Point p2{.x = 1, .z = 3, .y = 2};  // ERROR: out of order!
Point p3{.y = 2, .z = 3};          // OK: can skip x

This differs from C99, which allows any order.

Cannot Mix with Positional

struct Data {
    int a, b, c;
};

Data d1{1, .b = 2, .c = 3};  // ERROR: cannot mix
Data d2{.a = 1, 2, 3};       // ERROR: cannot mix
Data d3{.a = 1, .b = 2};     // OK: all designated
Data d4{1, 2, 3};            // OK: all positional

Nested Structs

Designated initializers work with nested structures:

struct Address {
    std::string street;
    std::string city;
    int zipcode;
};

struct Person {
    std::string name;
    int age;
    Address address;
};

Person p{
    .name = "Alice",
    .age = 30,
    .address = {
        .street = "123 Main St",
        .city = "Springfield",
        .zipcode = 12345
    }
};

Arrays in Structs

struct Palette {
    unsigned char colors[3];
    std::string name;
};

Palette red{
    .colors = {255, 0, 0},
    .name = "Red"
};

With Constructor Overloading

Designated initializers use aggregate initialization, so they work only with aggregates:

struct Simple {
    int x, y;
    // No user-defined constructors
};

Simple s{.x = 1, .y = 2};  // OK: aggregate

struct WithConstructor {
    int x, y;
    WithConstructor(int a, int b) : x(a), y(b) {}
};

WithConstructor w{.x = 1, .y = 2};  // ERROR: not an aggregate

std::array and Containers

Designated initializers don’t work directly with std::array, but you can use them in enclosing structs:

struct Config {
    std::array<int, 3> values;
    std::string name;
};

Config cfg{
    .values = {1, 2, 3},
    .name = "Test"
};

Real-World Example

Configuration management becomes much clearer:

struct ServerConfig {
    std::string host = "0.0.0.0";
    int port = 8080;
    int max_connections = 100;
    bool enable_tls = false;
    std::string cert_path;
    int timeout_seconds = 30;
    bool verbose = false;
};

// Clear what each setting does
ServerConfig production{
    .host = "api.example.com",
    .port = 443,
    .enable_tls = true,
    .cert_path = "/etc/ssl/cert.pem",
    .verbose = false
};

ServerConfig development{
    .port = 3000,
    .verbose = true
};

Function Parameters

Particularly useful for options objects:

struct DrawOptions {
    int line_width = 1;
    unsigned color = 0x000000;
    bool anti_alias = true;
    double opacity = 1.0;
};

void draw_line(Point start, Point end, DrawOptions opts = {});

// Call with clear intent
draw_line(
    {.x = 0, .y = 0},
    {.x = 100, .y = 100},
    {.color = 0xFF0000, .line_width = 3}
);

Comparison: Before and After

C++17

struct WindowConfig {
    int width, height;
    std::string title;
    bool resizable, fullscreen;
};

// Hard to read
WindowConfig cfg{800, 600, "My App", true, false};

// Better, but verbose
WindowConfig cfg;
cfg.width = 800;
cfg.height = 600;
cfg.title = "My App";
cfg.resizable = true;
cfg.fullscreen = false;

C++20

WindowConfig cfg{
    .width = 800,
    .height = 600,
    .title = "My App",
    .resizable = true,
    .fullscreen = false
};

With const and constexpr

struct Constants {
    int max_retries;
    double timeout;
};

constexpr Constants network_defaults{
    .max_retries = 3,
    .timeout = 5.0
};

Benefits

  1. Self-documenting: Code clearly shows what each value represents
  2. Refactor-safe: Adding/removing/reordering members is safer
  3. Default values: Easy to use defaults for some members
  4. Type safety: Compiler catches mismatched types
  5. IDE support: Better autocomplete and hints

Limitations

  1. Must be in declaration order (unlike C99)
  2. Only works with aggregates
  3. Cannot mix with positional initialization
  4. Cannot repeat members
  5. No computed field names
Conceptual Check

What is required about the order of designated initializers in C++20?

Interactive Lab

Complete the Code

struct Rectangle {
    int width = 10;
    int height = 20;
    std::string color = "red";
};

// Initialize rect with width=50, height=30, keep default color
Rectangle rect{};
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <string>
3 
4struct Employee {
5 std::string name;
6 int id;
7 double salary = 50000.0;
8 bool remote = false;
9};
10 
11int main() {
12 Employee e1{
13 .name = "Alice",
14 .id = 101,
15 .salary = 75000.0
16 };
17
18 Employee e2{
19 .name = "Bob",
20 .id = 102,
21 .remote = true
22 };
23
24 std::cout << e1.name << ": $" << e1.salary << ", Remote: " << e1.remote << '\n';
25 std::cout << e2.name << ": $" << e2.salary << ", Remote: " << e2.remote << '\n';
26
27 return 0;
28}
System Console

Waiting for signal...

Section Detail

constexpr and consteval (C++20/23)

Compile-Time Computation Evolution

Modern C++ continuously expands compile-time capabilities. C++20 and C++23 bring significant improvements to constexpr and introduce consteval and constinit.

constexpr Evolution

C++11: Basic constexpr Functions

constexpr int square(int x) {
    return x * x;
}

constexpr int result = square(5);  // Computed at compile-time
int runtime_val = square(n);       // Can still be used at runtime

C++14: Relaxed constexpr

Allows multiple statements, loops, and local variables:

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

constexpr int fact5 = factorial(5);  // 120, computed at compile-time

C++17: constexpr Lambda

auto squared = [](int x) constexpr { return x * x; };
constexpr int val = squared(10);  // 100

C++20: Major Expansions

Virtual functions, try-catch, dynamic allocation, and more:

#include <vector>
#include <algorithm>

constexpr int sum_vector() {
    std::vector<int> vec{1, 2, 3, 4, 5};  // Dynamic allocation!
    int sum = 0;
    for (int x : vec) {
        sum += x;
    }
    return sum;
}

constexpr int result = sum_vector();  // 15, at compile-time

consteval: Immediate Functions (C++20)

Functions that must execute at compile-time:

consteval int compile_time_only(int x) {
    return x * x;
}

constexpr int value1 = compile_time_only(5);  // OK: compile-time
int n = 5;
int value2 = compile_time_only(n);            // ERROR: not compile-time

Use Cases for consteval

Generate compile-time constants:

consteval unsigned hash_string(const char* str) {
    unsigned hash = 0;
    while (*str) {
        hash = hash * 31 + *str++;
    }
    return hash;
}

switch (event_type) {
    case hash_string("click"):    // Computed at compile-time
        handle_click();
        break;
    case hash_string("keypress"):
        handle_key();
        break;
}

consteval vs constexpr

constexpr int maybe_compile_time(int x) {
    return x * 2;
}

consteval int always_compile_time(int x) {
    return x * 2;
}

int runtime_value = 10;

int a = maybe_compile_time(5);              // OK: compile-time
int b = maybe_compile_time(runtime_value);  // OK: runtime

int c = always_compile_time(5);              // OK: compile-time
int d = always_compile_time(runtime_value);  // ERROR!

constinit: Compile-Time Initialization (C++20)

Ensures variables are initialized at compile-time, but allows runtime modification:

constinit int global_counter = 42;  // Must be compile-time initialized

void increment() {
    ++global_counter;  // OK: can modify at runtime
}

// constexpr would make it immutable
constexpr int immutable = 42;
// immutable = 43;  // ERROR: cannot modify

// constinit ensures initialization, allows modification
constinit int mutable_init = compute_value();  // compute_value() must be constexpr

Static Storage Duration

constinit is primarily for static/thread-local variables:

constinit static int initialized_once = expensive_computation();
// Guarantees no dynamic initialization order issues

Prevents Static Initialization Order Fiasco

// header.h
constinit extern int global_config;  // Declaration

// source.cpp
constinit int global_config = compute_config();  // Must be compile-time

// other.cpp
// Safe to use global_config - guaranteed initialized
int value = global_config + 10;

if consteval (C++23)

Different code paths for compile-time vs runtime:

constexpr int compute(int x) {
    if consteval {
        // This branch runs only at compile-time
        return expensive_compile_time_computation(x);
    } else {
        // This branch runs only at runtime
        return fast_runtime_computation(x);
    }
}

Practical Example

consteval int compile_time_sqrt(int x) {
    // Simple algorithm acceptable for compile-time
    int result = 0;
    while (result * result < x) ++result;
    return result;
}

int runtime_sqrt(int x) {
    // Optimized runtime algorithm
    return std::sqrt(x);
}

constexpr int safe_sqrt(int x) {
    if consteval {
        return compile_time_sqrt(x);
    } else {
        return runtime_sqrt(x);
    }
}

constexpr std::string and std::vector (C++20)

constexpr std::string build_greeting() {
    std::string greeting = "Hello, ";
    greeting += "World!";
    return greeting;
}

constexpr auto msg = build_greeting();  // Compile-time!
constexpr auto compute_primes(int max) {
    std::vector<int> primes;
    for (int n = 2; n <= max; ++n) {
        bool is_prime = true;
        for (int p : primes) {
            if (n % p == 0) {
                is_prime = false;
                break;
            }
        }
        if (is_prime) primes.push_back(n);
    }
    return primes;
}

constexpr auto primes = compute_primes(20);
// std::array or similar at compile-time

constexpr Virtual Functions (C++20)

struct Shape {
    virtual constexpr double area() const = 0;
    virtual ~Shape() = default;
};

struct Circle : Shape {
    double radius;
    
    constexpr Circle(double r) : radius(r) {}
    
    constexpr double area() const override {
        return 3.14159 * radius * radius;
    }
};

constexpr double compute_area() {
    Circle c{5.0};
    Shape& s = c;
    return s.area();  // Virtual call at compile-time!
}

constexpr double area = compute_area();  // 78.5398...

try-catch in constexpr (C++20)

constexpr int safe_divide(int a, int b) {
    try {
        if (b == 0) throw std::runtime_error("Division by zero");
        return a / b;
    } catch (...) {
        return 0;  // Error value
    }
}

constexpr int result = safe_divide(10, 2);  // 5

Note: Throwing exceptions causes compile-time evaluation to fail, but the structure is allowed.

std::is_constant_evaluated()

Detect compile-time evaluation:

#include <type_traits>

constexpr int compute(int x) {
    if (std::is_constant_evaluated()) {
        // Compile-time: use simple algorithm
        return x * x;
    } else {
        // Runtime: can use optimized version
        return fast_multiply(x, x);
    }
}

Practical Examples

Compile-Time Configuration

consteval auto get_build_config() {
    struct Config {
        const char* version;
        int optimization_level;
        bool debug;
    };
    
    return Config{
        .version = "1.0.0",
        .optimization_level = 2,
        .debug = false
    };
}

constexpr auto config = get_build_config();

Compile-Time Validation

consteval void validate_range(int min, int max) {
    if (min >= max) {
        throw "Invalid range";  // Compile-time error
    }
}

template<int Min, int Max>
struct Range {
    Range() {
        validate_range(Min, Max);  // Compile-time check
    }
};

Range<1, 10> valid;      // OK
Range<10, 1> invalid;    // Compile error!
Conceptual Check

What is the key difference between constexpr and consteval functions?

Interactive Lab

Complete the Code

// Create a function that MUST execute at compile-time
 int cube(int x) {
    return x * x * x;
}

constexpr int result = cube(3);  // Must work at compile-time
Runtime Environment

Interactive Lab

1#include <iostream>
2 
3constexpr int factorial(int n) {
4 int result = 1;
5 for (int i = 2; i <= n; ++i) {
6 result *= i;
7 }
8 return result;
9}
10 
11consteval int square(int x) {
12 return x * x;
13}
14 
15int main() {
16 constexpr int fact5 = factorial(5);
17 constexpr int sq10 = square(10);
18
19 std::cout << "5! = " << fact5 << '\n';
20 std::cout << "10² = " << sq10 << '\n';
21
22 // This works:
23 int n = 5;
24 std::cout << "factorial(" << n << ") = " << factorial(n) << '\n';
25
26 // This would fail: square(n) - consteval requires compile-time
27
28 return 0;
29}
System Console

Waiting for signal...

Section Detail

std::format (C++20)

Modern String Formatting

C++20’s std::format provides Python-style string formatting that’s type-safe, efficient, and more intuitive than iostream or printf.

Basic Syntax

#include <format>
#include <string>

std::string name = "Alice";
int age = 30;

// C++20 format
std::string msg = std::format("Hello, {}! You are {} years old.", name, age);
// "Hello, Alice! You are 30 years old."

Comparison with Alternatives

// printf: not type-safe, no std::string support
printf("Hello, %s! You are %d years old.\n", name.c_str(), age);

// iostream: verbose, awkward
std::ostringstream oss;
oss << "Hello, " << name << "! You are " << age << " years old.";
std::string msg = oss.str();

// std::format: clean and type-safe
auto msg = std::format("Hello, {}! You are {} years old.", name, age);

Positional Arguments

Reference arguments by position:

std::format("{0} {1} {0}", "Hello", "World");  // "Hello World Hello"
std::format("{1} {0}", "World", "Hello");      // "Hello World"

Format Specifications

Integers

int num = 42;

std::format("{}", num);         // "42"
std::format("{:d}", num);       // "42" (decimal)
std::format("{:x}", num);       // "2a" (hexadecimal lowercase)
std::format("{:X}", num);       // "2A" (hexadecimal uppercase)
std::format("{:o}", num);       // "52" (octal)
std::format("{:b}", num);       // "101010" (binary)

std::format("{:06d}", num);     // "000042" (zero-padded, width 6)
std::format("{:+d}", num);      // "+42" (show sign)
std::format("{: d}", num);      // " 42" (space for positive)

Floating-Point

double pi = 3.14159265359;

std::format("{}", pi);          // "3.14159265359"
std::format("{:.2f}", pi);      // "3.14" (2 decimal places)
std::format("{:.5f}", pi);      // "3.14159"
std::format("{:e}", pi);        // "3.141593e+00" (scientific)
std::format("{:E}", pi);        // "3.141593E+00"
std::format("{:g}", pi);        // "3.14159" (general format)

std::format("{:10.2f}", pi);    // "      3.14" (width 10)
std::format("{:010.2f}", pi);   // "0000003.14" (zero-padded)

Strings

std::string text = "Hello";

std::format("{}", text);        // "Hello"
std::format("{:10}", text);     // "Hello     " (width 10, left-aligned)
std::format("{:>10}", text);    // "     Hello" (right-aligned)
std::format("{:^10}", text);    // "  Hello   " (centered)
std::format("{:*<10}", text);   // "Hello*****" (fill with *)
std::format("{:.3}", text);     // "Hel" (truncate to 3)

Alignment and Fill

int n = 42;

std::format("{:<5}", n);        // "42   " (left-aligned, width 5)
std::format("{:>5}", n);        // "   42" (right-aligned)
std::format("{:^5}", n);        // " 42  " (centered)
std::format("{:*>5}", n);       // "***42" (fill with *, right-aligned)
std::format("{:0>5}", n);       // "00042" (zero-padded)

Type-Specific Formatting

Pointers

int* ptr = &value;
std::format("{}", ptr);         // "0x7ffc12345678"
std::format("{:p}", ptr);       // "0x7ffc12345678"

Booleans

bool flag = true;
std::format("{}", flag);        // "true"
std::format("{:s}", flag);      // "true" (string representation)
std::format("{:d}", flag);      // "1" (numeric representation)

Characters

char ch = 'A';
std::format("{}", ch);          // "A"
std::format("{:d}", ch);        // "65" (ASCII value)

Compile-Time Format String Checking

Format strings are checked at compile-time:

int n = 42;

std::format("{}", n);           // OK
std::format("{:d}", n);         // OK
std::format("{:f}", n);         // Compile error: can't format int as float
std::format("{} {}", n);        // Compile error: too few arguments

Custom Type Formatting

Extend formatting for your types:

struct Point {
    int x, y;
};

template<>
struct std::formatter<Point> {
    constexpr auto parse(format_parse_context& ctx) {
        return ctx.begin();
    }
    
    auto format(const Point& p, format_context& ctx) const {
        return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
    }
};

Point p{10, 20};
std::format("Point: {}", p);    // "Point: (10, 20)"

Advanced Custom Formatting

Support format specifications:

template<>
struct std::formatter<Point> {
    char presentation = 'c';  // 'c' for cartesian, 'p' for polar
    
    constexpr auto parse(format_parse_context& ctx) {
        auto it = ctx.begin();
        if (it != ctx.end() && (*it == 'c' || *it == 'p')) {
            presentation = *it++;
        }
        return it;
    }
    
    auto format(const Point& p, format_context& ctx) const {
        if (presentation == 'p') {
            double r = std::sqrt(p.x*p.x + p.y*p.y);
            double theta = std::atan2(p.y, p.x);
            return std::format_to(ctx.out(), "r={:.2f}, θ={:.2f}", r, theta);
        }
        return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
    }
};

Point p{3, 4};
std::format("{:c}", p);  // "(3, 4)"
std::format("{:p}", p);  // "r=5.00, θ=0.93"

format_to: Output Iterators

Write directly to output iterators for efficiency:

std::string buffer;
std::format_to(std::back_inserter(buffer), "Value: {}", 42);
// buffer == "Value: 42"

std::vector<char> vec;
std::format_to(std::back_inserter(vec), "Hello {}", "World");

format_to_n: Bounded Output

Format with size limit:

char buffer[20];
auto result = std::format_to_n(buffer, sizeof(buffer), "Value: {}", 12345);
// result.out points past the last written character
// result.size is the total that would have been written

formatted_size: Size Calculation

Calculate size without formatting:

size_t size = std::formatted_size("Value: {}, {}", 42, "test");
std::string buffer;
buffer.reserve(size);
std::format_to(std::back_inserter(buffer), "Value: {}, {}", 42, "test");

Localization Support

std::locale loc("de_DE.UTF-8");
double value = 1234.56;

std::format("{:L}", value);              // "1,234.56" (default locale)
std::format(loc, "{:L}", value);         // "1.234,56" (German locale)

Escaping Braces

std::format("{{}}");           // "{}"
std::format("{{{}}} ", 42);    // "{42} "

vformat: Runtime Format Strings

When format string isn’t known at compile-time:

std::string fmt = get_format_from_user();  // Runtime string
std::string result = std::vformat(fmt, std::make_format_args(arg1, arg2));

Performance Benefits

std::format is typically faster than iostream:

// Iostream: multiple virtual calls, manipulator overhead
std::ostringstream oss;
oss << "Value: " << std::setw(10) << std::setfill('0') << value;

// std::format: single operation, optimized
auto str = std::format("Value: {:010}", value);

Migration from printf

// printf
printf("%s: %d items, %.2f total\n", name.c_str(), count, total);

// std::format
std::println("{}: {} items, {:.2f} total", name, count, total);
// or
std::cout << std::format("{}: {} items, {:.2f} total\n", name, count, total);

C++23 std::print and std::println

C++23 adds convenience functions:

std::print("Hello, {}!\n", name);           // Print to stdout
std::println("Value: {}", value);           // Print with newline
std::print(stderr, "Error: {}\n", msg);     // Print to stderr
Conceptual Check

What is the advantage of std::format over printf?

Interactive Lab

Complete the Code

double price = 19.99;
int quantity = 3;

// Format as: "Total: $59.97 (3 items)"
std::string msg = std::format();
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <format>
3#include <string>
4 
5int main() {
6 std::string name = "Alice";
7 int score = 95;
8 double average = 87.456;
9
10 std::cout << std::format(&quot;Student: {}
11&quot;, name);
12 std::cout << std::format(&quot;Score: {:3d}/100
13&quot;, score);
14 std::cout << std::format(&quot;Average: {:6.2f}
15&quot;, average);
16 std::cout << std::format(&quot;Hex: 0x{:04X}
17&quot;, score);
18 std::cout << std::format(&quot;Binary: 0b{:08b}
19&quot;, score);
20 std::cout << std::format(&quot;{:=^30}
21", " Summary &quot;);
22
23 return 0;
24}
System Console

Waiting for signal...

Section Detail

std::expected (C++23)

Error Handling with std::expected

C++23 introduces std::expected<T, E>, a vocabulary type for functions that can return either a value or an error. It provides a type-safe alternative to exceptions and error codes.

Basic Concept

std::expected<T, E> holds either a value of type T (success) or an error of type E (failure):

#include <expected>
#include <string>

std::expected<int, std::string> parse_int(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::unexpected("Invalid integer format");
    }
}

auto result = parse_int("42");
if (result) {
    std::cout << "Parsed: " << *result << '\n';  // 42
} else {
    std::cout << "Error: " << result.error() << '\n';
}

Checking Success vs Failure

std::expected<double, std::string> divide(double a, double b) {
    if (b == 0.0) {
        return std::unexpected("Division by zero");
    }
    return a / b;
}

auto result = divide(10.0, 2.0);

// Method 1: Contextual conversion to bool
if (result) {
    double value = *result;  // Dereference to get value
}

// Method 2: has_value()
if (result.has_value()) {
    double value = result.value();  // Throws if error
}

// Method 3: Check for error
if (!result) {
    std::string err = result.error();
}

Accessing Values

auto result = divide(10.0, 2.0);

// Dereference (undefined behavior if error)
double v1 = *result;
double v2 = result.operator*();

// value() - throws std::bad_expected_access if error
double v3 = result.value();

// value_or() - returns default if error
double v4 = result.value_or(0.0);

Error Types

Using Enums

enum class Error {
    FileNotFound,
    PermissionDenied,
    InvalidFormat
};

std::expected<std::string, Error> read_file(const std::string& path) {
    if (!file_exists(path)) {
        return std::unexpected(Error::FileNotFound);
    }
    if (!has_permission(path)) {
        return std::unexpected(Error::PermissionDenied);
    }
    // Read and return content
    return file_content;
}

Custom Error Types

struct ParseError {
    std::string message;
    size_t position;
    
    ParseError(std::string msg, size_t pos) 
        : message(std::move(msg)), position(pos) {}
};

std::expected<int, ParseError> parse_number(const std::string& str) {
    for (size_t i = 0; i < str.length(); ++i) {
        if (!std::isdigit(str[i])) {
            return std::unexpected(ParseError{
                "Invalid character", i
            });
        }
    }
    return std::stoi(str);
}

Monadic Operations

C++23 expected supports monadic operations for chaining:

and_then: Chain Operations

std::expected<int, std::string> parse_int(const std::string& s);
std::expected<int, std::string> validate_positive(int n);
std::expected<double, std::string> compute_sqrt(int n);

auto result = parse_int("16")
    .and_then(validate_positive)
    .and_then(compute_sqrt);

if (result) {
    std::cout << "Square root: " << *result << '\n';  // 4.0
}

Implementation:

std::expected<int, std::string> validate_positive(int n) {
    if (n > 0) {
        return n;
    }
    return std::unexpected("Number must be positive");
}

std::expected<double, std::string> compute_sqrt(int n) {
    return std::sqrt(n);
}

or_else: Handle Errors

auto result = parse_int("invalid")
    .or_else([](const std::string& err) -> std::expected<int, std::string> {
        std::cerr << "Parse error: " << err << '\n';
        return 0;  // Return default value
    });

transform: Map Values

auto result = parse_int("42")
    .transform([](int n) { return n * 2; });  // std::expected<int, std::string>

if (result) {
    std::cout << *result << '\n';  // 84
}

transform_error: Map Errors

auto result = parse_int("invalid")
    .transform_error([](const std::string& err) {
        return Error::InvalidFormat;  // Convert error type
    });
// result is std::expected<int, Error>

Practical Example: File Processing Pipeline

enum class FileError {
    NotFound,
    PermissionDenied,
    InvalidFormat,
    ProcessingFailed
};

std::expected<std::string, FileError> read_file(const std::string& path);
std::expected<Data, FileError> parse_data(const std::string& content);
std::expected<Result, FileError> process_data(const Data& data);
std::expected<void, FileError> save_result(const Result& result);

// Chain operations
auto outcome = read_file("input.txt")
    .and_then(parse_data)
    .and_then(process_data)
    .and_then([](const Result& r) {
        return save_result(r);
    });

if (!outcome) {
    switch (outcome.error()) {
        case FileError::NotFound:
            std::cerr << "File not found\n";
            break;
        case FileError::PermissionDenied:
            std::cerr << "Permission denied\n";
            break;
        // ...
    }
}

std::expected<void, E>

For operations that don’t return a value:

std::expected<void, std::string> validate_config(const Config& cfg) {
    if (cfg.port < 1024) {
        return std::unexpected("Port must be >= 1024");
    }
    if (cfg.host.empty()) {
        return std::unexpected("Host cannot be empty");
    }
    return {};  // Success
}

auto result = validate_config(config);
if (!result) {
    std::cerr << "Validation failed: " << result.error() << '\n';
}

Comparison with Alternatives

vs Exceptions

// Exceptions: implicit control flow
double divide_throws(double a, double b) {
    if (b == 0.0) throw std::runtime_error("Division by zero");
    return a / b;
}

try {
    auto result = divide_throws(10.0, 0.0);
} catch (const std::exception& e) {
    // Handle error
}

// std::expected: explicit error handling
std::expected<double, std::string> divide_expected(double a, double b) {
    if (b == 0.0) return std::unexpected("Division by zero");
    return a / b;
}

auto result = divide_expected(10.0, 0.0);
if (!result) {
    // Handle error explicitly
}

vs std::optional

// std::optional: success/failure, but no error information
std::optional<int> parse_int_opt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;  // No error information
    }
}

// std::expected: success/failure with error details
std::expected<int, std::string> parse_int_exp(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (const std::exception& e) {
        return std::unexpected(e.what());  // Error details preserved
    }
}

vs Error Codes

// C-style error codes
int parse_int_code(const std::string& s, int* out, std::string* error) {
    try {
        *out = std::stoi(s);
        return 0;  // Success
    } catch (const std::exception& e) {
        *error = e.what();
        return -1;  // Failure
    }
}

// std::expected: type-safe, ergonomic
std::expected<int, std::string> parse_int(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (const std::exception& e) {
        return std::unexpected(e.what());
    }
}

Error Propagation

Simplify error propagation with monadic operations:

std::expected<int, Error> step1();
std::expected<double, Error> step2(int);
std::expected<std::string, Error> step3(double);

std::expected<std::string, Error> process() {
    return step1()
        .and_then(step2)
        .and_then(step3);
    // Error automatically propagates through chain
}

Best Practices

  1. Use for recoverable errors: std::expected is ideal for expected failures (validation, parsing, I/O)
  2. Use exceptions for exceptional cases: Programming errors, violations of invariants
  3. Consistent error types: Use enums or custom error types across a module
  4. Leverage monadic operations: Chain operations for clean error handling
  5. Document error cases: Clearly document what errors a function can return

When to Use std::expected

✅ Use when:

  • Errors are expected and recoverable
  • Performance is critical (no exception overhead)
  • Error information needs to be propagated
  • Explicit error handling is preferred

❌ Don’t use when:

  • Errors are truly exceptional (use exceptions)
  • Error handling is optional (use std::optional)
  • Working with legacy code that uses error codes
Conceptual Check

What does std::expected<T, E> contain?

Interactive Lab

Complete the Code

std::expected<double, std::string> safe_sqrt(double x) {
    if (x < 0) {
        return std::("Cannot compute sqrt of negative number");
    }
    return std::sqrt(x);
}
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <expected>
3#include <string>
4#include <cmath>
5 
6std::expected<double, std::string> safe_divide(double a, double b) {
7 if (b == 0.0) {
8 return std::unexpected("Division by zero");
9 }
10 return a / b;
11}
12 
13std::expected<double, std::string> safe_sqrt(double x) {
14 if (x < 0) {
15 return std::unexpected("Cannot compute sqrt of negative");
16 }
17 return std::sqrt(x);
18}
19 
20int main() {
21 auto result = safe_divide(16.0, 4.0)
22 .and_then(safe_sqrt);
23
24 if (result) {
25 std::cout << "Result: " << *result << '\n';
26 } else {
27 std::cout << "Error: " << result.error() << '\n';
28 }
29
30 auto error_result = safe_divide(10.0, 0.0)
31 .and_then(safe_sqrt);
32
33 if (!error_result) {
34 std::cout << "Error: " << error_result.error() << '\n';
35 }
36
37 return 0;
38}
System Console

Waiting for signal...

Tools and Practices

Section Detail

CMake and Build Systems

Modern C++ Build Systems

CMake has become the de facto standard for C++ build configuration. It generates platform-specific build files and manages dependencies, compiler flags, and project structure.

Basic CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Create executable
add_executable(myapp main.cpp)

Build process:

mkdir build
cd build
cmake ..
cmake --build .

Project Structure

project/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   ├── utils.cpp
│   └── utils.h
├── include/
│   └── mylib/
│       └── api.h
├── tests/
│   └── test_main.cpp
└── build/  (generated)

Root CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(MyProject)

set(CMAKE_CXX_STANDARD 20)

# Include directories
include_directories(include)

# Source files
add_executable(myapp
    src/main.cpp
    src/utils.cpp
)

# Enable testing
enable_testing()
add_subdirectory(tests)

Libraries

Static Library

add_library(mylib STATIC
    src/lib.cpp
    src/helper.cpp
)

target_include_directories(mylib PUBLIC include)

# Link library to executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)

Shared Library

add_library(mylib SHARED
    src/lib.cpp
)

target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Header-Only Library

add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)

Target Properties

Modern CMake uses target-based configuration:

add_library(mylib src/lib.cpp)

# Include directories
target_include_directories(mylib
    PUBLIC include           # Consumers need these
    PRIVATE src/internal     # Only this target needs these
)

# Compile definitions
target_compile_definitions(mylib
    PUBLIC API_VERSION=2
    PRIVATE INTERNAL_DEBUG
)

# Compile options
target_compile_options(mylib
    PRIVATE -Wall -Wextra -Wpedantic
)

# Link libraries
target_link_libraries(mylib
    PUBLIC fmt::fmt          # Consumers need this too
    PRIVATE spdlog::spdlog   # Only this target needs this
)

Visibility:

  • PUBLIC: Used by this target and propagated to consumers
  • PRIVATE: Used only by this target
  • INTERFACE: Not used by this target, only propagated to consumers

Compiler Flags

# Global flags (discouraged)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")

# Target-specific flags (preferred)
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

Build Types

# Set default build type
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# Build type specific flags
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")

Build with specific type:

cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Release ..

Finding Packages

find_package

find_package(Boost 1.75 REQUIRED COMPONENTS system filesystem)

add_executable(myapp main.cpp)
target_link_libraries(myapp
    Boost::system
    Boost::filesystem
)

pkg-config

find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBXML libxml-2.0 REQUIRED)

target_include_directories(myapp PRIVATE ${LIBXML_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${LIBXML_LIBRARIES})

FetchContent: Modern Dependency Management

Download and build dependencies at configure time:

include(FetchContent)

FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 9.1.0
)

FetchContent_MakeAvailable(fmt)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)

Multiple dependencies:

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG v1.12.0
)

FetchContent_MakeAvailable(googletest spdlog)

Generator Expressions

Conditional compilation based on configuration:

target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
    $<$<CONFIG:Release>:RELEASE_MODE>
)

target_link_libraries(myapp PRIVATE
    $<$<PLATFORM_ID:Windows>:ws2_32>
    $<$<PLATFORM_ID:Linux>:pthread>
)

Testing with CTest

enable_testing()

add_executable(test_main tests/test_main.cpp)
target_link_libraries(test_main PRIVATE mylib GTest::gtest_main)

add_test(NAME MainTests COMMAND test_main)

Run tests:

cmake --build build
ctest --test-dir build

Installation

install(TARGETS myapp mylib
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)

install(DIRECTORY include/
    DESTINATION include
)

Install to custom location:

cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
cmake --install build

Presets (CMake 3.19+)

CMakePresets.json:

{
    "version": 3,
    "configurePresets": [
        {
            "name": "debug",
            "binaryDir": "build/debug",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        },
        {
            "name": "release",
            "binaryDir": "build/release",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        }
    ],
    "buildPresets": [
        {
            "name": "debug",
            "configurePreset": "debug"
        },
        {
            "name": "release",
            "configurePreset": "release"
        }
    ]
}

Use presets:

cmake --preset debug
cmake --build --preset debug

Package Management: vcpkg and Conan

vcpkg

vcpkg install fmt spdlog
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake ..

Conan

conanfile.txt:

[requires]
fmt/9.1.0
spdlog/1.12.0

[generators]
CMakeDeps
CMakeToolchain
conan install . --build=missing
cmake -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake ..

Modern CMake Best Practices

  1. Use targets, not variables: Prefer target_* commands
  2. Avoid global commands: Use target-specific settings
  3. Proper visibility: PUBLIC/PRIVATE/INTERFACE
  4. Minimum version: Set cmake_minimum_required appropriately
  5. Out-of-source builds: Always build in separate directory
  6. Generator expressions: For conditional configuration
  7. Export targets: For library packages
  8. Use FetchContent: For dependencies when appropriate

Complete Example

cmake_minimum_required(VERSION 3.20)
project(ModernApp VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Options
option(BUILD_TESTS "Build tests" ON)
option(ENABLE_WARNINGS "Enable compiler warnings" ON)

# Dependencies
include(FetchContent)
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)

# Library
add_library(applib
    src/lib.cpp
    src/utils.cpp
)

target_include_directories(applib PUBLIC include)
target_link_libraries(applib PUBLIC fmt::fmt)

if(ENABLE_WARNINGS)
    target_compile_options(applib PRIVATE
        $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
        $<$<CXX_COMPILER_ID:MSVC>:/W4>
    )
endif()

# Executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE applib)

# Tests
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# Installation
install(TARGETS myapp applib
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)
Conceptual Check

What is the recommended way to set compiler flags in modern CMake?

Interactive Lab

Complete the Code

# Link fmt library to myapp target
add_executable(myapp main.cpp)
(myapp PRIVATE fmt::fmt)
Section Detail

Testing with GoogleTest

Modern C++ Testing

Google Test (gtest) is the most widely-used C++ testing framework, providing comprehensive features for unit testing, integration testing, and test-driven development.

Basic Test Structure

#include <gtest/gtest.h>

// Simple function to test
int add(int a, int b) {
    return a + b;
}

// TEST(TestSuiteName, TestName)
TEST(MathTests, Addition) {
    EXPECT_EQ(add(2, 3), 5);
    EXPECT_EQ(add(-1, 1), 0);
    EXPECT_EQ(add(0, 0), 0);
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Assertions

EXPECT vs ASSERT

  • EXPECT_*: Continues test after failure (non-fatal)
  • ASSERT_*: Stops test after failure (fatal)
TEST(AssertionExample, DemonstratesExpectVsAssert) {
    EXPECT_EQ(1, 1);  // Passes
    EXPECT_EQ(1, 2);  // Fails, but continues
    EXPECT_EQ(3, 3);  // Still runs
}

TEST(AssertionExample, AssertStopsOnFailure) {
    ASSERT_EQ(1, 1);  // Passes
    ASSERT_EQ(1, 2);  // Fails, stops test
    EXPECT_EQ(3, 3);  // Never runs
}

Common Assertions

TEST(AssertionTypes, BooleanAssertions) {
    EXPECT_TRUE(true);
    EXPECT_FALSE(false);
}

TEST(AssertionTypes, ComparisonAssertions) {
    EXPECT_EQ(10, 10);    // Equal
    EXPECT_NE(10, 20);    // Not equal
    EXPECT_LT(5, 10);     // Less than
    EXPECT_LE(5, 10);     // Less or equal
    EXPECT_GT(10, 5);     // Greater than
    EXPECT_GE(10, 5);     // Greater or equal
}

TEST(AssertionTypes, StringAssertions) {
    EXPECT_STREQ("hello", "hello");      // C-strings equal
    EXPECT_STRNE("hello", "world");      // C-strings not equal
    EXPECT_STRCASEEQ("Hello", "hello");  // Case-insensitive
}

TEST(AssertionTypes, FloatingPointAssertions) {
    EXPECT_FLOAT_EQ(1.0f, 1.0001f);   // Within 4 ULPs
    EXPECT_DOUBLE_EQ(1.0, 1.0001);    // Within 4 ULPs
    EXPECT_NEAR(1.0, 1.1, 0.2);       // Within absolute error
}

Test Fixtures

Reuse setup and teardown code:

class DatabaseTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Runs before each test
        db = std::make_unique<Database>();
        db->connect("test.db");
    }
    
    void TearDown() override {
        // Runs after each test
        db->disconnect();
    }
    
    std::unique_ptr<Database> db;
};

TEST_F(DatabaseTest, InsertRecord) {
    EXPECT_TRUE(db->insert("key", "value"));
    EXPECT_EQ(db->get("key"), "value");
}

TEST_F(DatabaseTest, DeleteRecord) {
    db->insert("key", "value");
    EXPECT_TRUE(db->remove("key"));
    EXPECT_FALSE(db->contains("key"));
}

Static Setup/Teardown

Runs once per test suite:

class ExpensiveSetupTest : public ::testing::Test {
protected:
    static void SetUpTestSuite() {
        // Runs once before all tests in suite
        shared_resource = new Resource();
    }
    
    static void TearDownTestSuite() {
        // Runs once after all tests in suite
        delete shared_resource;
    }
    
    static Resource* shared_resource;
};

Resource* ExpensiveSetupTest::shared_resource = nullptr;

Parameterized Tests

Test with multiple inputs:

class PrimeTest : public ::testing::TestWithParam<int> {
};

TEST_P(PrimeTest, IsPrime) {
    int n = GetParam();
    EXPECT_TRUE(is_prime(n));
}

INSTANTIATE_TEST_SUITE_P(
    PrimeNumbers,
    PrimeTest,
    ::testing::Values(2, 3, 5, 7, 11, 13, 17, 19)
);

With multiple parameters:

class AdditionTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {
};

TEST_P(AdditionTest, ValidateAddition) {
    auto [a, b, expected] = GetParam();
    EXPECT_EQ(add(a, b), expected);
}

INSTANTIATE_TEST_SUITE_P(
    AddCases,
    AdditionTest,
    ::testing::Values(
        std::make_tuple(1, 2, 3),
        std::make_tuple(-1, 1, 0),
        std::make_tuple(100, 200, 300)
    )
);

Death Tests

Test for expected crashes or aborts:

void fatal_error(bool condition) {
    if (condition) {
        std::abort();
    }
}

TEST(DeathTest, FatalError) {
    EXPECT_DEATH(fatal_error(true), "");
    EXPECT_EXIT(std::exit(42), ::testing::ExitedWithCode(42), "");
}

Exception Tests

TEST(ExceptionTest, ThrowsException) {
    EXPECT_THROW(risky_function(), std::runtime_error);
    EXPECT_ANY_THROW(another_risky_function());
    EXPECT_NO_THROW(safe_function());
}

TEST(ExceptionTest, SpecificException) {
    try {
        risky_function();
        FAIL() << "Expected exception";
    } catch (const std::runtime_error& e) {
        EXPECT_STREQ(e.what(), "Expected error message");
    }
}

Custom Matchers

#include <gmock/gmock.h>

using ::testing::MatchesRegex;
using ::testing::StartsWith;
using ::testing::EndsWith;
using ::testing::HasSubstr;

TEST(StringMatchers, RegexAndSubstrings) {
    std::string text = "Hello, World!";
    
    EXPECT_THAT(text, StartsWith("Hello"));
    EXPECT_THAT(text, EndsWith("World!"));
    EXPECT_THAT(text, HasSubstr("lo, Wo"));
    EXPECT_THAT(text, MatchesRegex("H.*!"));
}

Container matchers:

using ::testing::ElementsAre;
using ::testing::UnorderedElementsAre;
using ::testing::Contains;
using ::testing::SizeIs;

TEST(ContainerMatchers, VectorMatching) {
    std::vector<int> vec{1, 2, 3, 4, 5};
    
    EXPECT_THAT(vec, ElementsAre(1, 2, 3, 4, 5));
    EXPECT_THAT(vec, Contains(3));
    EXPECT_THAT(vec, SizeIs(5));
}

Mocking with Google Mock

#include <gmock/gmock.h>

// Interface to mock
class Database {
public:
    virtual ~Database() = default;
    virtual bool insert(const std::string& key, const std::string& value) = 0;
    virtual std::string get(const std::string& key) = 0;
};

// Mock implementation
class MockDatabase : public Database {
public:
    MOCK_METHOD(bool, insert, (const std::string& key, const std::string& value), (override));
    MOCK_METHOD(std::string, get, (const std::string& key), (override));
};

// Class under test
class UserManager {
    Database* db_;
public:
    UserManager(Database* db) : db_(db) {}
    
    bool addUser(const std::string& name) {
        return db_->insert(name, "default_value");
    }
};

TEST(UserManagerTest, AddsUser) {
    MockDatabase mock_db;
    UserManager manager(&mock_db);
    
    // Set expectations
    EXPECT_CALL(mock_db, insert("Alice", "default_value"))
        .WillOnce(::testing::Return(true));
    
    EXPECT_TRUE(manager.addUser("Alice"));
}

Mock Expectations

using ::testing::Return;
using ::testing::_;
using ::testing::AtLeast;

TEST(MockTest, Expectations) {
    MockDatabase mock_db;
    
    // Expect specific call
    EXPECT_CALL(mock_db, get("key"))
        .WillOnce(Return("value"));
    
    // Expect any string argument
    EXPECT_CALL(mock_db, get(_))
        .WillRepeatedly(Return("default"));
    
    // Expect at least 2 calls
    EXPECT_CALL(mock_db, insert(_, _))
        .Times(AtLeast(2))
        .WillRepeatedly(Return(true));
}

Test Organization

namespace {  // Anonymous namespace for test-only code

class CalculatorTest : public ::testing::Test {
protected:
    Calculator calc;
};

TEST_F(CalculatorTest, Addition) {
    EXPECT_EQ(calc.add(2, 3), 5);
}

TEST_F(CalculatorTest, Subtraction) {
    EXPECT_EQ(calc.subtract(5, 3), 2);
}

}  // namespace

Running Tests

Command Line

# Run all tests
./test_binary

# Run specific test
./test_binary --gtest_filter=MathTests.Addition

# Run tests matching pattern
./test_binary --gtest_filter=Math*

# Repeat tests
./test_binary --gtest_repeat=10

# Shuffle test order
./test_binary --gtest_shuffle

# Generate XML output
./test_binary --gtest_output=xml:test_results.xml

CMake Integration

enable_testing()

find_package(GTest REQUIRED)

add_executable(my_tests test_main.cpp test_math.cpp)
target_link_libraries(my_tests PRIVATE GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(my_tests)

Run with CTest:

ctest --output-on-failure

Test-Driven Development (TDD)

  1. Write failing test:
TEST(StringUtils, Reverse) {
    EXPECT_EQ(reverse("hello"), "olleh");  // Test fails - function doesn't exist
}
  1. Implement minimum code to pass:
std::string reverse(const std::string& str) {
    return std::string(str.rbegin(), str.rend());
}
  1. Refactor:
std::string reverse(std::string_view str) {
    return std::string(str.rbegin(), str.rend());
}

Best Practices

  1. One assertion per test (when possible) - easier to identify failures
  2. Descriptive test names - document what’s being tested
  3. Arrange-Act-Assert pattern - structure test clearly
  4. Fast tests - avoid I/O when possible
  5. Isolated tests - no dependencies between tests
  6. Mock external dependencies - test in isolation
  7. Test edge cases - empty input, null, overflow
  8. Continuous integration - run tests automatically
Conceptual Check

What's the difference between EXPECT and ASSERT macros?

Interactive Lab

Complete the Code

// Create a test fixture for a Stack class
class StackTest : public ::testing:: {
protected:
    Stack<int> stack;
};
Section Detail

Performance Optimization

C++ Performance Optimization

C++ provides fine-grained control over performance. Understanding optimization techniques and measurement tools is essential for writing efficient code.

Measurement First

Never optimize without measuring. Use profilers to identify bottlenecks:

Profiling Tools

  • perf (Linux): CPU profiling
  • Valgrind/Callgrind: Call graph analysis
  • gprof: GNU profiler
  • Intel VTune: Advanced profiling
  • Tracy: Real-time profiling
# Compile with debug symbols
g++ -g -O2 program.cpp -o program

# Profile with perf
perf record ./program
perf report

# Profile with Valgrind
valgrind --tool=callgrind ./program
kcachegrind callgrind.out.*

Compiler Optimization Levels

-O0  # No optimization (default, fastest compile)
-O1  # Basic optimization
-O2  # Recommended for release builds
-O3  # Aggressive optimization (may increase code size)
-Os  # Optimize for size
-Ofast  # -O3 + unsafe math optimizations

Enable link-time optimization:

g++ -O3 -flto program.cpp -o program

Memory Layout and Cache

Structure Packing

// Poor layout: 24 bytes (with padding)
struct Bad {
    char a;    // 1 byte + 7 padding
    double b;  // 8 bytes
    char c;    // 1 byte + 7 padding
};

// Good layout: 16 bytes
struct Good {
    double b;  // 8 bytes
    char a;    // 1 byte
    char c;    // 1 byte + 6 padding
};

static_assert(sizeof(Bad) == 24);
static_assert(sizeof(Good) == 16);

Cache-Friendly Data Structures

Prefer contiguous memory (vector) over linked structures:

// Cache-unfriendly
std::list<int> list;  // Nodes scattered in memory

// Cache-friendly
std::vector<int> vec;  // Contiguous memory

// Benchmark shows vector is often 10-100x faster for iteration

Structure of Arrays (SoA) vs Array of Structures (AoS)

// AoS: Poor cache utilization if you only need x coordinates
struct Point { float x, y, z; };
std::vector<Point> points;

for (const auto& p : points) {
    process(p.x);  // Loads y and z unnecessarily
}

// SoA: Better cache utilization
struct Points {
    std::vector<float> x;
    std::vector<float> y;
    std::vector<float> z;
};

Points points;
for (float x_val : points.x) {
    process(x_val);  // Only loads x values
}

Move Semantics and RVO

Return Value Optimization (RVO)

Compiler automatically elides copies:

std::vector<int> create_vector() {
    std::vector<int> vec(1000);
    // ...
    return vec;  // No copy! RVO applied
}

auto v = create_vector();  // vec moved directly into v

Named Return Value Optimization (NRVO)

std::string build_string() {
    std::string result;
    result += "Hello";
    result += " World";
    return result;  // NRVO: no copy
}

Explicit Move

std::vector<int> source = create_large_vector();
std::vector<int> dest = std::move(source);  // Move, not copy
// source is now empty

Avoid Unnecessary Copies

Pass by Reference

// Bad: copies vector
void process(std::vector<int> vec) { }

// Good: references vector
void process(const std::vector<int>& vec) { }

// Better: use std::span for read-only access
void process(std::span<const int> data) { }

Returning Large Objects

// Good: RVO applies
std::vector<int> compute() {
    std::vector<int> result;
    // ...
    return result;
}

// Don't do this:
void compute(std::vector<int>& out) {  // Output parameter
    // ...
}

Small String Optimization (SSO)

std::string optimizes for small strings:

std::string small = "Hello";  // No heap allocation (typically < 16 chars)
std::string large = "This is a very long string...";  // Heap allocated

Use string_view for temporary views:

void process(std::string_view sv) {  // No copy
    // ...
}

std::string str = "Hello";
process(str);  // Efficient
process("World");  // No temporary string created

Reserve Capacity

Avoid reallocations:

// Bad: multiple reallocations
std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {
    vec.push_back(i);  // May reallocate many times
}

// Good: single allocation
std::vector<int> vec;
vec.reserve(1000);
for (int i = 0; i < 1000; ++i) {
    vec.push_back(i);  // No reallocations
}

// Even better: direct initialization
std::vector<int> vec(1000);
std::iota(vec.begin(), vec.end(), 0);

emplace vs push

std::vector<std::string> vec;

// push_back: creates temporary, then moves
vec.push_back(std::string("Hello"));

// emplace_back: constructs in-place
vec.emplace_back("Hello");  // No temporary

// With complex types
std::vector<std::pair<int, std::string>> pairs;
pairs.push_back({1, "one"});      // Creates temporary pair
pairs.emplace_back(1, "one");     // Constructs directly

Inline Functions

Small functions benefit from inlining:

inline int square(int x) { return x * x; }

// Better: let compiler decide
constexpr int square(int x) { return x * x; }

Modern compilers inline automatically with optimization enabled.

Branch Prediction

Likely/Unlikely Hints (C++20)

int process(int x) {
    if (x > 0) [[likely]] {
        return x * 2;
    } else {
        return handle_error();
    }
}

Avoid Branches in Tight Loops

// Bad: branch in loop
for (int i = 0; i < n; ++i) {
    if (condition) {
        result += array[i];
    }
}

// Better: predication
for (int i = 0; i < n; ++i) {
    result += array[i] * condition;  // Branchless
}

Algorithm Choice

// O(n²) - slow for large n
std::vector<int> vec;
for (int i = 0; i < n; ++i) {
    vec.erase(std::remove(vec.begin(), vec.end(), i), vec.end());
}

// O(n) - much faster
std::unordered_set<int> set(vec.begin(), vec.end());

Choose appropriate containers:

  • std::vector: Random access, cache-friendly
  • std::deque: Fast insertion at both ends
  • std::list: Fast insertion/deletion in middle (but slow iteration)
  • std::unordered_map: O(1) average lookup
  • std::map: O(log n) lookup, ordered

Compile-Time Computation

Move work to compile-time:

// Runtime computation
int factorial_runtime(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

// Compile-time computation
constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

constexpr int fact5 = factorial(5);  // Computed at compile-time

Parallel Algorithms (C++17)

#include <algorithm>
#include <execution>

std::vector<int> vec(1000000);

// Sequential
std::sort(vec.begin(), vec.end());

// Parallel
std::sort(std::execution::par, vec.begin(), vec.end());

// Parallel + vectorized
std::sort(std::execution::par_unseq, vec.begin(), vec.end());

String Operations

// Slow: creates many temporary strings
std::string result = str1 + str2 + str3 + str4;

// Fast: single allocation
std::string result;
result.reserve(str1.size() + str2.size() + str3.size() + str4.size());
result += str1;
result += str2;
result += str3;
result += str4;

// Or use std::format (C++20)
auto result = std::format("{}{}{}{}", str1, str2, str3, str4);

Virtual Function Overhead

// Virtual call: runtime dispatch
struct Base {
    virtual void process() { }
};

// Template: compile-time dispatch (faster)
template<typename T>
void process(T& obj) {
    obj.process();  // No virtual call
}

// Consider CRTP for static polymorphism
template<typename Derived>
struct Base {
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

Memory Pools

For many small allocations:

#include <memory_resource>

std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> vec(&pool);  // Uses pool allocator

// All allocations come from pool - much faster

Benchmarking

Use Google Benchmark:

#include <benchmark/benchmark.h>

static void BM_VectorPush(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> vec;
        for (int i = 0; i < state.range(0); ++i) {
            vec.push_back(i);
        }
    }
}

BENCHMARK(BM_VectorPush)->Range(8, 8<<10);

BENCHMARK_MAIN();

Common Pitfalls

  1. Premature optimization: Profile first!
  2. Over-optimization: Hurting readability
  3. Ignoring algorithmic complexity: O(n²) vs O(n log n)
  4. Not using compiler optimizations: Always use -O2 or -O3
  5. Unnecessary abstractions: Virtual functions when not needed

Best Practices

  1. Measure, don’t guess: Use profilers
  2. Optimize hot paths: Focus on 10% of code that takes 90% of time
  3. Choose right algorithms: Big-O matters more than micro-optimizations
  4. Use modern C++ idioms: Move semantics, RVO, constexpr
  5. Enable compiler optimizations: -O2/-O3, LTO
  6. Cache-friendly data structures: Contiguous memory
  7. Avoid copies: References, move, in-place construction
Conceptual Check

What should you do BEFORE optimizing code?

Interactive Lab

Complete the Code

std::vector<int> vec;
// Reserve space for 1000 elements to avoid reallocations
vec.(1000);
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <chrono>
4 
5int main() {
6 const int n = 1000000;
7
8 // Without reserve
9 auto start = std::chrono::high_resolution_clock::now();
10 std::vector<int> vec1;
11 for (int i = 0; i < n; ++i) {
12 vec1.push_back(i);
13 }
14 auto end = std::chrono::high_resolution_clock::now();
15 auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
16
17 // With reserve
18 start = std::chrono::high_resolution_clock::now();
19 std::vector<int> vec2;
20 vec2.reserve(n);
21 for (int i = 0; i < n; ++i) {
22 vec2.push_back(i);
23 }
24 end = std::chrono::high_resolution_clock::now();
25 auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
26
27 std::cout << "Without reserve: " << duration1.count() << " μs\n";
28 std::cout << "With reserve: " << duration2.count() << " μs\n";
29 std::cout << "Speedup: " << (double)duration1.count() / duration2.count() << "x\n";
30
31 return 0;
32}
System Console

Waiting for signal...

Section Detail

Memory Management Deep Dive

Advanced Memory Management

C++ provides extensive control over memory management. Understanding allocators, alignment, and memory debugging tools is crucial for high-performance applications.

Memory Allocation Basics

// Stack allocation
int x = 42;  // Automatic storage duration
char buffer[1024];  // Fixed size, fast

// Heap allocation
int* ptr = new int(42);  // Manual lifetime
delete ptr;

auto vec = std::make_unique<std::vector<int>>();  // Automatic cleanup

Alignment

alignof and alignas

struct alignas(16) Aligned {
    double x, y;
};

static_assert(alignof(Aligned) == 16);
static_assert(sizeof(Aligned) == 16);  // Padded to alignment

// Cache line alignment
struct alignas(64) CacheAligned {
    int data;
    // Padded to 64 bytes
};

std::aligned_storage

#include <type_traits>

alignas(16) std::byte buffer[sizeof(MyClass)];

// Construct object in aligned buffer
MyClass* obj = new (buffer) MyClass(args);

// Destroy when done
obj->~MyClass();

Dynamic Aligned Allocation

// C++17: aligned new
void* ptr = ::operator new(size, std::align_val_t{64});
::operator delete(ptr, std::align_val_t{64});

// Aligned unique_ptr
template<typename T, std::size_t Alignment>
struct AlignedDeleter {
    void operator()(T* ptr) {
        ptr->~T();
        ::operator delete(ptr, std::align_val_t{Alignment});
    }
};

template<typename T, std::size_t Alignment, typename... Args>
auto make_aligned_unique(Args&&... args) {
    void* mem = ::operator new(sizeof(T), std::align_val_t{Alignment});
    T* ptr = new (mem) T(std::forward<Args>(args)...);
    return std::unique_ptr<T, AlignedDeleter<T, Alignment>>(ptr);
}

Custom Allocators

Basic Allocator Interface

template<typename T>
class MyAllocator {
public:
    using value_type = T;
    
    MyAllocator() = default;
    
    template<typename U>
    MyAllocator(const MyAllocator<U>&) noexcept {}
    
    T* allocate(std::size_t n) {
        if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
            throw std::bad_array_new_length();
        }
        
        void* ptr = ::operator new(n * sizeof(T));
        return static_cast<T*>(ptr);
    }
    
    void deallocate(T* ptr, std::size_t) noexcept {
        ::operator delete(ptr);
    }
};

template<typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) { return true; }

template<typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) { return false; }

// Usage
std::vector<int, MyAllocator<int>> vec;

Stack Allocator

Allocate from stack buffer:

template<typename T, std::size_t N>
class StackAllocator {
    alignas(T) std::byte buffer_[N * sizeof(T)];
    std::byte* ptr_ = buffer_;
    
public:
    using value_type = T;
    
    T* allocate(std::size_t n) {
        if (ptr_ + n * sizeof(T) > buffer_ + sizeof(buffer_)) {
            throw std::bad_alloc();
        }
        T* result = reinterpret_cast<T*>(ptr_);
        ptr_ += n * sizeof(T);
        return result;
    }
    
    void deallocate(T*, std::size_t) noexcept {
        // No-op for stack allocator
    }
};

// Fast vector for small sizes
std::vector<int, StackAllocator<int, 100>> small_vec;

Memory Pools

Fixed-Size Pool

template<typename T, std::size_t BlockSize = 4096>
class MemoryPool {
    union Node {
        T value;
        Node* next;
    };
    
    struct Block {
        std::byte data[BlockSize];
        Block* next;
    };
    
    Block* blocks_ = nullptr;
    Node* free_list_ = nullptr;
    
public:
    ~MemoryPool() {
        while (blocks_) {
            Block* next = blocks_->next;
            ::operator delete(blocks_);
            blocks_ = next;
        }
    }
    
    T* allocate() {
        if (!free_list_) {
            allocate_block();
        }
        
        Node* node = free_list_;
        free_list_ = node->next;
        return &node->value;
    }
    
    void deallocate(T* ptr) {
        Node* node = reinterpret_cast<Node*>(ptr);
        node->next = free_list_;
        free_list_ = node;
    }
    
private:
    void allocate_block() {
        Block* block = static_cast<Block*>(::operator new(sizeof(Block)));
        block->next = blocks_;
        blocks_ = block;
        
        const std::size_t node_count = BlockSize / sizeof(Node);
        Node* nodes = reinterpret_cast<Node*>(block->data);
        
        for (std::size_t i = 0; i < node_count; ++i) {
            nodes[i].next = free_list_;
            free_list_ = &nodes[i];
        }
    }
};

Polymorphic Memory Resources (C++17)

std::pmr provides runtime polymorphism for allocators:

#include <memory_resource>

// Monotonic allocator: fast, no deallocation
std::pmr::monotonic_buffer_resource pool;

std::pmr::vector<int> vec1(&pool);
std::pmr::string str(&pool);

// All allocations from pool

// Pool memory released when pool destroyed

Pool Options

// Unsynchronized pool: single-threaded
std::pmr::unsynchronized_pool_resource pool;

// Synchronized pool: thread-safe
std::pmr::synchronized_pool_resource thread_safe_pool;

// New/delete resource: default
std::pmr::new_delete_resource();

// Null resource: throws on allocation
std::pmr::null_memory_resource();

Custom pmr Resource

class LoggingResource : public std::pmr::memory_resource {
    std::pmr::memory_resource* upstream_;
    
public:
    LoggingResource(std::pmr::memory_resource* upstream)
        : upstream_(upstream) {}
    
protected:
    void* do_allocate(std::size_t bytes, std::size_t alignment) override {
        std::cout << "Allocating " << bytes << " bytes\n";
        return upstream_->allocate(bytes, alignment);
    }
    
    void do_deallocate(void* ptr, std::size_t bytes, std::size_t alignment) override {
        std::cout << "Deallocating " << bytes << " bytes\n";
        upstream_->deallocate(ptr, bytes, alignment);
    }
    
    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        return this == &other;
    }
};

Memory Debugging

Address Sanitizer (ASan)

Detects memory errors:

# Compile with ASan
g++ -fsanitize=address -g program.cpp -o program

# Run
./program

Detects:

  • Use after free
  • Heap buffer overflow
  • Stack buffer overflow
  • Memory leaks
  • Use after return

Example:

int main() {
    int* ptr = new int[10];
    delete[] ptr;
    
    ptr[0] = 42;  // ASan detects: heap-use-after-free
}

Valgrind

# Check for memory leaks
valgrind --leak-check=full ./program

# Memory profiler
valgrind --tool=massif ./program
ms_print massif.out.*

Memory Sanitizer (MSan)

Detects uninitialized memory reads:

clang++ -fsanitize=memory -g program.cpp -o program

Smart Pointer Internals

unique_ptr Implementation

template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
    T* ptr_ = nullptr;
    [[no_unique_address]] Deleter deleter_;
    
public:
    unique_ptr() = default;
    
    explicit unique_ptr(T* ptr) : ptr_(ptr) {}
    
    ~unique_ptr() {
        if (ptr_) deleter_(ptr_);
    }
    
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
    
    unique_ptr(unique_ptr&& other) noexcept 
        : ptr_(other.ptr_), deleter_(std::move(other.deleter_)) {
        other.ptr_ = nullptr;
    }
    
    unique_ptr& operator=(unique_ptr&& other) noexcept {
        reset(other.release());
        deleter_ = std::move(other.deleter_);
        return *this;
    }
    
    T* release() noexcept {
        T* ptr = ptr_;
        ptr_ = nullptr;
        return ptr;
    }
    
    void reset(T* ptr = nullptr) noexcept {
        T* old_ptr = ptr_;
        ptr_ = ptr;
        if (old_ptr) deleter_(old_ptr);
    }
    
    T* get() const noexcept { return ptr_; }
    T& operator*() const { return *ptr_; }
    T* operator->() const noexcept { return ptr_; }
    explicit operator bool() const noexcept { return ptr_ != nullptr; }
};

shared_ptr Reference Counting

// Simplified shared_ptr internals
template<typename T>
class shared_ptr {
    T* ptr_ = nullptr;
    
    struct ControlBlock {
        std::atomic<long> ref_count{1};
        std::atomic<long> weak_count{1};
        
        virtual ~ControlBlock() = default;
        virtual void destroy_object() = 0;
        virtual void destroy_control_block() = 0;
    };
    
    ControlBlock* control_ = nullptr;
    
public:
    shared_ptr(T* ptr) : ptr_(ptr) {
        control_ = new ControlBlockImpl(ptr);
    }
    
    shared_ptr(const shared_ptr& other) 
        : ptr_(other.ptr_), control_(other.control_) {
        if (control_) {
            control_->ref_count.fetch_add(1, std::memory_order_relaxed);
        }
    }
    
    ~shared_ptr() {
        if (control_ && control_->ref_count.fetch_sub(1) == 1) {
            control_->destroy_object();
            if (control_->weak_count.fetch_sub(1) == 1) {
                control_->destroy_control_block();
            }
        }
    }
};

Memory Order and Synchronization

#include <atomic>

std::atomic<int> counter{0};

// Relaxed: no synchronization
counter.fetch_add(1, std::memory_order_relaxed);

// Acquire-Release: synchronize with matching operations
counter.store(1, std::memory_order_release);
int value = counter.load(std::memory_order_acquire);

// Sequential consistency: total order (default)
counter.fetch_add(1, std::memory_order_seq_cst);

RAII Patterns

Scope Guard

template<typename F>
class ScopeGuard {
    F cleanup_;
    bool active_ = true;
    
public:
    explicit ScopeGuard(F cleanup) : cleanup_(std::move(cleanup)) {}
    
    ~ScopeGuard() {
        if (active_) cleanup_();
    }
    
    void dismiss() { active_ = false; }
    
    ScopeGuard(const ScopeGuard&) = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;
};

template<typename F>
auto make_scope_guard(F cleanup) {
    return ScopeGuard<F>(std::move(cleanup));
}

// Usage
void process_file(const char* filename) {
    FILE* file = fopen(filename, "r");
    auto guard = make_scope_guard([=] { if (file) fclose(file); });
    
    // Use file...
    // Automatically closed on scope exit
}

Best Practices

  1. Prefer stack allocation: Faster and automatic cleanup
  2. Use smart pointers: unique_ptr by default, shared_ptr when needed
  3. Profile memory usage: Identify allocation hotspots
  4. Consider custom allocators: For performance-critical code
  5. Enable sanitizers in development: Catch bugs early
  6. Align data properly: For SIMD and cache efficiency
  7. Use pmr for runtime flexibility: When allocator type isn’t fixed
  8. Avoid memory leaks: RAII and smart pointers
Conceptual Check

What does alignof return?

Interactive Lab

Complete the Code

// Force struct to be aligned to 64-byte boundary (cache line)
struct (64) CacheAligned {
    int data[16];
};
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <memory_resource>
3#include <vector>
4 
5int main() {
6 // Stack buffer for allocations
7 std::byte buffer[1024];
8 std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
9
10 // Vector using custom allocator
11 std::pmr::vector<int> vec(&pool);
12
13 for (int i = 0; i < 10; ++i) {
14 vec.push_back(i);
15 }
16
17 std::cout << "Vector size: " << vec.size() << '\n';
18 std::cout << "Allocated from stack buffer\n";
19
20 // Memory automatically released when pool destroyed
21 return 0;
22}
System Console

Waiting for signal...

Section Detail

Best Practices and Idioms

C++ Best Practices

Modern C++ provides powerful features that, when used correctly, lead to clean, efficient, and maintainable code. This lesson covers essential idioms and best practices.

RAII: Resource Acquisition Is Initialization

The fundamental C++ idiom: tie resource lifetime to object lifetime.

// Bad: Manual resource management
void process_file() {
    FILE* file = fopen("data.txt", "r");
    // ... process ...
    fclose(file);  // Easy to forget!
}

// Good: RAII with smart pointers
void process_file() {
    auto file = std::unique_ptr<FILE, decltype(&fclose)>(
        fopen("data.txt", "r"), fclose
    );
    // Automatically closed
}

// Better: Use RAII wrappers
class File {
    FILE* file_;
public:
    explicit File(const char* path) : file_(fopen(path, "r")) {
        if (!file_) throw std::runtime_error("Failed to open file");
    }
    ~File() { if (file_) fclose(file_); }
    
    File(const File&) = delete;
    File& operator=(const File&) = delete;
    
    FILE* get() const { return file_; }
};

Rule of Zero/Three/Five

Rule of Zero

If you don’t need custom resource management, don’t define special members:

// Good: Compiler-generated special members work perfectly
struct Person {
    std::string name;
    int age;
    std::vector<std::string> hobbies;
    // Compiler generates correct copy/move/destructor
};

Rule of Five

If you define one special member, define all:

class Resource {
    int* data_;
    
public:
    // Constructor
    Resource(int size) : data_(new int[size]) {}
    
    // Destructor
    ~Resource() { delete[] data_; }
    
    // Copy constructor
    Resource(const Resource& other) 
        : data_(new int[other.size()]) {
        std::copy(other.data_, other.data_ + size(), data_);
    }
    
    // Copy assignment
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new int[other.size()];
            std::copy(other.data_, other.data_ + size(), data_);
        }
        return *this;
    }
    
    // Move constructor
    Resource(Resource&& other) noexcept 
        : data_(other.data_) {
        other.data_ = nullptr;
    }
    
    // Move assignment
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            other.data_ = nullptr;
        }
        return *this;
    }
};

Better: Use RAII Members

class Resource {
    std::unique_ptr<int[]> data_;
    size_t size_;
    
public:
    Resource(size_t size) 
        : data_(std::make_unique<int[]>(size)), size_(size) {}
    
    // Rule of Zero: compiler-generated special members work!
};

Const Correctness

class String {
    std::string data_;
    
public:
    // Read-only access: const method
    size_t length() const { return data_.length(); }
    const char* c_str() const { return data_.c_str(); }
    
    // Modification: non-const method
    void append(const std::string& str) { data_ += str; }
    
    // Const and non-const overloads
    char& operator[](size_t i) { return data_[i]; }
    const char& operator[](size_t i) const { return data_[i]; }
};

// Use const references to avoid copies
void print(const String& str) {  // Won't copy
    std::cout << str.c_str() << '\n';
}

Prefer References to Pointers

// Bad: Nullable, unclear ownership
void process(int* value) {
    if (value) {  // Must check for null
        *value += 10;
    }
}

// Good: Not nullable, clear value semantics
void process(int& value) {
    value += 10;  // Always valid
}

// When nullability is needed: std::optional
void process(std::optional<int>& value) {
    if (value) {
        *value += 10;
    }
}

Initialization

Prefer Uniform Initialization

// Direct initialization
int x{42};
std::string name{"Alice"};
std::vector<int> vec{1, 2, 3, 4, 5};

// Avoid narrowing conversions
int i{3.14};  // Error: narrowing

// For copy construction, use auto
auto str = std::string("Hello");  // Clear intent

Member Initialization

class Config {
    // In-class initializers (C++11)
    int timeout = 30;
    std::string host = "localhost";
    bool debug = false;
    
public:
    // Constructor: only set non-default values
    Config(std::string h) : host(std::move(h)) {}
};

Use auto Appropriately

// Good: Obvious type
auto count = 42;
auto name = std::string("Alice");
auto ptr = std::make_unique<Widget>();

// Good: Complex types
auto it = vec.begin();
auto result = some_function_with_long_return_type();

// Good: Generic code
template<typename T>
auto process(const T& value) {
    auto result = value.compute();
    return result;
}

// Bad: Type not obvious
auto x = get_value();  // What type is x?

// Better: Explicit when clarity helps
std::string x = get_value();

Range-Based For Loops

std::vector<int> vec{1, 2, 3, 4, 5};

// Read-only: const reference
for (const auto& value : vec) {
    std::cout << value << ' ';
}

// Modify: reference
for (auto& value : vec) {
    value *= 2;
}

// Copy when needed
for (auto value : vec) {
    value += 1;  // Modifies copy
}

Algorithms Over Raw Loops

// Bad: Manual loop
int sum = 0;
for (size_t i = 0; i < vec.size(); ++i) {
    sum += vec[i];
}

// Good: Algorithm expresses intent
int sum = std::accumulate(vec.begin(), vec.end(), 0);

// Better: Ranges (C++20)
int sum = std::ranges::fold_left(vec, 0, std::plus{});

More examples:

// Find element
auto it = std::find(vec.begin(), vec.end(), 42);

// Transform
std::vector<int> doubled;
std::transform(vec.begin(), vec.end(), 
               std::back_inserter(doubled),
               [](int x) { return x * 2; });

// Filter
std::vector<int> evens;
std::copy_if(vec.begin(), vec.end(),
             std::back_inserter(evens),
             [](int x) { return x % 2 == 0; });

Error Handling

Exceptions for Exceptional Cases

// Use exceptions for errors you can't handle locally
void process_file(const std::string& path) {
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("Cannot open file: " + path);
    }
    // Process...
}

// Handle where you can recover
try {
    process_file("data.txt");
} catch (const std::exception& e) {
    std::cerr << "Error: " << e.what() << '\n';
    // Fallback action
}

Expected for Expected Errors

// Use std::expected for errors that are expected
std::expected<int, std::string> parse_int(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::unexpected("Invalid integer");
    }
}

auto result = parse_int(input);
if (result) {
    use(*result);
} else {
    handle_error(result.error());
}

Avoid Premature Optimization

// Don't do this without profiling:
void process() {
    // "Optimization": manually unrolled loop, hard to read
    for (size_t i = 0; i < n; i += 4) {
        result[i] = data[i] * 2;
        result[i+1] = data[i+1] * 2;
        result[i+2] = data[i+2] * 2;
        result[i+3] = data[i+3] * 2;
    }
}

// Instead: Write clear code, let compiler optimize
void process() {
    for (size_t i = 0; i < n; ++i) {
        result[i] = data[i] * 2;
    }
    // Modern compilers will vectorize this automatically
}

Dependency Injection

// Bad: Hard-coded dependency
class UserManager {
    Database db;  // Tightly coupled
    
public:
    void save_user(const User& user) {
        db.insert(user);
    }
};

// Good: Inject dependency
class UserManager {
    Database& db_;  // Dependency injected
    
public:
    UserManager(Database& db) : db_(db) {}
    
    void save_user(const User& user) {
        db_.insert(user);
    }
};

// Better: Use interface
class IDatabase {
public:
    virtual ~IDatabase() = default;
    virtual void insert(const User&) = 0;
};

class UserManager {
    IDatabase& db_;
    
public:
    UserManager(IDatabase& db) : db_(db) {}
    
    void save_user(const User& user) {
        db_.insert(user);
    }
};

Prefer Composition to Inheritance

// Bad: Inheritance for reuse
class Stack : public std::vector<int> {
    // Exposes vector interface (push_back, etc.)
};

// Good: Composition
class Stack {
    std::vector<int> data_;
    
public:
    void push(int value) { data_.push_back(value); }
    int pop() { 
        int val = data_.back();
        data_.pop_back();
        return val;
    }
    // Only expose stack operations
};

Value Semantics

// Prefer value semantics
std::vector<std::string> names;  // Owns strings
names.push_back("Alice");        // Makes copy/move

// Over pointer semantics
std::vector<std::string*> names;  // Doesn't own
names.push_back(new std::string("Alice"));  // Manual memory management

Type Safety with Strong Types

// Bad: Primitive obsession
void transfer(int from_account, int to_account, int amount);
transfer(123, 100, 456);  // Easy to swap arguments!

// Good: Strong types
struct AccountId {
    int value;
    explicit AccountId(int v) : value(v) {}
};

struct Amount {
    int value;
    explicit Amount(int v) : value(v) {}
};

void transfer(AccountId from, AccountId to, Amount amount);
transfer(AccountId{123}, AccountId{456}, Amount{100});  // Type-safe!

Factory Functions

// Good: make_unique/make_shared
auto ptr = std::make_unique<Widget>(args);
auto shared = std::make_shared<Widget>(args);

// Custom factory
template<typename T, typename... Args>
auto create(Args&&... args) {
    return std::make_unique<T>(std::forward<Args>(args)...);
}

auto widget = create<Widget>(arg1, arg2);

Naming Conventions

// Classes: PascalCase
class UserManager {};

// Functions/variables: snake_case
void process_data();
int user_count;

// Constants: SCREAMING_SNAKE_CASE or kPascalCase
constexpr int MAX_USERS = 100;
constexpr int kMaxUsers = 100;

// Member variables: trailing underscore
class Widget {
    int value_;
    std::string name_;
};

// Template parameters: PascalCase
template<typename ValueType, typename AllocatorType>
class Container {};

Documentation

/**
 * @brief Computes the factorial of a number.
 * 
 * @param n The number (must be non-negative)
 * @return The factorial of n
 * @throws std::invalid_argument if n < 0
 */
int factorial(int n) {
    if (n < 0) {
        throw std::invalid_argument("n must be non-negative");
    }
    // ...
}

Modern C++ Checklist

✅ Use RAII for resource management
✅ Prefer smart pointers to raw pointers
✅ Use const correctness
✅ Prefer references to pointers
✅ Initialize variables at declaration
✅ Use auto when type is obvious
✅ Prefer algorithms to raw loops
✅ Use range-based for loops
✅ Follow Rule of Zero when possible
✅ Use exceptions for exceptional cases
✅ Avoid premature optimization
✅ Prefer composition to inheritance
✅ Use strong types for type safety
✅ Document public APIs
✅ Enable compiler warnings (-Wall -Wextra)

Conceptual Check

What is the Rule of Zero?

Interactive Lab

Complete the Code

// Make this function take a non-nullable reference
void increment(int_ value) {
    value += 1;
}
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <numeric>
5 
6int main() {
7 std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
8
9 // Modern C++: algorithms over raw loops
10 int sum = std::accumulate(numbers.begin(), numbers.end(), 0);
11
12 auto it = std::find_if(numbers.begin(), numbers.end(),
13 [](int x) { return x > 5; });
14
15 std::vector<int> evens;
16 std::copy_if(numbers.begin(), numbers.end(),
17 std::back_inserter(evens),
18 [](int x) { return x % 2 == 0; });
19
20 std::cout << "Sum: " << sum << '\n';
21 std::cout << "First > 5: " << *it << '\n';
22 std::cout << "Evens: ";
23 for (const auto& n : evens) {
24 std::cout << n << ' ';
25 }
26 std::cout << '\n';
27
28 return 0;
29}
System Console

Waiting for signal...

Section Detail

The C++ Journey: Past, Present, Future

The C++ Journey

C++ has evolved from a “C with Classes” in 1983 to a sophisticated, multi-paradigm language powering everything from embedded systems to game engines, financial systems to web browsers.

Historical Evolution

C++98: The First Standard

The first ISO standard brought:

  • Templates and STL
  • Exceptions
  • RTTI and dynamic_cast
  • Namespaces
  • bool type
// C++98 style
std::vector<int> vec;
for (std::vector<int>::iterator it = vec.begin(); 
     it != vec.end(); ++it) {
    std::cout << *it << '\n';
}

C++11: Modern C++ Begins

Revolutionary update with:

  • auto and decltype
  • Range-based for loops
  • Lambda expressions
  • Smart pointers (unique_ptr, shared_ptr)
  • Move semantics and rvalue references
  • constexpr
  • nullptr
  • Uniform initialization
  • Threading library
// C++11 style
auto vec = std::make_unique<std::vector<int>>();
for (const auto& item : *vec) {
    std::cout << item << '\n';
}

C++14: Refinements

Incremental improvements:

  • Generic lambdas
  • Return type deduction
  • Binary literals
  • std::make_unique
  • Variable templates
auto lambda = [](auto x, auto y) { return x + y; };

C++17: Major Features

Significant additions:

  • Structured bindings
  • if/switch with initializers
  • std::optional, std::variant, std::any
  • std::string_view
  • Filesystem library
  • Parallel algorithms
  • Fold expressions
std::map<std::string, int> map = {{"Alice", 30}};
if (auto [it, inserted] = map.insert({"Bob", 25}); inserted) {
    std::cout << "Inserted: " << it->first << '\n';
}

C++20: Transformative Release

Game-changing features:

  • Concepts
  • Ranges
  • Coroutines
  • Modules
  • Three-way comparison (spaceship operator)
  • Designated initializers
  • constexpr expansions
  • std::format
  • Calendar and timezone
// C++20: Concepts constrain templates
template<std::integral T>
T add(T a, T b) {
    return a + b;
}

// Ranges: composable algorithms
auto result = vec 
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * 2; });

C++23: Continued Evolution

Recent additions:

  • std::expected
  • Deducing this
  • std::print
  • Multidimensional subscript operator
  • static operator()
  • if consteval
  • std::flat_map and std::flat_set
// C++23: std::print
std::println("Hello, {}!", "World");

// std::expected
std::expected<int, Error> result = compute();
if (result) {
    process(*result);
}

The Current Ecosystem

Major Implementations

GCC (GNU Compiler Collection)

  • Open source
  • Excellent standards conformance
  • Wide platform support
  • Strong optimization

Clang/LLVM

  • Modern architecture
  • Fast compilation
  • Excellent diagnostics
  • Used by Apple, Google

MSVC (Microsoft Visual C++)

  • Windows-focused
  • Improving standards support
  • Integrated with Visual Studio
  • Good debugging tools

Intel C++ Compiler

  • Performance-focused
  • Advanced optimizations
  • Scientific computing

Build Systems

CMake: Industry standard, cross-platform
Meson: Fast, user-friendly
Bazel: Scalable, reproducible builds
xmake: Lua-based, modern

Package Managers

vcpkg: Microsoft-backed, cross-platform
Conan: Decentralized, flexible
Hunter: CMake-driven

Testing Frameworks

Google Test: Most widely used
Catch2: Header-only, BDD-style
Doctest: Fast, minimal

Libraries

Boost: Extensive, battle-tested
Abseil: Google’s C++ library
fmt: Fast formatting
spdlog: Fast logging
nlohmann/json: JSON processing
cpr: HTTP requests

Where C++ Excels

Systems Programming

  • Operating systems (Windows, Linux, macOS)
  • Device drivers
  • Embedded systems
  • Real-time systems

Performance-Critical Applications

  • Game engines (Unreal, Unity core)
  • Graphics (DirectX, OpenGL, Vulkan)
  • Scientific computing
  • High-frequency trading

Large-Scale Software

  • Databases (MySQL, MongoDB)
  • Web browsers (Chrome, Firefox)
  • Office suites (LibreOffice)
  • Media processing (Adobe tools)

Cross-Platform Development

  • Mobile: Native Android (NDK)
  • Desktop: Qt, wxWidgets
  • Server: Backend services

Future Directions

C++26 and Beyond

Proposed features:

  • Pattern matching
  • Reflection
  • Contracts
  • Executors
  • Linear algebra library
  • Improved compile times

Ongoing Improvements

Safety: Addressing memory safety concerns
Simplicity: Reducing complexity
Performance: Zero-cost abstractions
Tooling: Better IDE support
Modules: Universal adoption

Modern C++ Principles

Zero-Overhead Abstraction

// High-level abstractions...
auto result = vec 
    | std::views::filter(is_valid)
    | std::views::transform(compute);

// ...compile to efficient machine code
for (size_t i = 0; i < vec.size(); ++i) {
    if (is_valid(vec[i])) {
        result.push_back(compute(vec[i]));
    }
}

Type Safety

// Compile-time checking prevents errors
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<Numeric T>
T add(T a, T b) {  // Only accepts numeric types
    return a + b;
}

Resource Management

// RAII: no manual cleanup needed
{
    auto file = std::ifstream("data.txt");
    auto buffer = std::vector<char>(1024);
    // Automatic cleanup on scope exit
}

Learning Resources

Books

  • ”A Tour of C++” - Bjarne Stroustrup (overview)
  • “Effective Modern C++” - Scott Meyers (best practices)
  • “C++ Concurrency in Action” - Anthony Williams (threading)
  • “C++ Templates: The Complete Guide” - Vandevoorde et al.

Online

  • cppreference.com: Comprehensive reference
  • isocpp.org: Standard committee site
  • CppCon: Annual conference talks
  • C++ Weekly: Jason Turner’s videos

Communities

  • r/cpp: Reddit community
  • cpplang Slack: Real-time chat
  • Stack Overflow: Q&A
  • GitHub: Open source projects

Career Opportunities

C++ developers are in demand for:

  • Embedded Systems: IoT, automotive, aerospace
  • Game Development: AAA games, engines
  • Finance: Trading systems, risk analysis
  • Cloud Infrastructure: Distributed systems
  • AI/ML: TensorFlow, PyTorch backends
  • Computer Graphics: Rendering engines
  • Cybersecurity: System-level security tools

Typical roles:

  • Systems Engineer
  • Game Engine Programmer
  • Embedded Software Developer
  • Quantitative Developer
  • Graphics Engineer
  • Performance Engineer

Continuing Your Journey

Practice Projects

Beginner

  • Text-based games
  • Data structure implementations
  • File processing utilities

Intermediate

  • HTTP server
  • JSON parser
  • Simple database
  • 2D game engine

Advanced

  • Memory allocator
  • Multithreaded server
  • Graphics renderer
  • Programming language interpreter

Contributing to Open Source

Find projects on GitHub:

  • Fix bugs in libraries you use
  • Add features to tools
  • Improve documentation
  • Review pull requests

Stay Current

C++ evolves every three years:

  • Read committee papers
  • Experiment with new features
  • Attend conferences (CppCon, Meeting C++)
  • Follow C++ blogs and podcasts

The Philosophy of C++

“You don’t pay for what you don’t use” - Bjarne Stroustrup

C++ provides powerful abstractions without runtime cost. You can write high-level code that compiles to efficient machine code.

Multi-Paradigm

  • Procedural: C-style functions
  • Object-Oriented: Classes and inheritance
  • Generic: Templates and concepts
  • Functional: Lambdas and algorithms

Backward Compatibility

C++ maintains compatibility with previous versions, allowing gradual modernization of codebases.

Final Thoughts

C++ is complex but rewarding. It offers:

  • Control: Direct hardware access
  • Performance: Zero-overhead abstractions
  • Expressiveness: Modern features
  • Versatility: Multiple paradigms
  • Longevity: 40+ years, still evolving

The journey from C++98 to C++23 shows continuous improvement while maintaining backward compatibility. Modern C++ (C++11 onward) is a different language from legacy C++.

Key Takeaways:

  1. Use modern features (auto, ranges, smart pointers)
  2. Follow RAII and value semantics
  3. Leverage the type system (concepts, constexpr)
  4. Prefer composition and algorithms
  5. Profile before optimizing
  6. Keep learning - C++ never stops evolving

The C++ community is active and welcoming. Whether you’re building games, financial systems, embedded devices, or exploring systems programming, C++ provides the tools and performance you need.

Your journey doesn’t end here—it’s just beginning. Keep coding, keep learning, and embrace modern C++!

Conceptual Check

Which C++ version introduced move semantics and lambda expressions?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <vector>
3#include <ranges>
4#include <algorithm>
5 
6int main() {
7 std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
8
9 // Modern C++: from C++11 through C++20
10
11 // C++11: auto, range-for, lambda
12 auto squared = [](int x) { return x * x; };
13
14 // C++17: structured bindings (would need map)
15 // C++20: ranges
16 auto result = numbers
17 | std::views::filter([](int x) { return x % 2 == 0; })
18 | std::views::transform([](int x) { return x * x; });
19
20 std::cout << "Even numbers squared: ";
21 for (auto value : result) {
22 std::cout << value << ' ';
23 }
24 std::cout << '\n';
25
26 // The evolution continues...
27 std::cout << "C++ keeps evolving!\n";
28
29 return 0;
30}
System Console

Waiting for signal...

Congratulations on completing this C++ course! You now have a solid foundation in modern C++ programming. Keep practicing, building projects, and exploring the ever-evolving C++ ecosystem. Happy coding! 🚀