Chapter 1: From C to C++

C++ began as an extension of C that added strong abstraction tools while keeping tight control over performance and memory layout. This chapter sets the historical stage, shows how the compilation model works, explains why undefined behavior exists, highlights the signature features that differ from C, identifies which C parts remain essential, surveys language versions from C++98 to C++26, and teaches how to read standard library headers with confidence.

Why C++ grew from C

C provided portable systems programming with direct access to memory and predictable performance. As projects grew, developers needed stronger ways to manage complexity without losing that control. C++ grew from C by adding classes, templates, overloading, and zero cost abstractions that compile away in optimized builds. The design goal was to layer expressive tools on top of C’s power while keeping the programmer in charge of costs.

Zero cost abstraction as a guiding idea

A zero cost abstraction adds no overhead compared with a hand written C style equivalent once compiled with typical optimizations. For example, std::span carries a pointer and a size; the optimizer can inline checks or remove them entirely when proven unnecessary. You get clarity in code while retaining predictable machine code.

Backward compatibility and the C subset

C++ aimed to compile much existing C code with minor adjustments. This choice kept the ecosystem large and practical. It also meant C++ inherited C’s compilation model, pointer arithmetic, and many low level concerns; the language then built safety and structure on top through RAII, references, and strong typing.

💡 When porting C code, begin by isolating ownership boundaries and replacing manual resource cleanup with RAII (Resource Acquisition Is Initialization) wrappers; you will keep performance while gaining reliability. RAII means you acquire a resource in an object’s constructor and you release it in the destructor. The lifetime of the resource is bound to the lifetime of the object. When the object goes out of scope, cleanup happens automatically and predictably.

Classes solved real world problems

Early large C systems suffered from scattered invariants and manual resource discipline. Classes let you centralize invariants in constructors and destructors; overloading gives natural syntax for domain types; templates let you write algorithms once for many types with no runtime cost.

The compilation model

C++ compiles each source file after preprocessing into a translation unit, then links the resulting object files into an executable or library. Understanding this pipeline explains why duplicate definitions fail, why inline semantics matter, and why headers must be designed carefully.

The translation unit boundary

A translation unit is the source file after applying all #include directives and macros. Name lookup, inline choices, and template instantiation can depend on what the unit sees. One design goal is to keep headers self contained and minimal so each unit compiles reliably.

Managing declarations and definitions

Headers declare interfaces; source files define them. Re declarations are allowed, but multiple definitions across translation units cause linker errors. Place function and class declarations in headers; put non inline definitions in exactly one source file.

Using include guards and embracing modules

Traditional headers use include guards or #pragma once to prevent multiple inclusion. Modern C++ also supports modules that express interfaces explicitly and speed up builds. You can keep headers for interop while migrating performance sensitive parts to modules over time.

// file: math.hpp
#ifndef MATH_HPP
#define MATH_HPP
int add(int a, int b);
#endif
// file: math.cpp
#include "math.hpp"
int add(int a, int b) { return a + b; }
⚠️ Avoid defining non inline variables or non inline functions in headers; doing so will produce multiple-definition linker errors when several translation units include that header.

The order of compilation steps

The pipeline is preprocessing, compilation to object code, template instantiation during or after parsing as needed, optimization, and linking. Templates in headers are typically defined entirely in the header so every translation unit can instantiate them when required.

// A header-only template must include its definition
template<class T>
T square(T x) { return x * x; }

Undefined behavior and the cost of speed

Undefined behavior allows compilers to assume certain bad states never occur; in return the optimizer can remove checks and generate faster code. The trade off is that violating rules can produce arbitrary results that vary by build, platform, or input.

Cmmon undefined behavior patterns

Frequent sources include out of bounds access, use after free, signed integer overflow, dereferencing null pointers, and violating strict aliasing. Many of these issues are invisible at first because release builds optimize away evidence of failure.

// Signed overflow is undefined; result can vary
int mul(int a, int b) { return a * b; }

Applying tools to catch problems early

Use address, undefined, and thread sanitizers during development; combine with static analysis and assertions. These tools insert checks that make misbehavior visible while you still have context to fix it.

💡 Compile with -fsanitize=address,undefined on Clang or GCC during tests; many subtle lifetime and overflow bugs will surface quickly.

Designing APIs that reduce risk

Prefer safe views like std::span, return std::optional for missing values, and document preconditions as runtime asserts in debug builds. By moving checks to boundaries you keep hot loops lean.

Key differences from C

C++ keeps C’s low level power but adds first class language features for safe composition. Four ideas change daily coding: references for aliasing without null, overloading for natural APIs, namespaces for modular names, and classes for encapsulated invariants.

Using references to express intent

A reference must bind to an object at initialization and cannot be reseated. This models required aliases. Combine with const to promise read only access. For optional references prefer pointers or std::optional<std::reference_wrapper<T>>.

void scale(double &r, double factor) { r *= factor; }

Overloading functions and operators

Overloading lets you use the same function name for related operations. This reduces mental overhead and makes generic code easier to read, as long as conversions and ambiguity are managed carefully.

int area(int w, int h);
double area(double r); // circle

Organizing with namespaces

Namespaces prevent collisions across libraries by grouping related declarations. Use nested namespaces for structure. For local convenience bring in selective names with using at minimal scope.

namespace geom::poly { int sides(int n); }

Building invariants into classes and RAII

Classes bind data and operations. Constructors establish invariants; destructors release resources. The RAII pattern ensures cleanup runs on scope exit even during exceptions or early returns.

class File {
FILE* f;
public:
explicit File(const char* path) : f(std::fopen(path, "r")) {}
~File() { if (f) std::fclose(f); }
auto get() const -> FILE* { return f; }
};
⚠️ Prefer std::unique_ptr with a custom deleter or std::filesystem helpers for production code; a simple wrapper example is useful for teaching principles.

The C subset you still use

Modern C++ code still leans on core C ideas such as the object model, pointer arithmetic in constrained contexts, bitwise operations, and the portable ABI expectations that C established. Many system interfaces remain C based, and C++ interoperates with them smoothly.

Interfacing with C

Linkers expect specific symbol names. C++ performs name mangling to encode types; C does not. Declare C functions with extern "C" to keep a C compatible name so the linker can match them correctly.

extern "C" int c_api_call(int);

Working with POD layouts and memcpy

Plain old data structures with standard layout can be copied by std::memcpy. Once a type has non trivial constructors or virtual functions, prefer memberwise copy or moves; raw copying can violate invariants.

Using bitwise and fixed width integers

Flags, device registers, and protocols often require bitwise operations. Favor fixed width types such as std::uint32_t for clarity about size and overflow behavior.

std::uint32_t flags = 0u;
flags |= 1u << 3; // set bit 3
💡 Prefer enum class with explicit bit masks when possible; the stronger typing prevents accidental mixing of unrelated flags.

Standard versions at a glance

The language evolves through ISO standards that add features while keeping strong focus on compatibility and performance. The table lists headline additions you will encounter most often in real projects.

VersionYearHighlights
C++98/031998/2003Templates, exceptions, namespaces, iostreams, STL containers and algorithms
C++112011Move semantics, auto, range for, lambdas, constexpr, nullptr, unique_ptr/shared_ptr, threads
C++142014Generic lambdas, variable templates, relaxed constexpr, make_unique
C++172017string_view, optional, variant, filesystem, structured bindings, if with init
C++202020Concepts, ranges, coroutines, modules, constexpr on more library, calendar/time zones
C++232023std::expected, mdspan, print, span extensions, constexpr further expanded
C++262026Planned refinements, executors and networking candidates, pattern matching proposals, more ranges views

Selecting a standard level in your builds

Pass a flag to your compiler to pick a standard, for example -std=c++20 on GCC or Clang, or /std:c++20 on MSVC. Choose the newest your toolchain and platform support; newer standards give simpler code and better diagnostics.

# CMake
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

Planning for portability across compilers

Different vendors deliver features on their own schedules. For widely used libraries, gate code with feature test macros like __cpp_concepts and provide simple fallbacks. This approach keeps one codebase workable across many environments.

#if __cpp_concepts >= 201907
template<class T> concept Number = requires(T v) { v + v; };
#else
// … provide a traits-based substitute
#endif
⚠️ Feature test macros indicate presence, not completeness; verify corner cases in your CI matrix when relying on very new library facilities.

Reading the standard library headers

Standard headers package related facilities behind stable names such as <vector>, <span>, and <chrono>. Learning the naming patterns helps you guess where things live and what lifetime or iterator requirements they expect.

Mapping components to headers

Containers live in headers named after them, algorithms in <algorithm>, iterators in <iterator>, string utilities in <string> or <string_view>, formatting in <format>, and time utilities in <chrono>. If a facility builds on another it often includes that dependency for you.

USynopsis style and <...> placeholders

The standard presents each header with a synopsis that sketches declarations using placeholders such as <...> for template parameters and … for omitted sections. Real headers are implementation specific; treat the synopsis as a contract rather than a literal file.

// <vector> synopsis style sketch, not a literal header
namespace std {
template<class T, class Allocator = allocator<T>>
class vector {
  // … members and typedefs
};
}

Including only what you need and letting ADL work

Include the most specific header that declares the symbols you use. For free functions that participate in templates, place them in your own namespace and rely on argument dependent lookup so generic algorithms can find them without global using declarations.

⚠️ ADL (Argument Dependent Lookup) means that when a function call is made, the compiler searches not only the current scope but also the namespaces associated with the argument types. This lets generic code find the correct overload without global using directives.
#include <algorithm>
#include <vector>

namespace geom {
struct Point { double x, y; };
}

int main() {
std::vector<geom::Point> pts{{0,0}, {1,1}};
std::ranges::for_each(pts, [](auto const& p){ /* … */ });
}
💡 When a header name and a component share a name, include that header directly; this keeps compile times lower and makes intent obvious in code review.

Distinguishing headers from modules in modern code

Headers remain universal; modules provide faster builds and clearer interfaces where supported. You can import standard library modules in some environments, for example import std;. Keep headers as the default for portability and phase in modules where the toolchain is ready.

Chapter 2: Toolchains, Projects, and Build Systems

C++ development lives inside a constellation of compilers, linkers, build generators, package managers, and diagnostic tools. This chapter gives you a grounded view of the major toolchains and shows how to structure projects so they build cleanly and portably across platforms.

Compilers: GCC, Clang, and MSVC

Three families of compilers dominate C++ work today. GCC offers broad Unix style coverage and mature optimizers; Clang provides fast diagnostics and toolchain support for static analysis; MSVC anchors Windows development with strong ABI stability and deep IDE integration. Each follows the same language standard but differs in flags, extension sets, and code generation details.

Comparing their personalities and strengths

GCC performs aggressive optimizations and supports bleeding edge features quickly once standardized. Clang produces some of the clearest error messages and powers many analysis tools such as clang tidy. MSVC provides stable support for Windows specific libraries and has its own set of warnings that catch subtle portability issues. Understanding each helps you pick the right tool for the job.

Cross platform projects

Portable libraries often test against all three compilers in CI. This practice exposes assumptions about calling conventions, undefined behavior, or non standard extensions early. A good rule is to treat builds that pass on all three as healthy and maintainable long term.

Interacting with compilers

The front end parses code into an abstract syntax tree; the optimizer performs passes; the backend emits assembly; the driver links everything into an executable. Flags such as -O2, -g, -Wall, and -std=c++20 instruct the driver on which phases to activate and how aggressively to tune them.

💡 When comparing compilers for performance, measure complete optimized binaries rather than intermediate assembly; optimizers can remove large parts of naive microbenchmarks.

Command line flags

Compilers need explicit instructions about optimization level, warnings, debug info, and the desired C++ standard. Flags differ slightly across vendors but share a common spirit. Selecting the correct set shapes the behavior of the entire build.

Using common flag patterns

GCC and Clang rely on short flags such as -std=c++20, -O3, -Wall, and -Wextra. MSVC uses switch style flags such as /std:c++20, /O2, and /W4. For serious development prefer a strict warning level combined with optimizations and debug symbols.

# GCC or Clang
g++ main.cpp -std=c++20 -Wall -Wextra -O2 -g -o app

Enabling and verifying the chosen standard

When you set the standard level the compiler adopts that language version’s rules for syntax and semantics. Testing with a small feature specific program verifies that the flag works as intended. For example, using concept keywords confirms C++20 support.

// concept test
template<class T> concept Num = requires(T x){ x + x; };
int main() { return 0; }

Configuring builds across compilers

Cross platform scripts detect the compiler and apply the matching flag family. CMake automates this; you set a language standard once and let the generator map it to the correct backend flags.

⚠️ Avoid mixing flags from different compiler families; for example, MSVC will not recognize -std=c++20 and may silently fall back to an older mode.

Organizing sources and headers

A predictable project layout makes navigation easy and prevents linker errors. The general idea is to place public interfaces in headers, private implementation in source files, and keep directory names meaningful. Consistent organization helps compilers find definitions cleanly.

Separating declarations and definitions

Headers store class declarations, function prototypes, inline templates, and small helpers. Source files hold function definitions and complex logic. This structure keeps rebuilds fast because modifying a source file does not force all consumers to recompile.

// include/math.hpp
int add(int a, int b);
// src/math.cpp
#include "math.hpp"
int add(int a, int b) { return a + b; }

Arranging directories

Common layouts include include/ for public headers, src/ for implementation, tests/ for unit tests, and cmake/ for build scripts. Public header paths often mirror namespace structure, reducing cognitive load for readers.

Avoiding unnecessary recompilation

Small changes in a header can trigger full rebuilds. Keeping template heavy code in separate header only modules and reducing cross includes keeps compile times low and project structure healthy.

💡 Prefer forward declarations when possible. Including a full header is required only when the implementation needs complete type information.

CMake essentials

CMake generates platform specific build files and lets you specify targets, dependencies, and configuration settings in a portable way. It has become the standard build system for C++ projects because it separates intent from platform quirks.

Describing targets

A target is an executable or library paired with sources, include directories, and compile options. Well defined targets reduce global state and produce reproducible builds. CMake’s target_* commands attach properties directly to targets rather than the entire project.

# CMakeLists.txt snippet
add_executable(app main.cpp)
target_compile_features(app PUBLIC cxx_std_20)

Managing include paths and dependencies

Use target_include_directories to set include paths in a scoped way. PRIVATE, PUBLIC, and INTERFACE keywords control propagation. This model lets you describe how components relate without relying on fragile global defines.

Generating builds across platforms

CMake emits Makefiles on Unix systems, Ninja files when requested, and Visual Studio project files on Windows. This flexibility lets one configuration file support many environments without rewriting build logic.

⚠️ Avoid using add_definitions or global include directories when target scoped alternatives exist; global settings can create subtle dependency problems.

Package managers

Dependency management improves reproducibility and keeps third party libraries isolated from system installations. vcpkg and Conan are two widely used package managers that integrate well with CMake.

Using vcpkg

vcpkg installs libraries in a controlled directory and provides integration scripts that CMake picks up automatically. Its design favors simplicity and predictable layouts. Once integrated, find_package can locate libraries without extra configuration.

# Example
vcpkg install fmt

Using Conan

Conan creates binary or source packages for many platforms and lets you set options such as shared vs static builds or specific compiler versions. Its recipe system helps teams maintain consistent dependency graphs across environments.

Choosing the right manager

Small desktop projects often prefer vcpkg. Larger teams with strict versioning or cross compilation needs often choose Conan instead. Both integrate with modern CMake smoothly.

💡 Keep dependency versions pinned through lock files; uncontrolled upgrades can introduce ABI mismatches or silent behavior changes.

Static, shared, and header only libraries

C++ code can be packaged in different forms that affect how linking works, how updates propagate, and how symbols are resolved. Understanding the distinctions helps you select the correct form for each component.

Building static libraries

A static library bundles object files into a single archive that is copied into the final executable at link time. This approach produces self contained binaries that do not depend on external copies of the library.

# CMake
add_library(util STATIC util.cpp)

Using shared libraries

Shared libraries live on the system and are loaded at runtime. Updating a shared library can upgrade many programs at once but requires careful ABI discipline to prevent breakage. API stability matters greatly for shared libraries.

Designing header only libraries

Header only libraries contain all functionality in headers, usually through templates or inline functions. They avoid linking steps and improve portability at the cost of longer build times when headers change.

⚠️ A header only design should avoid complex global state and keep compile time overhead manageable; reckless template metaprogramming can slow down entire builds.

Sanitizers and warnings

Modern compilers and runtimes offer strong diagnostic tools that reveal undefined behavior, data races, memory leaks, and suspicious code patterns. Using them early keeps projects robust and maintainable.

Enabling sanitizers

AddressSanitizer detects buffer overflows and use after free; UndefinedBehaviorSanitizer catches rule violations; ThreadSanitizer discovers race conditions. You enable them with simple flags in GCC or Clang.

g++ main.cpp -fsanitize=address,undefined -g -O1

Treating warnings as errors

Warnings hint at code that may be incorrect or non portable. Turning them into errors forces you to address issues immediately. Consistent warning policies across a team encourage clean, predictable coding habits.

Balancing debug and release configurations

Debug builds use assertions, sanitizers, and reduced optimizations; release builds maximize speed and remove checks. Maintaining both ensures correctness during development and high performance in production binaries.

💡 Use continuous integration to build both debug and release variants. This practice catches sanitizer flagged bugs while confirming that optimized builds still behave correctly.

Chapter 3: Lexical Elements, Declarations, and Initialization

This chapter tours the surface of C++ that the compiler reads first: the tokens, literals, and punctuation that form source code; the ways you declare objects and types; how initialization works; how the language helps you infer types with auto and decltype; how qualifiers such as const alter types; and how names are found across scopes and translation units. By the end you will read C++ code with clearer eyes and write declarations that say exactly what you mean.

Tokens, literals, and basic syntax

C++ is processed as a stream of tokens. A token is a smallest meaningful unit such as an identifier, a keyword, a literal, an operator, or a punctuation mark. Whitespace usually separates tokens. Comments are removed during translation, so they never become tokens. Understanding tokens helps you read tricky declarations and spot where one token ends and the next begins.

Recognizing tokens

Identifiers name things; keywords reserve language features; literals embed values like numbers or text; operators and punctuators shape expressions and statements. The compiler performs maximal munch when forming tokens (it takes the longest valid token), so >>= is one token rather than >> followed by =.

⚠️ The sequence template<<T>> in expressions can collide with >> as a shift operator in older code. Modern C++ parses nested template closers without extra spaces, yet spacing can still aid clarity.

Working with literals

Literals include integers, floating points, characters, strings, and booleans. You can add suffixes such as u, l, ull for integers; f, l for floating points; and s, sv for string types when using the standard literal operators. Digit separators with single quotes improve readability.

auto dec = 1'000'000;    // int
auto hex = 0xFFu;        // unsigned int
auto bin = 0b1010′1100;  // binary literal
auto fp  = 6.022e23f;    // float
auto ch  = 'é';          // UTF-8 code unit in narrow char
                         // literal (implementation encoding)
auto up  = U'𝛑';         // char32_t
auto s   = "hello"s;     // std::string via ""s
auto sv  = "view"sv;     // std::string_view via ""sv

Separating statements and grouping

Statements usually end with a semicolon, and blocks use braces to form compound statements. When showing elided content inside braces, write { … } to indicate omitted statements or members.

💡 Prefer braces around multi-statement bodies even if the grammar allows a single statement, since this prevents accidental attachment of future lines.

Types, objects, and declarations

A declaration introduces a name and its type. A definition allocates storage or provides the body for a function or class. Every object has a type that determines its size, alignment, and operations. Declarations can be simple or very dense, so read them from the name outward.

Declaring and defining clearly

You declare a variable by providing its type and a name; you define it when you also create storage. You declare a function by providing its type and name; you define it when you also provide the function body.

extern int g;                        // declaration (no storage here)
int g = 42;                          // definition

int square(int x);                   // function declaration
int square(int x) { return x * x; }  // definition

Understanding declarator syntax

In a full declaration, the base type appears first, followed by the declarator that attaches pointers, references, arrays, and function parts to the name. Reading from the identifier outward prevents confusion.

int* p;          // p is a pointer to int
int& r = g;      // r is a reference to int
int a[10];       // a is an array of 10 int
int (*fp)(int);  // fp is a pointer to function taking int and returning int
⚠️ The placement of * binds to the declarator, not the base type. Write one name per line when mixing pointers and non-pointers to avoid surprises.

Initialization styles

C++ offers several initialization forms. Each form has distinct rules, especially around narrowing conversions and constructor selection. Pick the form that expresses intent; do not treat them as interchangeable.

Using direct initialization

Direct initialization calls constructors or performs direct conversion. Parentheses signal that a constructor or converting function should be considered.

std::string s("hi");       // direct
std::vector<int> v(3, 1);  // 3 elements, all 1
auto x = int(3.14);        // direct conversion, value becomes 3

Using copy initialization

Copy initialization uses =. It can call constructors too, although some narrowing can occur and some constructors may be considered explicit only for direct initialization.

std::string s = "hi";          // copy
std::vector<int> v = {1,2,3};  // copy-list (still uses list rules)
auto n = 3.0f;                 // copy from float literal

Using list initialization

Braced lists create objects with uniform initialization rules. List initialization prevents narrowing by default and prefers initializer_list constructors when available.

int a{42};                  // ok
// int b{3.14};             // error: narrowing
std::vector<int> v{1,2,3};  // chooses initializer_list constructor

Aggregates and aggregate initialization

Aggregates are simple classes or arrays that have public data members, no user-declared constructors, no virtual functions, and no private or protected non-static data. They can be initialized directly with braces in member order.

struct Point { int x; int y; };
Point p{10, 20};            // aggregate initialization
Point q{ .x = 1, .y = 2 };  // designated initializers (C++20)
💡 Prefer list initialization for aggregates and for situations where you want to guard against narrowing; it communicates shape and safety in one step.
FormSyntaxNotes
DirectT obj(args)Calls constructors; can pick explicit ones.
CopyT obj = expr;May call constructors; explicit constructors are not considered.
ListT obj{args}Prevents narrowing; prefers initializer_list when present.
AggregateAggregate{m1, m2, …}Initializes members in declaration order.

Type deduction

Type deduction reduces noise and repetition. With auto, the compiler deduces a variable’s type from its initializer. With decltype, you ask for the type of an expression without evaluating it. Together they make templates and modern code more readable.

Deducing with auto

auto drops top-level const and references in deduction, and it follows template deduction rules. You can add const, &, or && on top of the deduced type to control binding.

const int ci = 42;
auto a = ci;        // a is int
const auto b = ci;  // b is const int
auto& r = a;        // r is int&
auto&& rr = 5;      // rr is int&& bound to a temporary
                    // (lifetime until end of full-expression)

Capturing types with decltype

decltype(expr) yields the declared type of a name or the type of the expression, including references. If you want the type of the expression as a plain value, wrap the name with ( ) rules in mind.

int x = 0;
decltype(x)   d1 = x;  // int
decltype((x)) d2 = x;  // int& (lvalue expression)
decltype(42)  d3 = 0;  // int
⚠️ The difference between decltype(x) and decltype((x)) is significant. The second form preserves lvalue reference when the expression is an lvalue.

const/volatile qualifiers and references

The cv qualifiers modify types: const promises no mutation through that path; volatile marks objects that can change for reasons outside the program’s control. References alias objects and come in two flavors: lvalue references (&) and rvalue references (&&). The combination of cv and references determines what operations are permitted.

Applying const

Top-level const applies to the object itself; low-level const applies through a pointer or reference. Use const to signal intent and enable optimizations, and prefer initializing const objects directly.

const int ci = 7;       // top-level const
const int* p = &ci;     // low-level const (points to const int)
int const* q = &ci;     // same type as above
int* const r = nullptr; // top-level const (const pointer to int)

Binding references

Lvalue references bind to named objects. Rvalue references bind to temporaries and movable results, which enables efficient transfers of resources. A const& can bind to temporaries, which is useful for parameters.

std::string make(); 
std::string s = "hi";
std::string& lr = s;               // binds to lvalue
std::string&& rr = make();         // binds to rvalue (temporary)
const std::string& cr = "hello"s;  // binds to temporary via const lvalue reference
💡 Use const& for parameters that accept both lvalues and temporaries without copying; use && and perfect forwarding for templates that must preserve value category.

Name lookup, scope, and linkage

C++ resolves a name by searching scopes in a defined order. The scope determines visibility; the linkage determines whether the same name refers to the same entity across translation units. Reading the rules helps avoid accidental hiding and multiple-definition errors.

Understanding scope rules

Block scope begins at a brace and ends at the matching brace. Class scope and namespace scope introduce their own member lookup rules. A name declared in an inner scope hides an outer one with the same name.

int x = 1;
void f() { 
  int x = 2;  // hides outer x
  { int x = 3; /* … */ }
}

Choosing appropriate linkage

Names can have external linkage, internal linkage, or no linkage. static at namespace scope gives internal linkage to variables and functions in that translation unit. extern declares names with external linkage that can be defined elsewhere.

// a.cpp
static int counter = 0;  // internal linkage
void tick();             // extern by default for functions

// b.cpp
extern int counter;      // refers to a different name
                         // (b.cpp cannot see a.cpp's static)

Navigating unqualified and qualified lookup

Unqualified lookup searches local, class, and namespace scopes in a defined order. Qualified lookup with :: follows the scopes you name. For function calls, argument-dependent lookup (ADL) also inspects associated namespaces of the argument types.

namespace geo { struct Point {}; void draw(Point); }
void draw(int);

int main() {
  geo::Point p;
  draw(p);     // finds geo::draw via ADL
  ::draw(42);  // qualified lookup chooses global draw(int)
}
⚠️ Overload sets can change when you add using-directives or new functions in associated namespaces. Prefer explicit qualification or using declarations with care in headers.

Chapter 4: Storage Duration, Lifetime, and Ownership

This chapter explores how C++ decides where objects live, how long they exist, and who controls their cleanup. The language gives several storage durations, each with distinct behavior. You will learn how constructors and destructors define lifetime, why RAII anchors the safety model, and how clear ownership rules keep code stable even when exceptions slice through the flow. By the end, you will navigate lifetime issues with steadier instincts.

Types of storage

C++ objects live in one of four main storage durations. Automatic storage covers local variables that appear when a block begins and disappear when it ends. Static storage covers global and namespace-scope variables plus function-level static variables that exist for the entire program. Thread storage associates objects with a single thread. Dynamic storage handles allocations that you request through new, allocators, or custom memory pools.

Automatic storage

Objects with automatic storage are born when execution enters their block and are destroyed when the block ends. Their destructors always run, including during stack unwinding from exceptions. This predictable pattern makes automatic storage the safest and most expressive default.

void f() { 
  std::string s = "auto"; 
  /* … */ 
} // s destroyed here
💡 Place important cleanup inside destructors of local objects. This places your safety net in the language rather than in manual checks.

Static storage

Objects with static storage exist from program start to program end. Function-level static variables are initialized upon first entry in a thread-safe manner. Their destruction happens after main returns. Because static objects can create tricky initialization order problems across translation units, treat them with care.

static std::string config = "global"; // whole-program lifetime

Thread storage

thread_local objects provide separate instances for each thread. Creation happens when a thread begins and destruction happens when that thread ends. This works well for caches or state that must not be shared across threads.

thread_local int depth = 0; // separate for each thread

Dynamic storage

Dynamic storage lives between your explicit allocation and your explicit reclamation. Using new and delete directly is error prone because ownership becomes clear only in comments or unwritten code. Smart pointers and container classes express ownership explicitly and handle cleanup automatically.

auto p = std::make_unique<int>(42);  // dynamic storage with automatic cleanup
⚠️ Prefer containers or smart pointers over raw new. They reduce leaks, simplify reasoning, and handle exceptions safely.

Object lifetime and initialization order

The lifetime of an object begins when its initialization completes and ends when its destructor finishes. For automatic objects this follows block structure. For static objects this follows the rules of their translation units. Understanding initialization order prevents bugs that arise before main even starts running.

Tracking lifetime

Automatic objects follow lexical scope, so their start and end points are easy to track. Temporaries live until the end of the full expression in which they appear. References extend the lifetime of some temporaries, especially through const&. Smart pointers extend lifetime intentionally by controlling ownership.

const std::string& r = std::string("temp"); // temporary lifetime extended

Handling initialization order for statics

Initialization of static objects across translation units follows no guaranteed order except that constant initialization happens first when possible. If a static object depends on another static object in a different translation unit, you may see undefined behavior. Use function-local statics or dependency injection patterns to avoid such hazards.

⚠️ Relying on initialization order across translation units invites fragile bugs that are hard to reproduce. Prefer constructing shared resources on first use inside functions where the order is explicit and thread safe.

RAII as the core model

RAII stands for Resource Acquisition Is Initialization. It tells you that an object owns a resource for exactly as long as the object lives, and that the destructor releases the resource. This pairs acquisition and cleanup in one place, which makes exception paths predictable and declutters control flow.

Using RAII for cleanup

Any resource that must be released should be wrapped in a class that uses a destructor for cleanup. This includes file handles, mutexes, memory, sockets, and temporary state changes. RAII converts error-prone try and catch blocks into simple block-local objects.

std::lock_guard<std::mutex> guard(m); // lock acquired here, released automatically
/* … critical section … */
💡 Treat RAII as the grammar of reliable C++. If something has a lifetime, give it a class that expresses ownership and cleanup directly.

Ownership vs borrowing

Ownership tells you who is responsible for cleanup. Borrowing tells you who may observe or temporarily use a resource without owning it. In C++ this distinction appears through raw objects, references, and smart pointers. Choosing the right category reduces bugs and explains your intent to readers and tools.

Expressing ownership

A unique owner should hold a std::unique_ptr. Shared ownership should use std::shared_ptr, although it must be used sparingly because it can hide cycles. An owning container or class can also express ownership by storing objects directly rather than through pointers.

auto owner = std::make_unique<Widget>();   // unique ownership
std::shared_ptr<Widget> shared = owner.get(); // borrowing through raw pointer (not owning)

Borrowing without owning

Borrowing happens when a function takes a reference or a pointer that does not own the object. Use & and const& to accept lvalues and temporary-friendly inputs. A raw pointer expresses optional borrowing, especially when nullptr is meaningful. A weak_ptr can observe a shared_ptr without increasing its reference count.

void draw(const Widget& w);  // borrowed reference
void maybe_use(Widget* w);   // borrowed pointer
⚠️ Borrowers must not outlive owners. Keep function parameters short lived and avoid storing borrowed pointers in long lived objects unless the lifetime relationship is clear.

Guidelines for lifetime management

Strong lifetime habits prevent subtle bugs that can survive for years. Most guidelines are simple once you commit to them, and they align with idiomatic C++ rather than fighting the language.

Preferring automatic storage

Start with automatic storage when possible. It places lifetime decisions in the compiler’s hands and pairs well with RAII wrappers for cleanup. This also limits the surface area where errors can creep in.

Designing for single ownership

Use single ownership whenever practical. If multiple components must share, prefer clear hierarchy rather than a mesh of shared_ptr instances. When you must share, keep cycles impossible or use weak_ptr to break them.

struct Node {
  std::shared_ptr<Node> next;
  std::weak_ptr<Node> prev;   // breaks cycle
};

Avoiding raw new and delete

Avoid raw dynamic allocation in new code. Use factories such as std::make_unique and std::make_shared or store objects directly. Containers take ownership explicitly and handle cleanup automatically, which makes code easier to reason about.

Borrowed references

Check that any borrowed reference or pointer cannot outlive its owner. When you store a pointer, document the lifetime relationship and ensure that code preserves the assumption. When in doubt, redesign the relationship to make ownership clearer.

💡 A clear ownership model shrinks the invisible shadow that lifetime bugs cast. The simpler your model, the sharper your code’s behavior.

Chapter 5: Expressions and Operators

C++ expressions are the gears that make computation move, and operators provide the teeth on those gears. This chapter tours operator categories, precedence, and associativity; the rules that decide when and how values convert; the meaning of lvalues and rvalues; and the main families of logical, bitwise, and arithmetic operators. You will also see how user defined conversions join the system, although careful design keeps them tame rather than surprising.

Operator categories and precedence

Operators fall into families such as arithmetic, logical, comparison, bitwise, member access, and assignment. Precedence tells you which operator binds more tightly when several appear in a single expression. Associativity tells you whether grouping proceeds from left to right or the other way. Knowing this prevents accidental ambiguities and keeps your expressions readable.

Major operator groups

Most operators behave predictably across categories. Arithmetic operators like + and - compute new numeric values. Comparison operators such as < and >= produce booleans. Logical operators combine truth values. Bitwise operators manipulate individual bits. Member access operators such as . and -> navigate object structure.

CategoryExamplesNotes
Arithmetic+, -, *, /, %Numeric operations with usual promotions.
Logical&&, ||, !Short circuit evaluation for && and ||.
Comparison<, >, <=, >=, ==, !=Return booleans and chain cleanly.
Bitwise&, |, ^, ~, <<, >>Operate on integer types at bit level.
Member access., ->, ::Navigate objects, pointers, and namespaces.
Assignment=, +=, -=, *=, /=, …Modify objects and return lvalues.

Precedence and grouping

Certain operators bind more tightly than others. Multiplication binds more tightly than addition. Member access binds more tightly than function calls, which bind more tightly than arithmetic. Parentheses override every rule and communicate intent clearly. Use them freely whenever code reads better with explicit grouping.

int r = a + b * c;    // multiplication first
int s = (a + b) * c;  // overridden grouping
auto p = &v[0];       // member access binds tightly
⚠️ Avoid clever expressions that rely on subtle precedence. Clarity helps both readers and compilers.

Conversions

C++ performs conversions in many expressions. Some are safe and quiet, such as integer promotions. Others can narrow or lose information. Explicit casts express your intent by telling the compiler exactly what type you want. Be clear about conversions because they shape the meaning of expressions.

Implicit conversions

Implicit conversions include arithmetic promotions, pointer adjustments, array to pointer conversions, and class defined conversions. Although implicit conversions make code flexible, they can hide precision loss. Narrowing is restricted in list initialization and should be avoided elsewhere unless you intentionally coerce values.

double d = 3;       // implicit int to double
int n = d;          // implicit double to int (truncation)
const char* p = s;  // array to pointer for string literal

Explicit casts

C++ provides static_cast, reinterpret_cast, const_cast, and in rare cases dynamic_cast. Prefer static_cast when converting between arithmetic types or related class types. Reserve reinterpret_cast for low level transformations such as bit reinterpretation. Use const_cast only to remove const from objects that were never originally const. Use dynamic_cast for checked downcasts in polymorphic hierarchies.

int n = static_cast<int>(3.14);  
auto base = static_cast<Base*>(derived_ptr); 
💡 Cast only when the target type is clear and stable. Most casts in mature C++ vanish once interfaces are shaped properly.

Value categories and move semantics

C++ expressions have value categories that describe how the resulting values behave. These categories decide whether you can bind references, whether you can move from an object, and how overload resolution selects functions. Although move semantics appear in depth later, this preview sets the stage.

Value categories

The central categories are lvalues, xvalues, and prvalues. An lvalue describes a stable location. An xvalue describes an expiring object that can be moved from. A prvalue describes a pure temporary value. C++11 merged several older forms into these three modern categories.

int n = 42;
n             // lvalue
std::move(n)  // xvalue
n + 1         // prvalue

Move readiness

Move semantics apply when an object is an xvalue or a prvalue that can be treated as an expiring object. A move constructor or move assignment operator can then steal resources rather than copy them. This speeds up code and reduces allocations. The preview here helps you spot where moves may occur once you read later chapters.

std::string make();
std::string s = make();  // move from temporary when possible
std::vector<int> v;
v.push_back(5);          // moves when resizing or reusing capacity
⚠️ Moving from an object leaves it valid but unspecified. Use it only in ways that do not assume old contents remain present.

Operator types

C++ offers rich operators for boolean logic, bit-level manipulation, and number crunching. Practice with these operators builds fluency and helps you avoid common pitfalls such as precedence confusion or sign extension during shifts.

Logical operators

Logical operators apply to boolean expressions. && and || perform short circuit evaluation, so the right-hand side executes only when needed. This style enables safe checks before dereferencing pointers or performing expensive computations.

if (ptr && *ptr > 0) { /* … */ }

Bitwise operators

Bitwise operators let you flip bits, mask values, or combine flags. They work on integer types and require attention to signedness because right shifts on signed values are implementation defined when negative. Use << and >> for shifts, & for masks, | for unions, and ^ for toggling bits.

unsigned mask = 0b1111;
unsigned value = 0b1010;
auto masked = value & mask;     // keep low four bits
auto flipped = value ^ mask;    // toggle low four bits

Arithmetic operators

Arithmetic operators follow familiar rules from mathematics. Addition, subtraction, multiplication, division, and remainder form expressions with predictable behavior except for division by zero or overflow. Unsigned overflow wraps, while signed overflow is undefined. Choose appropriate types for your domain.

auto sum = a + b;
auto diff = a - b;
auto prod = a * b;
auto quot = a / b;
auto rem = a % b;
💡 When shifting or masking, use unsigned types because their behavior is defined for all bit patterns.

User defined conversions

User defined conversions extend the conversion rules with constructors and conversion operators. Conversion constructors let objects form from other types. Conversion operators let objects turn into other types. Use both mechanisms with restraint because implicit conversions can create surprising paths during overload resolution.

Conversion constructors

A non explicit constructor with one parameter may act as an implicit conversion path. Consider marking such constructors explicit unless you truly want silent conversions. This keeps expressions stable and ensures that readers understand when a conversion is happening.

struct Meter {
  explicit Meter(double m) : value(m) {}
  double value;
};

Conversion operators

A conversion operator named operator T() allows objects to convert to another type. Keep such operators explicit in meaning and avoid creating broad or ambiguous paths. Modern code often prefers named functions rather than implicit conversions.

struct Angle {
  double radians;
  explicit operator double() const { return radians; }
};
⚠️ Avoid pairs of user defined conversions that form cycles because they may create ambiguous overloads or surprising implicit behavior.

Chapter 6: Control Flow

Control flow is how a program decides what to do next. C++ provides selection with if and switch, repetition with while, do while, and for, modern iteration with range for, and features such as initialization statements, structured bindings, and short circuiting. This chapter explores the core building blocks and the practical choices that help you write readable code that behaves correctly.

if, switch, while, do while, and for

These five constructs are the everyday tools for branching and looping. Choosing which one to use depends on whether you need a simple conditional, a multiway selection, an entry controlled loop, an exit controlled loop, or a counted loop. Start with the form that communicates intent most clearly, then refine for correctness and performance.

Using if for binary choices

Use if when you have a true or false condition and at most a small number of alternatives. Keep the condition simple; extract complex checks to well named functions for clarity.

int score = get_score();
if (score >= 50) std::cout << "Pass\n";
else std::cout << "Retry\n";
💡 Prefer braces even for single statements in real projects; this prevents accidental errors when adding lines later.

Using switch for multiway selection

switch selects among discrete integral or enumeration values. With modern compilers it often generates a jump table or similar efficient code. Remember that cases fall through unless you terminate with break or another jump.

enum class token { add, sub, mul, div, other };
token t = next_token();
switch (t) {
  case token::add: std::cout << "+"; break;
  case token::sub: std::cout << "-"; break;
  case token::mul: std::cout << "*"; break;
  case token::div: std::cout << "/"; break;
  default: std::cout << "?"; break;
}
⚠️ Only integral types, scoped or unscoped enums, and a few related categories are valid for switch. Strings and floating point values are not permitted.

Entry controlled loops with while

while checks the condition before each iteration. Use it when the number of iterations is not known in advance and the loop may not run at all.

std::string line;
while (std::getline(std::cin, line)) {
  if (line.empty()) break;
  process(line);
}

Exit controlled loops with do while

do while guarantees at least one iteration, because the condition is checked after the body. It suits menu loops and read until patterns where a first action must occur.

int choice;
do {
  show_menu();
  choice = read_choice();
  handle(choice);
} while (choice != 0);

Counted loops with for

for is ideal for a known range of integers or for iterators with a clear start, condition, and step. Keep all loop control in the header so the body stays focused on work.

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

Choosing among control structures

Pick the construct that communicates intent: if for simple branching; switch for many discrete options; while when zero iterations are possible; do while when at least one iteration is required; classic for for counted or iterator based loops.

GoalBest fitNotes
Binary conditionifShort and direct
Many discrete casesswitchEnumerations read well
Unknown count, may be zerowhileCondition first
Unknown count, at least onedo whileCondition last
Known range or iterator walkforAll control in header

Range for and structured bindings

The range for loops over elements of a container or range by calling begin() and end(). Combine it with structured bindings to unpack tuple like elements, map entries, or custom types that expose get or tie support.

Looping by value, reference, and const reference

Choose how elements are viewed. By value copies each element; by reference allows modification; by const reference avoids copies and prevents edits. Prefer const reference for large objects when you only read values.

std::vector<std::string> names = {"Ada","Bjarne","Grace"};
for (const std::string& s : names) std::cout << s << "\n";
for (std::string& s : names) s += "!";
for (auto s : names) use_copy(s);

Structured bindings with pairs and maps

Structured bindings split an aggregate into named variables. This is valuable with associative containers where each element is a std::pair<const Key, T>. Use descriptive names that clarify intent.

std::map<std::string, int> freq = {{ "red", 3 }, { "blue", 5 }};
for (auto& [color, count] : freq) {
  std::cout << color << ": " << count << "\n";
}

Unpacking tuples and custom types

Types that support tuple like access can be unpacked. For custom structs, add get functions and specialize std::tuple_size and std::tuple_element when appropriate, or simply rely on public data members.

struct point { double x; double y; };
std::vector<point> pts = {{1.0,2.0},{3.0,4.0}};
for (auto [x, y] : pts) {
  std::cout << "(" << x << "," << y << ")\n";
}
💡 Use auto& in structured bindings when you intend to modify elements inside a container.

Initialization statements in if and switch

An initialization statement allows you to declare and initialize a variable that is scoped to the condition and the related body. This reduces scope creep and keeps temporary values near their use.

Initializing inside if

Use this to bind the result of a lookup or parse operation, then test it. The variable remains visible in the if body and the corresponding else body, but not after the statement ends.

if (auto it = dict.find(key); it != dict.end()) {
  use(it->second);
} else {
  log_missing(key);
}

Initializing inside switch

This form suits computations that feed a switch. The initialized variable is available to all case labels inside the statement and remains hidden from surrounding code.

switch (int r = std::clamp(value, 0, 100); r / 10) {
  case 10: case 9: std::cout << "A\n"; break;
  case 8: std::cout << "B\n"; break;
  case 7: std::cout << "C\n"; break;
  default: std::cout << "D or lower\n"; break;
}
⚠️ Avoid heavy work inside the initialization if not required. Keep it cheap and obvious; move complex logic to a helper function that returns the needed value.

goto, labels, and when not to use them

goto jumps to a labeled statement within the same function. It can exit multiple nested blocks quickly, yet it harms readability and makes resource handling fragile. Prefer structured control flow and RAII; reserve goto for rare, tightly controlled cases such as breaking out of deeply nested error checks in low level code where other refactors are not possible.

Why goto is risky

Jumps bypass constructors and destructors in the skipped statements, which makes reasoning about object lifetime harder. Although destructors for fully constructed automatic objects still run when leaving scope, scattered labels and jumps often lead to subtle bugs and tangled logic.

Alternatives that read better

Replace goto with early returns, break or continue, function extraction, or a boolean flag that signals completion. These options keep the flow local and clearer.

for (auto& row : grid) {
  bool found = false;
  for (auto& cell : row) {
    if (cell == target) { found = true; break; }
  }
  if (found) break;
}
💡 If you feel tempted to add a label, consider extracting the loop body into a helper function that returns on success or failure. Clear names beat jumps.

Short circuiting and lazy evaluation

Logical && and || operators evaluate from left to right and stop as soon as the result is known. This behavior is called short circuiting. It allows you to guard operations that must only run when earlier checks pass, and it avoids unnecessary work.

Guarding with &&

With &&, the right operand evaluates only when the left operand is true. Use this to perform a follow up action that depends on the first condition.

ptr && ptr->do_work();

In this example, do_work() is called only when ptr is non null. This reads compactly and avoids an extra if block when the action is a single expression.

Fallbacks with ||

With ||, the right operand evaluates only when the left operand is false. This is handy for defaulting and for simple validation with a single failure handler.

valid(input) || throw std::runtime_error("invalid input");

Beware of hidden work in conditions

Keep conditions free of surprising side effects. A function that both checks and mutates state can make short circuiting hard to reason about. Prefer pure checks in the left operand and simple actions in the right operand.

Lazy views and generators

Short circuiting composes nicely with lazy ranges. When you chain filters and transforms, evaluation proceeds only as far as required to produce the next element. This can cut large problems down to just in time work.

#include <ranges>
#include <vector>
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
auto evens = v | std::views::filter([](int x){ return x % 2 == 0; })
               | std::views::transform([](int x){ return x * x; });
for (int x : evens) {
  if (x > 10) break;
  std::cout << x << " ";
}

Only the elements required before the break are computed. This is both efficient and expressive when paired with clear predicates.

Chapter 7: Functions and Callable Things

Functions in C++ are the gateways to behavior. They accept parameters, return values, and may carry defaults. You can overload them, ask the compiler to evaluate them at compile time, and bundle behavior in objects and lambdas. This chapter defines the building blocks, the rules that guide overload resolution, and the techniques for efficient argument passing and perfect forwarding.

Parameters, return types, and default arguments

Every function has a signature: a name, parameter types, and a return type. Defaults allow callers to omit trailing arguments. Prefer clear parameter types and names; keep default arguments stable once published, because they participate at the call site during overload resolution.

Declaring parameters and return types

Use references for large objects you do not want to copy. Use const where mutation is not intended. Return by value for small or move friendly types. Trailing return types can improve readability with complex types.

int add(int a, int b);

std::string join(const std::string& a, const std::string& b);

auto make_pair_sum(int a, int b) -> std::pair<int,int> {
  return {a + b, a - b};
}

Default arguments

Default arguments fill in when the caller omits trailing values. Defaults are looked up at the call site. Avoid setting defaults in multiple declarations; place them in a single header declaration and keep the definition free of defaults.

void log(const std::string& msg, int level = 1);

void use() {
  log("hello");    // level = 1
  log("warn", 2);  // explicit
}
⚠️ Defaults are bound at the call site; changing a default requires recompilation of all callers to see the new value.

Return value optimization and noexcept

Modern compilers elide copies in common cases. Mark functions noexcept when they cannot throw; this enables stronger optimizations and cleaner code when composing operations.

std::string greet() noexcept {
  return "hi";
}

Overloading and overload resolution

C++ allows multiple functions with the same name as long as their parameter types differ. The compiler selects the best match by ranking viable candidates. Write overload sets that are unambiguous and easy to predict.

Creating a coherent overload set

Keep behavior consistent across overloads. Avoid surprising implicit conversions. Prefer distinct parameter types or tag types to separate meanings that would otherwise collide.

void draw(int radius);
void draw(double radius);
void draw(std::string_view name);

How resolution ranks candidates

Resolution prefers exact matches, then promotions, then standard conversions, then user defined conversions. Templates add another layer: partial ordering among templates decides which template is more specialized.

RankExamplesComment
Exact matchint to intNo conversion
Promotionchar to intIntegral promotions
Standard conv.int to doubleWidening conversions
User definedFoo to intoperator int()
💡 When overloads still conflict, introduce a strongly typed wrapper or use distinct factory names that spell intent.

SFINAE and constraints preview

SFINAE (Substitution Failure Is Not An Error) is the rule that lets templates quietly step aside when a substitution fails. The compiler tries to form a valid function signature by plugging in template arguments; if that attempt produces an invalid type or expression inside the immediate context, the candidate is simply removed from the overload set rather than causing an error. This graceful collapse lets you write families of templates that become available only when their requirements make sense, which creates flexible and predictable interfaces.

Templates can be conditionally viable using traits or concepts. With constraints, overloads become readable, and resolution becomes simpler to reason about.

template<typename T>
requires std::integral<T>
T sum(T a, T b) { return a + b; }

Using inline, constexpr, and consteval

These specifiers guide linkage and evaluation time. inline affects the one definition rule and allows multiple definitions across translation units with identical bodies. constexpr enables compile time evaluation when arguments are constant expressions. consteval requires compile time evaluation.

Using inline

Use inline on functions defined in headers to allow a single logical definition across all translation units. It does not force inlining by the optimizer; it controls linkage and the one definition rule.

inline int square(int x) { return x * x; }

Using constexpr

constexpr functions may run at compile time or runtime. Write them with a body that also makes sense at runtime. Avoid heavy dynamic work; rely on pure calculations and simple data.

constexpr int fib(int n) {
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
static_assert(fib(5) == 5);

Using consteval

consteval enforces compile time evaluation. Use it for utilities that must produce constant values such as fixed lookups or validated constants.

consteval int id() { return 42; }
constexpr int k = id();  // ok, must be constant
⚠️ A consteval function cannot be called at runtime; every call must appear in a constant evaluation context.

Lambdas and captures

Lambdas are compact function objects. They can capture variables from the surrounding scope by value or by reference. Use clear capture lists to show intent and to manage lifetime safely.

Basic lambda syntax

The form is [captures](params) specifiers { body }. Default captures exist for convenience, yet an explicit list improves clarity and prevents accidental references.

int factor = 3;
auto times = [factor](int x) { return x * factor; };
std::cout << times(7);

Capturing by value vs reference

By value copies the variable at creation time. By reference binds to the original variable and requires that the captured object outlives the lambda. Prefer value for safety unless mutation of the original is required.

int n = 0;
auto by_val = [n]() { return n; };
auto by_ref = [&n]() { ++n; return n; };

Mutable lambdas and auto params

A mutable lambda allows modification of its captured-by-value state. With auto in parameters, a lambda becomes a generic callable that deduces argument types.

auto counter = [n = 0]() mutable { return ++n; };
auto apply = [](auto f, auto x) { return f(x); };
💡 Use an init capture to move expensive objects into a lambda: [ptr = std::move(p)]{…}. This avoids shared state and clarifies ownership.

std::function, function pointers, and callable objects

C++ treats many things as callable: free functions, member functions, lambdas, and objects with operator(). std::function is a type erased wrapper that can hold any callable with a given signature.

Function pointers

Function pointers store the address of a free function or a static member function. They are light and fast, yet less flexible than std::function or templates.

int twice(int x) { return 2 * x; }
int (*fp)(int) = &twice;
std::cout << fp(21);

std::function as a general holder

std::function<R(Args…)> can store lambdas, function pointers, and function objects. It allocates when the target does not fit small buffer optimization, so use it for type erasure at interfaces and prefer templates for inner loops.

#include <functional>
std::function<int(int)> f = [](int x){ return x + 1; };
f = twice;
std::cout << f(10);

Callable objects with operator()

Overloading operator() creates a function object with state. This pattern pairs data and behavior, and it composes well with algorithms.

struct scaler {
  double k;
  int operator()(int x) const { return int(k * x); }
};
scaler s{2.5};
std::cout << s(4);
⚠️ When storing capturing lambdas or large callables in std::function, be aware of allocations; pass heavy callables by reference when possible.

Argument passing and perfect forwarding

Choose parameter forms that match usage: pass small types by value, large read-only types by const&, and move-only or sink parameters by rvalue reference. Templates can forward arguments perfectly so that value categories are preserved.

Value, reference, and rvalue reference

Pick the simplest approach that avoids unnecessary copies. Value works well for small types. const& avoids copies for read-only access. Rvalue references accept temporaries that can be moved from.

void take_by_value(std::string s);
void take_by_cref(const std::string& s);
void take_by_rref(std::string&& s);

Forwarding references

A parameter of the form T&& in a deduced context becomes a forwarding reference. Use std::forward<T> to preserve lvalue or rvalue nature when passing along.

template<typename T, typename F>
void call(F&& f, T&& x) {
  std::forward<F>(f)(std::forward<T>(x));
}

Perfect forwarding in factories

Constructors often need to accept many argument shapes. Forwarding lets a factory pass them straight through to the constructed object with minimal overhead.

template<typename T, typename... Args>
std::unique_ptr<T> make_u(Args&&... args) {
  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
💡 Prefer std::make_unique and std::make_shared in real code; they are exception safe and concise.

Chapter 8: Pointers, References, and Arrays

Pointers, references, and arrays form the low level backbone of C++. They let you work with memory directly, bind names to objects, and manage sequences of elements. Although modern C++ offers safer abstractions, understanding these primitives explains how data moves and how storage behaves under the hood.

Raw pointers and pointer arithmetic

A raw pointer holds the address of an object. You can dereference it to access that object, and you can move it through memory using arithmetic. This style of traversal is common in low level code where direct control is required. Keep pointer lifetimes and ownership clear, because raw pointers do not express who manages the underlying object.

Declaring and using pointers

You create a pointer by storing the address of an existing object. Dereferencing accesses the pointed value. Always check validity before use.

int x = 7;
int* p = &x;
std::cout << *p;

Pointer arithmetic on contiguous sequences

Incrementing or decrementing a pointer moves it by whole elements, not bytes. Arithmetic is only defined within arrays or one past the end. Anywhere else leads to undefined behavior.

int arr[3] = {1,2,3};
int* q = arr;
++q;  // now points to arr[1]
std::cout << *q;
⚠️ Never perform arithmetic on a pointer unless you are sure the target is part of a valid contiguous block of elements.

References: lvalue and rvalue

A reference binds a name directly to an object. An lvalue reference binds to a persistent object, while an rvalue reference binds to a temporary or a movable resource. Use references to avoid unnecessary copies and to clarify ownership expectations.

Lvalue references

An lvalue reference must bind to an existing object with a stable address. It behaves like an alias and cannot be reseated. Use it when a function needs to read or modify the caller's data.

void inc(int& n) { ++n; }
int v = 10;
inc(v);

Rvalue references

An rvalue reference binds to temporaries and expresses move semantics. It allows efficient transfer of resources by stealing state from objects that will not be used again.

void take(std::string&& s);
take(std::string("hi"));
💡 Use std::move to convert an lvalue to an rvalue reference when you intend to move from it.

C style arrays and decay rules

C style arrays are fixed size blocks of contiguous elements. Their size is known at compile time when declared with a constant bound. However, they decay to pointers in many contexts, which hides size information and complicates interfaces.

Declaring arrays

An array groups elements of the same type. Access uses the familiar bracket form, and traversal uses indices or pointers.

int a[4] = {1,2,3,4};
std::cout << a[2];

Array to pointer decay

When passed to a function or used in most expressions, an array converts to a pointer to its first element. This means the size is lost unless passed separately, which encourages mistakes if not tracked carefully.

void print_all(const int* p, std::size_t n);
int b[3] = {4,5,6};
print_all(b, 3);  // b decays to int*
⚠️ Because decay hides array bounds, prefer safer alternatives such as std::array or std::span when you need size aware sequences.

std::array and std::span

std::array is a fixed size container that preserves size information and behaves like an STL type with iterators. std::span is a lightweight view that refers to an existing range without owning it. These forms let you keep C style layout when necessary while avoiding the pitfalls of raw arrays.

std::array as a safer fixed block

std::array<T,N> stores elements inline just like a C style array, but its size is part of the type. It supports standard algorithms and communicates its bounds clearly.

#include <array>
std::array<int,3> a = {7,8,9};
for (int x : a) std::cout << x;

std::span as a non owning view

std::span wraps a pointer and a size into a single object. It does not allocate or own memory; it simply views a range. This turns old pointer plus length conventions into a clean and self describing type.

#include <span>
void show(std::span<int> s) {
  for (int x : s) std::cout << x;
}
int c[4] = {1,2,3,4};
show(c);
💡 Use std::span to pass ranges without forcing callers to wrap them; it adapts to arrays, vectors, and many other containers.

Nullability and not null guidelines

A raw pointer can be null, which introduces the risk of dereferencing an invalid address. Clarify intent by using non null contracts, references, smart pointers, or helper types that express whether null is allowed. When null is meaningful, check for it early and fail clearly.

Checking for null before use

Always verify that a pointer is valid before dereferencing. Prefer early returns or guard expressions that prevent deeper logic from running with an invalid pointer.

void use(const widget* p) {
  if (!p) return;
  p->run();
}

Expressing not null intent

When a function requires a valid object, prefer a reference or a smart pointer that guarantees presence. Custom wrappers or third party not_null types can also encode the rule that null is forbidden.

void operate(widget& w);                 // cannot be null
void manage(std::shared_ptr<widget> p);  // may be null
⚠️ Avoid silent null handling in deep code paths. Make nullability explicit so callers know what guarantees they must provide.

Chapter 9: Text, Unicode, and Formatting

This chapter tours modern C++ text handling. You will meet std::string and std::string_view, learn what Unicode means for your code, format values with std::format, respect users through locale-aware choices, and parse text without surprises. The goal is simple; write programs that handle names, numbers, and messages correctly for everyone.

std::string and std::string_view

std::string owns a mutable byte buffer; it allocates and stores text. std::string_view is a non-owning window over a character sequence (think borrowing rather than owning). Use a view when you only need to read, and a string when you must store or modify. Views are cheap to copy; strings are cheap to move.

Choosing which to use

Prefer std::string_view for parameters when the callee will not keep the data. Prefer std::string for return values when the caller needs an owning result. This keeps lifetimes clear and avoids accidental dangling references.

Common operations and pitfalls

Substrings from std::string copy by default; substrings as std::string_view do not copy. A view can dangle if the referenced buffer goes out of scope. Converting a view to a string materializes a new buffer; this is often the right trade when you need to store or modify.

#include <string>
#include <string_view>

void greet(std::string_view who) {
  // uses the caller's storage; no allocation
  // safe only while 'who' remains valid
}

std::string shout(std::string_view s) {
  std::string owned(s); 
  for (char& c : owned) c = static_cast(std::toupper(static_cast(c)));
  return owned; 
}
💡 Accept std::string_view in APIs; store as std::string when you need to keep a copy. This pattern balances performance and safety.

Small string optimization and performance notes

Most standard library implementations keep short strings inside the object; this avoids heap traffic for tiny texts. Do not depend on the exact threshold. Treat it as an implementation detail that often helps but might vary.

Character types and Unicode basics

Text is bytes with meaning. C++ offers multiple character types to represent code units. Unicode assigns each character a code point; encodings such as UTF-8 and UTF-16 map code points to bytes. Always know which encoding your program reads and writes.

Character and string types at a glance

The following table summarizes the main options. Code unit size and encoding expectations differ; choose the pair that matches your I/O needs.

TypeCode unitTypical containerNotes
char1 bytestd::stringBytes; often UTF-8 by convention; not guaranteed
char8_t1 bytestd::u8stringIntended for UTF-8 code units
char16_t2 bytesstd::u16stringUTF-16 code units; surrogate pairs for some code points
char32_t4 bytesstd::u32stringUTF-32 code units; one unit per code point
wchar_t2 or 4 bytesstd::wstringImplementation defined; avoid for portable Unicode

UTF-8 as a practical default

UTF-8 is compact for ASCII; it is widely supported across platforms and protocols. Store text as UTF-8 where possible. When you must interoperate with system APIs that use UTF-16 or UTF-32, convert at the boundaries to keep the core simple.

Graphemes, code points, and user expectations

What users see as one character might be multiple code points (for example an emoji plus a skin tone modifier). Simple indexing by code unit can split a visible character. If you need user-visible operations, use a Unicode-aware library that understands grapheme clusters.

⚠️ The standard library does not provide full Unicode algorithms such as normalization or grapheme segmentation. Use a library such as ICU for advanced tasks.

Formatting output

std::format prints values with Python-style format strings. Place fields in braces like {…}; add alignment, width, precision, and type specifiers inside the braces. The function is safe and convenient; it avoids iostream quirks and printf hazards.

The format mini-language

A field looks like {:<10.2f} for left alignment, width 10, precision 2, floating style. You can name arguments or use automatic indexing. The same rules apply to std::format_to when writing into existing buffers.

#include <format>
#include <string>

std::string s = std::format("Hello {}, pi≈{:.3f}", "Ada", 3.14159);
// "Hello Ada, pi≈3.142"

std::string t = std::format("|{:>8}|{:^8}|{:<8}|", "R", "G", "B");
// right, centered, left

Custom formatting

Provide a specialization of std::formatter<T> with a parse function for options and a format function to render. This lets your types work naturally inside format strings alongside standard types.

#include <format>

struct point { int x; int y; };

template<>
struct std::formatter<point> {
  constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
  auto format(const point& p, std::format_context& ctx) const {
    return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
  }
};

std::string out = std::format("p={}", point{3,4});

Locale and chrono support

std::format can format time points using {:%Y-%m-%d %H:%M:%S}. By default formatting is locale independent for punctuation; some implementations and newer standards offer overloads that take a std::locale for locale-aware results. Check your toolchain and use explicit locale overloads when you need cultural formatting.

💡 Prefer std::format over printf for type safety; prefer it over stream manipulators for clarity and composability.

Locales and cultural correctness

Locale controls language, collation, number punctuation, and date rules. Hard-coding separators and names can confuse users. Respect the active locale when presenting user-facing text and when parsing user input that follows local rules.

Applying std::locale to streams

Imbue a stream with a locale to affect extraction and insertion. This can change decimal separators, thousands separators, and day or month names in formatted output.

#include <locale>
#include <iostream>

int main() {
  std::locale loc("fr_FR.UTF-8"); 
  std::cout.imbue(loc);
  std::cout << 1234567.89 << "\n"; 
}

Sorting and case rules differ by language

String comparison by raw code units is not user ordering. Collation depends on language rules; case folding is language sensitive. For human sorts and searches, use a library that implements locale collation rather than naive comparisons.

⚠️ Never assume ASCII rules fit all languages. Even uppercase and lowercase mappings vary; for example Turkish dotted and dotless I characters break simple assumptions.

Use boundary conversions instead of global settings

Keep the core of your program in a stable internal form (often UTF-8 and C locale). Convert and format at the boundaries where you talk to users or systems. This reduces surprises while still delivering culturally correct I/O at the edges.

Parsing text safely

Parsing turns text into typed values. Prefer functions that report errors without throwing when speed or control matters. Avoid ad-hoc parsing with unchecked indexing; use views and robust helpers to prevent overreads and partial conversions.

Numbers with std::from_chars

std::from_chars parses integers and floating values from a byte range without allocations. It reports errors via a return code and a pointer to the first unparsed character; this makes it a good fit for tight loops and streaming.

#include <charconv>
#include <string_view>

int parse_port(std::string_view sv) {
  int port = 0;
  auto* begin = sv.data();
  auto* end = sv.data() + sv.size();
  auto res = std::from_chars(begin, end, port);
  if (res.ec != std::errc() || res.ptr != end) return -1;
  return port;
}

Lines and fields with views and ranges

Use std::string_view to slice without copying and use algorithms to scan. For simple CSV-like data where values do not contain quotes or escapes, a careful split by delimiter can be enough; for full CSV rules rely on a proper parser.

#include <string_view>
#include <vector>

std::vector<std::string_view> split(std::string_view s, char delim) {
  std::vector<std::string_view> out;
  size_t pos = 0;
  while (pos < s.size()) {
    size_t next = s.find(delim, pos);
    if (next == std::string_view::npos) { out.push_back(s.substr(pos)); break; }
    out.push_back(s.substr(pos, next - pos));
    pos = next + 1;
  }
  return out;
}

Timestamps and structured input

For date and time, prefer the chrono parsing facilities that mirror strftime patterns. If your standard library lacks a direct parsing function, use a proven library that supports the formats you must accept, or parse with a finite set of checks and clear error paths.

💡 Validate first, then convert. Check length limits, allowed characters, and expected separators before calling a conversion routine; fail fast on the first mismatch.

Error reporting with context

Include position information and a short excerpt to help users fix input. Keep messages neutral and specific; show the unexpected token and the rule that failed. This approach speeds up debugging for both programs and people.

#include <stdexcept>
#include <string>

[[noreturn]] void parse_error(std::string_view where, size_t pos, std::string_view why) {
  std::string caret(pos, ' ');
  throw std::runtime_error(std::string(where) + "\n" + caret + "^\n" + std::string(why));
}

Chapter 10: Classes and Object Semantics

This chapter develops a practical model for C++ classes. You will learn how objects are laid out, how access control works, when to use friends, how special member functions define the rule of zero, five, and six, and how to design constructors and destructors that preserve invariants. You will also practice copy, move, and assignment, before building robust RAII wrappers for resources such as files and mutexes.

Class layout, access control, and friends

A class bundles state and behavior. Within the object, data members appear in declaration order with padding for alignment. Access control guards the interface; use public for the stable surface, private for internal details, and protected for inheritance cases where derived classes need a hook. A friend grants a specific function or class access to private members, which is a narrow tool for tightly coupled helpers.

Object representation basics

Standard layout types have predictable member order and address properties. Most everyday classes do not need to be standard layout; prefer clarity and rely on the language to manage padding. When binary wire formats are required, use explicit packing only at the boundary and keep the core in normal classes for safety.

struct rgba {
  std::uint8_t r, g, b, a;  // declaration order is the storage order
};
static_assert(std::is_standard_layout_v<rgba>);

Access specifiers in practice

Start with everything private and then promote stable operations to public. Avoid protected data; prefer protected virtual functions or private data with public non-virtual functions that call private helpers. This keeps invariants centralized and testable.

class counter {
  long value_ = 0; // private state
public:
  void increment() noexcept { ++value_; }
  long get() const noexcept { return value_; }
};

When a friend makes sense

Use friendship for symmetric operators that must inspect internals or for factory helpers that need to build valid states. Prefer free functions that use the public interface when possible; friendship is a targeted escape hatch, not a default.

class box {
  int w_, h_;
public:
  box(int w, int h): w_(w), h_(h) {}
  friend bool operator==(const box& a, const box& b) { return a.w_ == b.w_ && a.h_ == b.h_; }
};
⚠️ A friend declaration grants access but does not couple linkage or lifetime. Keep the set of friends minimal to reduce accidental tight coupling.

The rules of zero, five, and six

Special member functions manage ownership and lifetime. When a type does not own a resource, the compiler generated defaults usually do the right thing; this is the rule of zero. When a type owns a resource, you may need to provide specific operations; historically this was the rule of five, and with =default and =delete plus the swap pattern, many designs compress to fewer custom members in practice.

The rule of zero

If your class only stores values that already manage themselves (for example std::string, std::vector), do not write any special members. Let the compiler synthesize copy, move, destructor, and default constructor.

struct person {
  std::string name;
  std::vector<std::string> tags;
  // no custom special members; rule of zero
};

The classic rule of five

When manual ownership is involved, consider defining destructor, copy constructor, copy assignment, move constructor, and move assignment. Mark the ones you do not want as =delete to make intent explicit.

class file_handle {
  FILE* f_ = nullptr;
public:
  file_handle() = default;
  explicit file_handle(const char* path, const char* mode) : f_(std::fopen(path, mode)) {}
  ~file_handle() { if (f_) std::fclose(f_); }

  file_handle(const file_handle&) = delete;
  file_handle& operator=(const file_handle&) = delete;

  file_handle(file_handle&& other) noexcept : f_(std::exchange(other.f_, nullptr)) {}
  file_handle& operator=(file_handle&& other) noexcept {
    if (this != &other) {
      if (f_) std::fclose(f_);
      f_ = std::exchange(other.f_, nullptr);
    }
    return *this;
  }

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

The proposed rule of six

Some teams count the default constructor as a sixth when it carries meaningful work. Another view counts a custom swap as part of the set. Regardless of counting, the design idea remains the same; own exactly the members that represent ownership and make the rest defaults.

💡 Try zero first. If you add one special member, check whether you need the matching set to keep the class coherent; asymmetry often hints at a missing piece.

Constructors, destructors, and invariants

Constructors establish a valid state and destructors release owned resources. An invariant is a truth that holds between public calls. Keep invariants simple and make them true after construction, before and after each public function, and during destruction until the last step.

Member initialization order and safety

Members are initialized in the order of declaration, not the order in the initializer list. Declare members in dependency order and use the initializer list to construct them. Avoid complex work in destructors; release resources and keep logic minimal to prevent exceptions escaping.

class range {
  int lo_;
  int hi_;
public:
  range(int lo, int hi) : lo_(lo), hi_(hi) { if (lo_ > hi_) throw std::invalid_argument("bad range"); }
  bool contains(int x) const noexcept { return lo_ <= x && x <= hi_; }
};

Delegating and explicit constructors

Delegating constructors reduce duplication by routing common work through a single constructor. Mark single argument converting constructors as explicit when implicit conversions would surprise callers.

class url {
  std::string spec_;
public:
  url() : url("about:blank") {}
  explicit url(std::string s) : spec_(std::move(s)) {}
  const std::string& str() const noexcept { return spec_; }
};
💡 Prefer constructor bodies that only validate and assign. Shift parsing or heavy work into private helpers that can report detailed errors with context.

Copy, move, and assignment

Copying duplicates a value; moving transfers resources and leaves the source valid but unspecified within its documented post-move state. Assignment reuses an existing object. Aim for strong exception safety where possible and document the post-move guarantees.

Defaulted operations when possible

When all members have correct copy and move semantics, default your operations. This keeps behavior aligned with member types and reduces bugs.

struct image {
  std::vector<std::byte> pixels;
  int width = 0, height = 0;
  image() = default;
  image(const image&) = default;
  image(image&&) noexcept = default;
  image& operator=(const image&) = default;
  image& operator=(image&&) noexcept = default;
};

Strong exception safety with copy-and-swap

Implement copy assignment via pass by value and swap. This provides a commit or roll back effect; either the operation succeeds and the new state is in place, or nothing changes.

class buffer {
  std::unique_ptr<std::byte[]> data_;
  std::size_t size_ = 0;
public:
  buffer() = default;
  explicit buffer(std::size_t n) : data_(new std::byte[n]{}), size_(n) {}

  friend void swap(buffer& a, buffer& b) noexcept {
    using std::swap;
    swap(a.data_, b.data_);
    swap(a.size_, b.size_);
  }

  buffer(const buffer& other) : buffer(other.size_) {
    std::memcpy(data_.get(), other.data_.get(), size_);
  }
  buffer(buffer&&) noexcept = default;

  buffer& operator=(buffer other) noexcept { // by value
    swap(*this, other);
    return *this;
  }
};
⚠️ After a move, document the valid operations on the moved-from object. A common guarantee is that it remains destructible and assignable; most accessors may be called but will observe an empty state.

Customizing operator== and ordering

Define equality in terms of observable state. For ordering, consider <=> which can synthesize comparisons from member-wise rules. Keep comparison free of hidden global state so that containers and algorithms behave predictably.

struct point3 {
  int x, y, z;
  auto operator<=>(const point3&) const = default; // compares in member order
  bool operator==(const point3&) const = default;
};

RAII handles and resource wrappers

RAII ties resource lifetime to object lifetime. Acquire the resource during construction and release it in the destructor. This pattern makes code exception safe and simpler to reason about, since the compiler runs destructors automatically on scope exit.

Single ownership with std::unique_ptr

std::unique_ptr models exclusive ownership and deletes the object with a custom deleter if needed. Use std::make_unique to construct safely. Prefer unique ownership unless you truly need sharing.

struct widget { /* … */ };

std::unique_ptr<widget> make_widget() {
  return std::make_unique<widget>();
}

Shared ownership with std::shared_ptr

std::shared_ptr counts references and deletes when the last owner goes away. Cycles leak, so use std::weak_ptr to break reference loops in graphs. Consider whether shared ownership reflects the real design or if a caller should own and pass views instead.

struct node {
  std::shared_ptr<node> next;
  std::weak_ptr<node> prev; // breaks cycles
};

Writing a minimal RAII wrapper

Model a handle that owns a resource with clear move semantics and disabled copy. Expose a small interface that represents the capability you want to give to callers.

class fd {
  int n_ = -1;
public:
  fd() = default;
  explicit fd(int n) : n_(n) {}
  ~fd() { if (n_ >= 0) ::close(n_); }

  fd(const fd&) = delete;
  fd& operator=(const fd&) = delete;

  fd(fd&& other) noexcept : n_(std::exchange(other.n_, -1)) {}
  fd& operator=(fd&& other) noexcept {
    if (this != &other) {
      if (n_ >= 0) ::close(n_);
      n_ = std::exchange(other.n_, -1);
    }
    return *this;
  }

  int get() const noexcept { return n_; }
  explicit operator bool() const noexcept { return n_ >= 0; }
};
💡 Keep the wrapper tiny; constructor acquires, destructor releases, copy disabled or well defined, move enabled, and a small set of queries that reveal state without breaking invariants.

Chapter 11: Operator Overloading

Operator overloading lets your types behave like built-in values with familiar syntax. Used carefully it improves clarity; used carelessly it creates puzzles. This chapter shows which operators make sense to overload, how to choose between member and free function forms, how to use the comparison and spaceship operators, how to integrate with streams, and how strong types avoid accidental conversions.

Which operators to overload

Overload only when the meaning is obvious to readers. Arithmetic types benefit from +, -, *, and /. Containers often define == for equality. Smart pointers define * and -> to act as transparent handles. Avoid surprising uses; do not redefine + to do unrelated work, and do not overload operators that would hide important preconditions.

Consistency with built-in expectations

If you implement an operator, match the usual algebraic or logical expectations. Addition should not modify its operands. Equality should be reflexive and symmetric. Ordering should be stable and transitive. These expectations anchor readability and make generic algorithms behave without surprises.

struct vec2 {
  float x, y;
  vec2 operator+(const vec2& b) const noexcept { return {x + b.x, y + b.y}; }
  bool operator==(const vec2& b) const noexcept { return x == b.x && y == b.y; }
};
💡 If a user must check documentation to guess an operator’s intent, a named function is often clearer.

Operators that rarely make sense

Bitwise operators on classes seldom communicate meaning unless your type truly models bit patterns. Likewise overloading logical operators such as && and || does not give short-circuiting and creates hard-to-read code. Prefer explicit functions for these behaviors.

Member vs free function operators

Many operators can be written as either members or free functions. Members receive the left operand as *this. Free functions take both operands as parameters. For symmetric operations such as == and +, free functions are often more flexible because they allow conversions on both sides.

When to choose a member operator

Use a member when the operator conceptually belongs to the left operand or when only the left operand should convert. Assignment operators must be members, since the left operand is always a modifiable object.

class counter {
  int n_ = 0;
public:
  counter& operator+=(int x) noexcept { n_ += x; return *this; }
};

When to choose a free function

A free function helps with symmetric operations, since either operand may convert. It also decouples the operator from the class, which improves encapsulation. You can declare the free function as a friend when it needs internal access.

class point {
  int x_, y_;
public:
  point(int x, int y) : x_(x), y_(y) {}
  friend point operator+(const point& a, const point& b) {
    return {a.x_ + b.x_, a.y_ + b.y_};
  }
};
⚠️ Prefer free functions for commutative or symmetric operations. This avoids subtle conversion restrictions that occur when the operator is a member.

Comparison and spaceship operator

C++ lets you define equality and ordering in a compact way. The spaceship operator <=> synthesizes all six relational operators when possible. It also helps generic code by making comparison semantics explicit and uniform.

Defaulted comparisons

Let the compiler generate comparisons when member-wise comparison describes your type. This keeps code short and consistent. You may default both operator== and <=> if all members are comparable.

struct box {
  int w, h, d;
  auto operator<=>(const box&) const = default;
  bool operator==(const box&) const = default;
};

Custom comparison rules

Some types need comparisons that reflect semantic meaning rather than raw members. For example a duration type may compare seconds after normalizing internal fields. In these cases write the comparison manually and leave ordering well defined for all valid objects.

class duration {
  long seconds_;
public:
  explicit duration(long s) : seconds_(s) {}
  long count() const noexcept { return seconds_; }
  auto operator<=>(const duration& other) const noexcept {
    return seconds_ <=> other.seconds_;
  }
  bool operator==(const duration& other) const noexcept {
    return seconds_ == other.seconds_;
  }
};
💡 Keep comparison functions pure; avoid any hidden state or I/O inside them so algorithms behave predictably.

Stream insertion and extraction

Stream operators integrate your type with std::ostream and std::istream. Insertion prints readable text. Extraction parses values while propagating stream state. These operators must be free functions and often need friendship for private state.

Implementing operator<<

Write a clear textual form that helps debugging. Keep formatting simple and predictable, since users may wrap your output inside larger structures. Return the stream to support chaining.

class range {
  int lo_, hi_;
public:
  range(int lo, int hi) : lo_(lo), hi_(hi) {}
  friend std::ostream& operator<<(std::ostream& os, const range& r) {
    return os << "[" << r.lo_ << "," << r.hi_ << "]";
  }
};

Implementing operator>>

Extraction must leave the stream in a sensible state. Validate separators and fields carefully; set failbit on malformed input. Never throw exceptions for normal parse errors; streams already convey state.

friend std::istream& operator>>(std::istream& is, range& r) {
  char c1, c2, c3;
  int lo, hi;
  if (is >> c1 >> lo >> c2 >> hi >> c3 && c1 == '[' && c2 == ',' && c3 == ']') {
    r = range(lo, hi);
  } else {
    is.setstate(std::ios::failbit);
  }
  return is;
}
⚠️ Stream extraction is fragile. Parse defensively to prevent the stream entering a confused state that disrupts further reads.

Strong types and explicit conversions

Strong types wrap primitive values to prevent accidental mixing of units or meanings. They rely on explicit constructors and named operations rather than implicit conversions. This reduces subtle bugs while keeping intent clear.

Basic strong type pattern

Wrap the underlying value in a small class with an explicit constructor and well defined operations. Provide comparisons and arithmetic only when the meaning is unambiguous. Expose the stored value through a named function.

class meters {
  double v_;
public:
  explicit meters(double v) : v_(v) {}
  double value() const noexcept { return v_; }

  meters operator+(const meters& m) const noexcept { return meters(v_ + m.v_); }
  auto operator<=>(const meters&) const = default;
};

Preventing accidental conversions

Implicit conversions from raw values can hide errors. Mark single argument constructors as explicit. When converting back, require a named function such as value(). This forces the call site to carry intent rather than accident.

💡 Strong types shine in codebases with multiple numeric domains such as coordinates, speeds, and durations. Each domain becomes distinct and mixing them requires deliberate conversion.

Chapter 12: Inheritance and Polymorphism

Inheritance lets one class reuse and extend another; polymorphism lets code treat related objects through a shared interface. Used together, they enable substitution, late binding, and clean separation of interface from implementation. This chapter explains the forms of inheritance in C++, how dynamic dispatch works, where abstract classes fit, how to reason about multiple inheritance and the diamond, what object slicing is, and when composition is the better choice.

Types of inheritance

The access specifier in a base clause controls how the base’s public and protected interface is viewed from the outside through the derived type. It does not change the actual accessibility inside the derived class; it changes the relationship that clients see when they use the derived type.

The inheritance specifier

With public inheritance, a base’s public stays public and protected stays protected when reached through the derived type. With protected inheritance, both public and protected base members become protected as seen through the derived type. With private inheritance, both become private through the derived type. Inside the derived class body, the usual access rules still apply.

struct Base {
  void pub();
protected:
  void prot();
};

struct D1 : public Base { };     // Base::pub() is public via D1; Base::prot() is protected via D1
struct D2 : protected Base { };  // both are protected via D2
struct D3 : private Base { };    // both are private via D3

Use public inheritance for true “is a” relationships

Choose public inheritance when the derived type is substitutable for the base. If clients can use a Base reference and safely pass a Derived, substitution holds. protected and private inheritance express implementation reuse; they do not model “is a”; prefer composition for that kind of reuse in modern C++.

💡 A quick check for is a: every valid statement about a Base object should remain valid for a Derived object used through a Base&.

virtual functions and dynamic dispatch

A member function declared with virtual participates in dynamic dispatch. Calls made through a base pointer or reference are resolved at run time to the most derived override that matches the dynamic type of the object. This enables polymorphic behavior with a stable base interface.

Declaring, overriding, and finalizing

Mark a base function virtual and override it in derived classes. Use override on the derived declaration to request compile time checking; use final to prevent further overrides. Virtual dispatch requires at least one virtual function in the class to create a vtable on common ABIs.

struct Shape {
  virtual ~Shape() = default;
  virtual double area() const = 0;
};

struct Circle : Shape {
  double r{};
  explicit Circle(double rr) : r(rr) {}
  double area() const override { return 3.141592653589793 * r * r; }
};

double total_area(const std::vector<std::unique_ptr<Shape>>& v) {
  double sum = 0;
  for (auto& s : v) sum += s->area();  // dynamic dispatch
  return sum;
}

Call rules and qualified lookup

Virtual dispatch applies only to calls made through a base class subobject and only for functions declared virtual. Constructors do not dispatch virtually; destructors dispatch virtually when invoked through a base pointer if the base destructor is virtual. A qualified call like Base::f() suppresses virtual dispatch and calls the named function.

⚠️ Never call a virtual function from a constructor or destructor if it relies on derived state; during construction and base destruction the dynamic type is not yet the most derived and the derived part is not fully formed.

Abstract classes and interfaces

An abstract class has one or more pure virtual functions. Such a class cannot be instantiated, but it defines an interface for derived types to implement. In C++ there is no special “interface” keyword; a class with only pure virtuals, a virtual destructor, and no data members often serves the role.

Declaring pure virtual functions

Use = 0 on a virtual declaration to make it pure. Derived classes must override all inherited pure virtuals to become concrete. A pure virtual can still provide a definition that derived overrides may call.

struct Stream {
  virtual ~Stream() = default;
  virtual std::size_t read(std::byte* buf, std::size_t n) = 0;
  virtual std::size_t write(const std::byte* buf, std::size_t n) = 0;
};

struct FileStream : Stream {
  std::FILE* f{};
  explicit FileStream(const char* path, const char* mode) : f(std::fopen(path, mode)) {}
  std::size_t read(std::byte* buf, std::size_t n) override { return std::fread(buf, 1, n, f); }
  std::size_t write(const std::byte* buf, std::size_t n) override { return std::fwrite(buf, 1, n, f); }
};

Separating interface and implementation

Keep the abstract base minimal and stable; push optional behavior into free functions or nonvirtual helpers. This reduces coupling and keeps your polymorphic surface small. Prefer nonvirtual template helpers when behavior does not need run time polymorphism.

Multiple inheritance and the diamond

A class may inherit from more than one base. This can model types that must satisfy several independent interfaces. It can also create the diamond shape when two bases share a common base. Virtual base classes resolve the duplication at the cost of added complexity.

Combining independent interfaces

Use multiple inheritance safely when the bases are pure interface classes. Since such bases typically carry no data, there is no ambiguity about member layout, and the derived class supplies one concrete implementation that satisfies all required functions.

struct Drawable { virtual ~Drawable() = default; virtual void draw() const = 0; };
struct Updatable { virtual ~Updatable() = default; virtual void update(double dt) = 0; };

struct Sprite : Drawable, Updatable {
  void draw() const override { /* … */ }
  void update(double dt) override { /* … */ }
};

The diamond and virtual base classes

When two bases share a common base, the most derived class normally contains two copies of that common base. Mark the shared base as a virtual base to ensure there is exactly one shared subobject. Construction of a virtual base is the responsibility of the most derived class.

struct A { int id; };
struct B : virtual A { };
struct C : virtual A { };
struct D : B, C {
  D() { id = 42; }  // only one A subobject exists
};
💡 Keep state out of shared base classes when possible. If a base must be shared with virtual inheritance, document who constructs it and which invariants it owns.

Object slicing and non virtual pitfalls

Assigning a derived object to a base object by value slices away the derived part, leaving only the base subobject. Likewise, calling a nonvirtual function through a base reference binds statically and cannot reach derived behavior. These problems appear subtle in simple code and cause surprises at scale.

Using references or smart pointers

Store polymorphic objects via references or smart pointers rather than by value. Prefer std::unique_ptr<Base> for ownership. If sharing is required, use std::shared_ptr<Base> with care and clear lifetime rules.

struct Base { virtual ~Base() = default; virtual int kind() const = 0; };
struct Derived : Base { int kind() const override { return 2; } };

Derived d;
Base b = d;    // slicing; b no longer knows about Derived
Base& rb = d;  // no slicing
std::unique_ptr<Base> p = std::make_unique<Derived>();  // no slicing

Mark interfaces virtual and overrides override

If dynamic behavior is intended, the base must declare the function virtual and each derived implementation should use override. This catches signature mismatches and prevents accidental static binding that defeats polymorphism.

⚠️ A missing virtual on the base or a mismatched signature in the derived silently turns a virtual call into a nonvirtual call; the program still compiles and runs with the wrong behavior.

When to prefer composition

Prefer composition when you need to assemble behavior from parts rather than model an is a relationship. Composition keeps dependencies explicit, avoids the inheritance hierarchy trap, supports runtime swapping of strategies, and plays well with value semantics and templates.

Modeling roles with strategies

Use small interfaces and hold them by value or smart pointer inside a class. Switching strategies at run time often solves the same problem that inheritance tries to solve, while keeping object graphs flatter and more testable.

struct Logger {
  virtual ~Logger() = default;
  virtual void write(std::string_view msg) = 0;
};

struct ConsoleLogger : Logger {
  void write(std::string_view msg) override { std::fputs(msg.data(), stdout); std::fputc('\n', stdout); }
};

class Service {
  std::unique_ptr<Logger> log_;
public:
  explicit Service(std::unique_ptr<Logger> lg) : log_(std::move(lg)) {}
  void do_work() {
    log_->write("starting");
    /* … */
    log_->write("done");
  }
};

Choosing composition over inheritance

If you are not expressing true substitutability, choose composition. If the base would carry mutable state shared across unrelated features, choose composition. If you want to vary behavior per instance and not per type, choose composition. If testing becomes easier with injected roles, choose composition. In short, reach for inheritance last.

💡 A useful rule: design with composition first; introduce public inheritance only when substitutability is clear and proven by use cases.

Chapter 13: Templates (The Generics Engine)

Templates let you write algorithms and types that adapt to many concrete forms at compile time. They power the standard library, enable zero cost abstraction, and connect with type deduction, overload resolution, and instantiation rules. This chapter builds the essential mental model, then walks through declarations, deduction, non type parameters, specialization, and name lookup in dependent contexts.

Function and class templates

A function template is a pattern for generating functions; a class template is a pattern for generating classes. Instantiation happens when you use the template with specific arguments, and the compiler produces concrete entities for those arguments. You write one definition; the compiler synthesizes many versions as needed.

Function templates

Function templates parameterize types or values that appear in the function signature or body. You can let the compiler deduce template parameters from call arguments, or you can provide them explicitly when needed.

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

int a = max_of(3, 7);  // T = int
std::string s = max_of(std::string("a"), std::string("z"));  // T = std::string

Class templates

Class templates define families of types that vary by parameters. You can create type aliases for common specializations to reduce noise in client code.

template <class T, class Alloc = std::allocator<T>>
class SimpleVec {
  T* p{};
  std::size_t n{};
  Alloc alloc;
public:
  explicit SimpleVec(std::size_t count) : p(alloc.allocate(count)), n(count) {}
  /* … minimal vector-like operations … */
};

using Bytes = SimpleVec<std::byte>;
💡 Give template parameters clear names such as T, Key, Value, or Traits; this improves readability and communicates intent.

Template argument deduction

When you call a function template without specifying parameters, the compiler deduces template arguments from the call. Deduction follows rules that compare the function parameter types against the argument expressions while respecting references, cv qualifiers, and value category.

Type deduction basics

Type deduction treats T&, const T&, and T differently. Forwarding references preserve lvalue or rvalue status; plain T by value strips references and cv qualifiers from the argument. Understanding this keeps copies controlled and avoids surprises.

template <class T>  // T is decayed; arg is copied or moved
void by_value(T x);

template <class T>  // T is deduced without adding const
void by_ref(T& x);

template <class T>  // forwarding reference; preserves value category
void forward_ref(T&& x);

int i = 0;
const int ci = 0;
by_value(i);      // T = int
by_ref(i);        // T = int
by_ref(ci);       // error; cannot bind non-const lvalue ref to const
forward_ref(i);   // T = int&; param type becomes int&& &  →  int&
forward_ref(42);  // T = int;   param type becomes int&&

Class template argument deduction and guides

Many class templates support CTAD which lets you write the constructor arguments and let the compiler deduce template parameters. Deduction guides are rules that direct CTAD when constructor signatures are not enough or are ambiguous.

std::pair p{42, 3.14};   // deduces std::pair<int,double>
std::vector v{1, 2, 3};  // deduces std::vector<int>

template <class T, class Alloc>
class Box { public: Box(T, Alloc); };

Box b{std::string{"x"}, std::allocator<char>{}};  // may need a guide if not obvious
⚠️ CTAD never performs narrowing conversions during deduction; add explicit constructors or deduction guides when element types must be fixed precisely.

Non type template parameters

Templates can also take constant values known at compile time. These are non type template parameters. They control sizes, policies, and behavior without runtime overhead. Since C++20, many literal types are allowed including pointers, references, and even some class types with structural semantics.

Using integral and pointer non type parameters

Classic examples include fixed size arrays and lookup tables. Pointers and references as parameters encode object locations or function links known at compile time; use carefully with linkage and odr rules.

template <std::size_t N>
struct Fixed {
  std::array<int, N> data{};
};

template <auto* Fn>
struct Callback {
  void call() const { (*Fn)(); }
};

void hello();
using Hello = Callback<&hello>;

Structural types and auto non type parameters

The auto placeholder permits concise non type parameters. Structural types such as std::integral_constant like values or user types that satisfy structural rules can also serve. This improves expressiveness while keeping strong compile time guarantees.

template <auto V>
constexpr auto value_v = V;

static_assert(value_v<42> == 42);
💡 Prefer auto non type parameters when the exact type is obvious from usage; it reduces repetition and keeps signatures tidy.

Specialization and partial specialization

Specialization lets you customize a template for particular arguments. A full specialization fixes all parameters. A partial specialization matches a subset of patterns and participates in ordering rules. Function templates cannot be partially specialized; use overloading instead.

Full specialization for a specific case

Provide a full specialization when a specific set of parameters requires different representation or behavior. Place the specialization in the same namespace as the primary template to follow lookup rules.

template <class T>
struct Hash;

template <>
struct Hash<std::string> {
  std::size_t operator()(const std::string& s) const noexcept { /* … */ return 0; }
};

Partial specialization for families of cases

Partial specializations select among families by matching patterns. The most specialized viable match wins. Use these for traits and type transformations where structure matters more than values.

template <class T>
struct RemovePtr { using type = T; };

template <class T>
struct RemovePtr<T*> { using type = T; };

template <class T>
using RemovePtrT = typename RemovePtr<T>::type;
⚠️ Prefer specialization for class templates and overloads for function templates; attempting to partially specialize a function template is ill formed.

ADL and dependent names

ADL (Argument Dependent Lookup) searches the namespaces associated with the types of function call arguments. Dependent names are identifiers whose meaning depends on template parameters. These features interact strongly with templates and influence which functions are found and which names require annotation.

Making customization discoverable with ADL

Place free functions that customize algorithms in the same namespace as the types they extend. Calls that rely on ADL should use unqualified names so that lookup can find the right overload during instantiation.

namespace img {
  struct Pixel { int r, g, b; };
  void swap(Pixel& a, Pixel& b);  // found by ADL
}

template <class T>
void algo(T& a, T& b) {
  using std::swap;
  swap(a, b);  // finds img::swap via ADL or falls back to std::swap
}

Qualifying dependent types and templates

When a name depends on a template parameter, you may need keywords to disambiguate parsing. Use typename before dependent type names and use template before a dependent template when calling a member template. These markers tell the compiler to treat the names as types or templates during parsing.

template <class C>
void f(C& c) {
  using Iter = typename C::iterator;  // dependent type; needs typename
  Iter it = c.begin();
  c.template emplace_back(1, 2, 3);   // dependent template; needs template
}
💡 If a name depends on a template parameter and denotes a type, add typename; if it denotes a template and is followed by <…>, add template. This keeps parsing unambiguous and your intent clear.

Chapter 14: Concepts and Constraints

Concepts let you describe the expectations that a template places on its arguments. Instead of waiting for a deep instantiation error, you give the compiler a clear statement of intent, and it checks constraints before instantiating. This leads to better diagnostics, clearer interfaces, and more robust generic code. This chapter explores the purpose of constraints, the forms of requires, how to craft custom concepts, how to use constrained auto, and how to move from older SFINAE techniques to modern expressions.

Why constraints improve diagnostics

Without constraints, templates accept almost anything until substitution reaches a point where something fails to compile. The resulting error may refer to a detail buried far inside another template. A constraint moves that check to the boundary of the template so that failure happens early and the message points directly to the violated assumption.

Failing early at the call site

When a constraint is not satisfied, the compiler does not instantiate the template at all. This puts responsibility on the caller rather than the internals, which helps pinpoint the exact mismatch. Constraints therefore serve both as documentation and enforcement.

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

half(3.0);  // error at the call site; double is not integral
💡 Constrain templates at their boundary rather than deep inside. This keeps errors clear and directs readers toward the intended usage.

Using requires

A requires clause attaches a constraint to a declaration. A requires expression checks whether a set of operations or properties is valid for a given set of types or values. Both are central to expressing intent about template parameters.

Requires clausess

Use a requires clause after the template parameter list or after the function signature to state requirements on arguments. Clauses can combine concepts with logical operators such as && and || for more expressive contracts.

template <class T>
requires std::copy_constructible<T> && std::movable<T>
void swap_em(T& a, T& b) {
  T tmp = a;
  a = b;
  b = tmp;
}

Requires expressions

A requires expression describes exact operations that must be valid in a context. It can check expression validity, return types, and nested constraints while remaining purely compile time logic.

template <class T>
concept Addable = requires(T a, T b) {
  { a + b } -> std::same_as<T>;
  { a += b };
};
⚠️ Avoid overly strict requires expressions; check only what your algorithm truly needs rather than everything the type might support.

Defining custom concepts

Concepts let you name recurring patterns and use them in many templates. Custom concepts turn informal requirements into reusable constraints that improve readability and maintainability.

Concept definitions

A concept is a compile time predicate that returns true or false. You can compose concepts with logical operators, and you can combine broad concepts with narrow ones to refine meaning.

template <class T>
concept NumberLike =
  std::integral<T> ||
  std::floating_point<T>;

template <NumberLike T>
T scale(T x, T factor) { return x * factor; }

Traits and concepts working together

Use type traits to query properties such as nested typedefs, constant expressions, or member presence, then wrap those checks in a concept. This preserves the benefits of existing trait infrastructure while giving you clear constraint boundaries.

template <class T>
concept HasValueType = requires { typename T::value_type; };
💡 Keep concepts simple and focused. A small well named concept clarifies code far more than a large compound expression scattered across many templates.

Constrained auto and abbreviated templates

C++20 introduced constrained auto which allows a concept to appear directly in parameter lists. This is known as an abbreviated template. It reduces verbosity by placing constraints exactly where parameters appear rather than at the top of a declaration.

Using concepts with auto

You can place a concept before auto to constrain a parameter inline. The compiler treats this as shorthand for a template declaration with the concept as a constraint.

void print_twice(std::integral auto x) {
  std::cout << x << " " << x;
}

Abbreviated templates

Abbreviated templates reduce noise in simple function templates. They also help reveal intent at the point of use. Keep this style for shorter functions and reserve fuller template forms for complex contracts.

std::totally_ordered auto max3(std::totally_ordered auto a,
                               std::totally_ordered auto b,
                               std::totally_ordered auto c) {
  return std::max(std::max(a, b), c);
}
⚠️ Abbreviated templates cannot express every constraint relationship. Use a full template header when parameters depend on each other.

Migrating SFINAE to concepts

Older template code used SFINAE to remove overloads from consideration when substitution failed. Concepts make this intent explicit and readable. Migrating from SFINAE to concepts usually simplifies code, reveals intent, and trims dense helper machinery.

Replacing enable_if patterns with constraints

Patterns such as std::enable_if_t<cond, T> are replaced with straightforward requires clauses or concept constrained parameters. This removes boilerplate and puts the condition where people expect to find it.

// old style
template <class T,
          class = std::enable_if_t<std::integral<T>::value>>
T f(T x);

// modern style
template <std::integral T>
T f(T x);

Trait based detection

Type trait detection used to chain specializations, void_t patterns, and fallback overloads. Concepts now express the desired property directly using requires expressions and combine naturally with the rest of the type system.

// old detection idiom
template <class, class = void>
struct HasSize : std::false_type {};

template <class T>
struct HasSize<T, std::void_t<decltype(std::declval<T>().size())>>
  : std::true_type {};

// modern concept
template <class T>
concept HasSizeC = requires(T t) { t.size(); };
💡 When updating code, migrate traits or SFINAE expressions first, then apply concepts at call boundaries. This produces the clearest improvement in diagnostics and structure.

Chapter 15: Compile Time Programming

Compile time programming lets the compiler do real work before your program ever runs. You can calculate values, shape types, and even remove entire branches of code using constant evaluation. The result is smaller binaries, faster execution, and clearer intent; you tell the compiler what must be known early, and it verifies your claim.

constexpr and constant evaluation

constexpr marks expressions and entities that can be evaluated at compile time when given constant inputs. A constexpr function may still run at runtime if called with nonconstant arguments; the keyword grants permission for constant evaluation, and the compiler decides based on the call site. Constant evaluation applies to literals, pure arithmetic, suitable library calls, and well formed constexpr functions; the compiler then folds results directly into your program image.

💡 If a constexpr function is called with constant arguments, the result becomes a constant expression and can be used where an integral constant is required; for example an array bound or a template parameter.

Rules for constexpr functions

A constexpr function must have a body that the compiler can analyze without side effects; it can read literal types and call other constexpr functions, but it may not perform I/O or mutate nonlocal state. In practice this means most arithmetic and many algorithms over simple aggregates can be lifted to compile time.

constexpr int gcd(int a, int b) {
  while (b != 0) {
    int t = a % b;
    a = b;
    b = t;
  }
  return a;
}
static_assert(gcd(48, 18) == 6);

Variables, constructors, and constexpr objects

Objects can be declared constexpr when their initialization is a constant expression. Types with constexpr constructors allow you to build richer constant objects, such as fixed lookup tables or geometry primitives. If any subexpression is not constant, the variable silently falls back to runtime initialization.

struct Point {
  int x;
  int y;
  constexpr int manhattan() const { return x + y; }
};
constexpr Point p{3, 4};
static_assert(p.manhattan() == 7);

consteval and immediate functions

consteval declares an immediate function; every call must be evaluated during compilation. If a call happens in a context that is not constant, the program is ill formed. Use immediate functions when a value must exist before code generation, such as computing sizes for static arrays or validating format strings.

When to prefer consteval over constexpr

Choose consteval when runtime fallback would hide a bug. If a function encodes a contract that only makes sense at compile time, require it. Otherwise keep constexpr for flexibility. This policy keeps diagnostics sharp; mistakes surface where the bad call occurs.

consteval int index_of(char c) {
  if (c < 'a' || c > 'z') {
    throw "only lowercase a..z"; // triggers a compile time error
  }
  return c - 'a';
}
constexpr int i = index_of('c'); // ok
// int j = index_of(std::getchar()); // ill formed
⚠️ Throwing in immediate functions is only for signaling failure during constant evaluation; it does not create a runtime exception path.

Type traits and detection idioms

Type traits expose compile time facts about types, such as whether a type is trivially copyable or whether an expression is valid. The standard library provides a rich set in <type_traits>; you combine them with boolean logic or use them as predicates in templates. Detection idioms let you probe for the presence of members, aliases, or operations without forming ill formed expressions.

Core traits and boolean plumbing

Most traits follow the pattern std::is_…<T>::value and an alias form std::is_…_v<T>. To transform types there are helpers like std::remove_reference_t<T> and std::decay_t<T>. These tools let you write templates that adapt to arguments and still remain readable.

TraitMeaningExample
std::is_integral_v<T>True for integral typesstd::is_integral_v<int>
std::is_same_v<A,B>Type equality checkstd::is_same_v<int, const int>
std::is_trivially_copyable_v<T>Memcpy safe copystd::is_trivially_copyable_v<std::byte>
template<class T>
constexpr bool is_small_trivial_v =
  sizeof(T) <= sizeof(void*) && std::is_trivially_copyable_v<T>;

static_assert(is_small_trivial_v<int>);

The classic detection idiom

The detection idiom creates a dependent context that is valid when an expression is well formed. The simplest form uses std::void_t with a primary template and a partial specialization. If substitution fails, the specialization is discarded and the primary template remains.

template<class, class = void>
struct has_reserve : std::false_type {};

template<class T>
struct has_reserve<T, std::void_t<decltype(std::declval<T&>().reserve(0))>>
  : std::true_type {};

static_assert(has_reserve<std::vector<int>>::value);

Detection utilities

Modern C++ lets you detect expressions more directly with requires. The expression form yields a boolean at compile time and reads like a miniature interface declaration. It works well with concepts and simplifies older SFINAE patterns.

template<class T>
concept Reservable = requires(T t, std::size_t n) {
  { t.reserve(n) } -> std::same_as<void>;
};

static_assert(Reservable<std::vector<int>>);

Metafunctions and compile time containers

A metafunction is a template that maps types or values to new types or values. They are the building blocks of template metaprogramming; you use them to select overloads, compute constants, or generate structures. Compile time containers hold values in the type system or as constant data; they enable unrolled loops and table driven code generation.

Type to value and value to type transformations

Metafunctions often expose a nested ::type or an alias template for clarity. For values, use std::integral_constant and its aliases such as std::bool_constant. Compose these pieces to build readable compile time logic.

template<class T> struct add_ptr { using type = T*; };
template<class T> using add_ptr_t = typename add_ptr<T>::type;

static_assert(std::is_same_v<add_ptr_t<int>, int*>);

template<int N> using int_c = std::integral_constant<int, N>;
static_assert(int_c<42>::value == 42);

std::integer_sequence and index packs

std::integer_sequence and its helper std::make_index_sequence present a sequence of compile time integers. You use them to expand parameter packs, to unroll operations across tuples, and to synthesize arrays without loops in the runtime sense.

template<class Tuple, std::size_t... I>
constexpr auto tuple_to_array_impl(const Tuple& t, std::index_sequence<I...>) {
  return std::array{std::get<I>(t)...};
}

template<class... Ts>
constexpr auto tuple_to_array(const std::tuple<Ts...>& t) {
  return tuple_to_array_impl(t, std::make_index_sequence<sizeof...(Ts)>{});
}

static_assert(tuple_to_array(std::tuple{1,2,3})[1] == 2);
💡 Index sequences let you write a single source of truth; the pack drives both compile time shapes and the generated statements.

Building constant tables

You can fill std::array or custom structs in a constexpr context and then reference them at runtime without overhead. The compiler stores the final bytes directly in the data segment or even folds them into instruction immediates.

constexpr auto make_squares() {
  std::array<int, 8> a{};
  for (std::size_t i = 0; i < a.size(); ++i) a[i] = static_cast<int>(i * i);
  return a;
}
constexpr auto squares = make_squares();
static_assert(squares[3] == 9);

Generating efficient code at build time

Compile time programming is not only about cleverness; it is about guiding the compiler to form the best machine code. Use if constexpr to erase dead branches, compute shapes and sizes up front, and prebuild tables that replace runtime computation. The payoff is clear assembly and predictable performance.

Erasing branches

if constexpr evaluates its condition during compilation; the false branch is discarded and never instantiated. This removes unused code paths and avoids hard to read SFINAE tricks. The pattern pairs well with concepts and traits.

template<class T>
void serialize(const T& t) {
  if constexpr (std::is_integral_v<T>) {
    // write integer fast path
  } else if constexpr (std::is_floating_point_v<T>) {
    // write float path
  } else {
    // generic reflection path …
  }
}

Table driven computation

When inputs come from small finite sets, compute results ahead of time and index into a constant table. This replaces branches and arithmetic with a single load. Hash seeds, bit counts, and small transforms are classic candidates.

constexpr std::array<unsigned char, 256> popcount_lut = []{
  std::array<unsigned char, 256> a{};
  for (int i = 0; i < 256; ++i) {
    unsigned x = static_cast<unsigned>(i);
    unsigned c = 0;
    while (x) { c += x & 1u; x >>= 1; }
    a[static_cast<std::size_t>(i)] = static_cast<unsigned char>(c);
  }
  return a;
}();

inline unsigned popcount8(unsigned char x) {
  return popcount_lut[x];
}
⚠️ Precomputing tables increases binary size; prefer small domains or compress the data when the table is large.

Validating inputs during compilation

Immediate functions and static_assert let you check invariants before a program can be built. You can validate parseable string literals, format patterns, and bitfield layouts. The resulting diagnostics point at the call sites and speed up debugging.

consteval bool valid_hex_digit(char c) {
  return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}

template<std::size_t N>
consteval void check_hex_literal(const char(&s)[N]) {
  for (std::size_t i = 0; i + 2 < N; ++i) {
    if (s[i] == '0' && s[i+1] == 'x') {
      for (std::size_t j = i + 2; j + 1 < N; ++j) {
        if (!valid_hex_digit(s[j])) throw "bad hex digit";
      }
    }
  }
}

constexpr const char* id = "0x12af";
consteval auto _ = (check_hex_literal(id), 0);

Putting it together

The most effective pattern blends concepts, detection, if constexpr, and constant data. Detect capabilities, select fast paths, and bake shapes or tables at build time. The runtime code stays small and the compiler has maximum room to optimize away the scaffolding.

Chapter 16: The Standard Library/h1>

The standard library gives C++ its broad practical reach. It provides memory utilities, containers, algorithms, time facilities, error handling tools, filesystem access, and countless small helpers that turn raw language features into a comfortable working environment. This chapter offers a guided walk through major areas so that readers gain a sense of the layout and naming patterns before diving deeper.

Header map and naming patterns

The standard library uses simple header names such as <vector>, <tuple>, and <filesystem>. These contain declarations in the std namespace (with rare auxiliary namespaces for traits or literals). Identifiers often follow consistent suffixes such as _t for type aliases or _v for variable templates, and traits frequently end with _traits or _type.

Categories of library headers

Headers fall into broad groups. There are containers (<vector>, <map>), algorithms (<algorithm>, <numeric>), iterators (<iterator>), utilities (<utility>), I/O streams (<iostream>, <fstream>), multithreading (<thread>, <mutex>), and support libraries such as <type_traits> or <functional>. Each header keeps its domain small which helps you find features quickly and avoid large includes.

#include <utility>     // pair, move helpers, forward
#include <tuple>       // tuple and tie
#include <chrono>      // time and durations
#include <filesystem>  // paths, directories, files
💡 When unsure where something lives, look for a header name that matches the concept with only one or two words. The library stays consistent and avoids deep hierarchies.

Utility types

These utility types model small flexible containers for values. They enable structured binding, safe absence, tagged unions, and erased types. Each fits a distinct shape of problem and keeps code expressive without custom boilerplate.

pair and tuple for structured values

pair holds two values often used for maps or return types. tuple generalizes this to an arbitrary number of elements. Both support structured binding which lets you unpack them into named locals for clarity.

std::pair<int, std::string> p{7, "seven"};
auto [n, word] = p;

std::tuple t{3.14, 42, std::string{"pi-ish"}};
auto [x, y, label] = t;

optional for possibly absent values

optional<T> represents either a T or no value. It prevents sentinel clutter and makes absence explicit. Its interface mirrors pointers in some ways, but the semantics describe presence rather than ownership.

std::optional<int> read_int(std::string_view s) {
  if (auto pos = s.find_first_not_of("0123456789"); pos == std::string_view::npos)
    return std::stoi(std::string{s});
  return std::nullopt;
}

variant and any for dynamic shape

variant holds one value from a fixed set of types and enforces indexed access through std::visit. any holds any type that is copyable and hides its concrete form. variant is type safe and structured; any is flexible but relies on runtime casting.

std::variant<int, std::string> v = 42;
v = std::string{"answer"};

std::any a = 3.14;
double d = std::any_cast<double>(a);
⚠️ Prefer variant when the set of possible types is small and known. Reserve any for plugin interfaces or cases where the type set is open ended.

Error reporting

Errors in C++ can be signalled in several ways. error_code provides a lightweight value based mechanism for reporting system or library errors. expected represents either a successful result or an error value and offers a structured alternative to exceptions.

error_code for portable system style errors

error_code pairs an integer condition with a category that interprets it. Functions that may fail can take an std::error_code& parameter to report problems directly without throwing. This pattern keeps control flow explicit.

std::error_code ec;
auto sz = std::filesystem::file_size("data.txt", ec);
if (ec) {
  std::cerr << "cannot get size: " << ec.message();
}

expected for explicit success or failure

expected<T,E> represents either a value of type T or an error of type E. It avoids exceptions while preserving a clear structure for failure paths. C++23 introduces std::expected which brings this style into the standard library.

std::expected<int, std::string> parse_number(std::string_view s) {
  if (auto pos = s.find_first_not_of("0123456789"); pos == std::string_view::npos)
    return std::stoi(std::string{s});
  return std::unexpected("invalid number");
}
💡 Use expected when you want structured flow without exceptions and when callers must acknowledge both success and failure paths.

Time utilities: chrono

The chrono library models time points, durations, and clocks with strong types. It prevents unit mix ups by requiring explicit conversions and encourages clear reasoning about rates, intervals, and wall clock vs steady time.

Durations and time points

A duration is a count of ticks paired with a unit such as seconds or milliseconds. A time point represents a moment measured from a clock’s epoch. Arithmetic between them follows simple rules: time point plus duration yields another time point; difference of two time points yields a duration.

using namespace std::chrono;

auto t0 = steady_clock::now();
auto t1 = steady_clock::now();
auto dt = t1 - t0;          // duration
auto later = t0 + 250ms;    // time point plus duration

Clocks and conversions

system_clock tracks real world time; steady_clock guarantees monotonic progress; high_resolution_clock refers to the most precise available clock. Converting between durations uses duration_cast when precision must be controlled.

auto d = 3s;
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(d);
⚠️ Avoid mixing system_clock and steady_clock time points. Their epochs and purposes differ so arithmetic between them is ill formed.

Filesystem and paths

The filesystem library gives portable tools for navigating directories, managing paths, querying file status, and performing common operations such as copying, moving, or creating directories. It abstracts platform differences and offers value semantics similar to other library components.

Path manipulation

std::filesystem::path models a filesystem path with separators, extensions, and lexical operations. It keeps platform specific rules inside the type so your code can remain portable.

namespace fs = std::filesystem;

fs::path p = "output";
p /= "log.txt";          // append path component
auto ext = p.extension();

Queries and operations

The library provides functions for existence checks, file size, directory iteration, and file copying. All operations have overloads that accept error_code to report problems without throwing.

for (auto& entry : fs::directory_iterator("data")) {
  if (entry.is_regular_file()) {
    std::cout << entry.path() << "\n";
  }
}
💡 Use directory_iterator for simple one level scans and recursive_directory_iterator for deep walks. Both provide clean range based loops and clear intent.

Chapter 17: Containers and Iterators

The Standard Library containers give you ready made data structures with predictable complexity and value semantics. They pair with iterators, which act like generalized pointers. Together they power generic algorithms; you provide a container, an iterator range, and a policy, then the library does the work efficiently and safely.

Sequence containers

Sequence containers store elements in a specific order and let you access them by position or by traversal. Each choice trades constant factors and iterator capabilities for different workloads. Pick the one that matches your insertion and access patterns, then let algorithms operate on iterator pairs.

vector for contiguous growth

std::vector<T> owns a contiguous block; random access is constant time and cache friendly. Inserting or erasing at the end is amortized constant time; inserting in the middle moves a tail. Reallocation invalidates all iterators and references; push back may trigger it.

std::vector<int> v;
v.reserve(1024);
for (int i = 0; i < 10; ++i) v.push_back(i);
v[3] = 42;  // random access
💡 Reserve capacity when you can estimate size; this avoids repeated reallocations and keeps iterators valid longer.

deque for fast ends

std::deque<T> provides constant time push and pop at both ends with stable references across many operations. It is not one flat array; it is a map of blocks that still offers random access. Choose it for queue like workloads with frequent front operations.

std::deque<int> d{1,2,3};
d.push_front(0);
d.push_back(4);
int x = d[2];  // random access still works

list and forward_list for splicing

std::list<T> is a doubly linked list; std::forward_list<T> is a singly linked list with minimal overhead. They keep iterators valid on insert and erase of other elements, and they offer splice to move nodes between lists without allocation. Traversal is linear and cache locality is poor, so use them when stable node addresses and constant time splicing matter.

std::list<int> a{1,2,3}, b{4,5};
auto it = std::next(a.begin());  // points at 2
b.splice(b.end(), a, it);        // move 2 into b by relinking
⚠️ Algorithms that depend on random access, such as std::sort, do not work with list iterators; use list::sort or move elements into a contiguous container.

Complexity and invalidation summary

Operations differ across sequence containers, especially for insertions and iterator invalidation. The following quick reference helps match structure to workload.

ContainerEnd insertMiddle insertRandom accessIterator stability
vectorAmortized O(1)O(n)YesRealloc invalidates all
dequeO(1)O(n)YesMany ops keep validity
listO(1)O(1) with iteratorNoStable except erased
forward_listO(1) after before iteratorNoStable except erased

Associative containers: set, map, multiset, multimap

Associative containers organize elements as ordered trees keyed by a comparator. Lookups, inserts, and erases are logarithmic and iterators traverse keys in sorted order. set and map enforce unique keys; the multi versions allow duplicates.

Ordered lookup and hints

Typical operations include find, lower_bound, and range erasure. Insertion can accept a position hint; when correct, the tree can link in the node with minimal work. Value types are Key for set and std::pair<const Key, T> for map.

std::map<std::string, int> m;
m.emplace("alpha", 1);
m.try_emplace("beta", 2);
auto it = m.lower_bound("beta");
m.emplace_hint(it, "beta", 3); // efficient when near the right spot
💡 Use transparent comparators like std::less<>; they allow heterogeneous lookup with std::string_view without constructing a temporary key.

Ranges and node handles

Node extraction lets you move elements between containers without copy. The extracted node preserves allocation and key; you can modify the key of a map node before reinsertion. Range overloads accept iterator pairs and integrate well with modern algorithms.

auto nh = m.extract("alpha");
if (nh) {
  nh.key() = "aleph";
  m.insert(std::move(nh));
}

Unordered containers and hashing

Unordered containers are hash tables that trade order for expected constant time lookup. Buckets group elements by hash; equal keys reside in the same bucket. Performance depends on load factor, hash quality, and equality comparison.

unordered_set and unordered_map

The interfaces mirror the ordered versions, with bucket level tools for diagnostics. Reserve to reduce rehashing and keep the load factor near a value that suits your workload. Supplying robust hash and equals is essential for custom keys.

std::unordered_map<std::string, int> um;
um.reserve(1024);
um["tea"] = 1;
um["coffee"] = 2;
if (auto it = um.find("tea"); it != um.end()) { /* use it */ }
⚠️ Rehashing invalidates iterators and may invalidate references; plan bulk inserts with reserve to limit reshuffles.

Custom keys and hash specializations

For your own types, provide a hasher and an equality predicate that agree on equivalence. Combine member hashes using simple integer mixing. Keep equality consistent with the fields you hash; otherwise lookups will miss matches.

struct Point { int x; int y; };

struct PointHash {
  std::size_t operator()(const Point& p) const noexcept {
    std::size_t h1 = std::hash<int>{}(p.x);
    std::size_t h2 = std::hash<int>{}(p.y);
    return h1 ^ (h2 + 0x9e3779b9u + (h1 << 6) + (h1 >> 2));
  }
};

struct PointEq {
  bool operator()(const Point& a, const Point& b) const noexcept {
    return a.x == b.x && a.y == b.y;
  }
};

std::unordered_set<Point, PointHash, PointEq> ps;

Container adaptors

Adaptors expose restricted interfaces built on underlying sequence containers. They enforce specific access patterns and hide operations that would violate the abstraction. You can choose the base container when constructing the adaptor to fit your needs.

stack and queue basics

stack is last in, first out; queue is first in, first out. By default they use deque; you may switch to vector when middle insert is unnecessary and you prefer contiguous storage.

std::stack<int, std::vector<int>> st;
st.push(1); st.push(2);
int t = st.top(); st.pop();
💡 Use emplace on adaptors to construct elements in place; it avoids a temporary and copies.

priority_queue for heaps

priority_queue maintains a heap where top returns the largest element under a comparator. It supports push, emplace, pop, and top. For bulk construction, build a vector then call std::make_heap for linear time setup.

std::priority_queue<int> pq;
for (int v : {3,1,4,1,5}) pq.push(v);
int best = pq.top();  // 5

Iterators and iterator categories

Iterators act like generalized pointers that traverse ranges. Categories describe what operations are valid and how algorithms may step. The stronger the category, the more algorithms apply and the more efficient they can be. Since C++20 there is also the contiguous category for memory linearity.

Categories and capabilities

The table lists classic categories plus contiguous, with typical operations and examples. Use category aware algorithms; for example std::advance runs in constant time for random access and linear time for forward.

CategoryCore opsExamples
InputRead, single passistream_iterator
OutputWrite, single passostream_iterator
ForwardRead write, multi pass, ++forward_list iterators
Bidirectional++, --list iterators
Random access+=, indexing, <vector, deque iterators
ContiguousRandom access with linear memoryvector, string iterators
template<class It>
void skip_even_positions(It first, It last) {
  using cat = typename std::iterator_traits<It>::iterator_category;
  if constexpr (std::is_base_of_v<std::random_access_iterator_tag, cat>) {
    for (; first < last; first += 2) { /* … */ }
  } else {
    bool keep = true;
    for (; first != last; ++first) { if (keep) { /* … */ } keep = !keep; }
  }
}
⚠️ Invalidating iterators leads to undefined behavior; consult the specific container rules for when operations keep or break validity.

Iterator adaptors and sentinels

Adaptor types modify traversal without copying data. Examples include std::reverse_iterator, std::move_iterator, and the C++20 range based sentinels where end is a different type from the iterator. These parts stack with algorithms to express intent cleanly.

std::vector<int> v{1,2,3};
std::copy(std::make_move_iterator(v.begin()),
          std::make_move_iterator(v.end()),
          std::ostream_iterator<int>(std::cout, " "));

Allocators and memory resource

Allocators abstract memory acquisition so containers can adapt to different strategies. The classic allocator model uses types like std::allocator<T>; the polymorphic model in std::pmr routes allocations through runtime selected resources that match a use case.

Classic allocators

Every standard container template parameterizes on an Allocator type. Custom allocators let you count bytes, place objects in shared arenas, or map storage to special regions. Correctness requires honoring the allocator interface; performance depends on your allocation pattern more than micro details.

template<class T>
struct CountingAlloc {
  using value_type = T;
  static inline std::size_t bytes = 0;
  T* allocate(std::size_t n) {
    bytes += n * sizeof(T);
    return static_cast<T*>(::operator new(n * sizeof(T)));
  }
  void deallocate(T* p, std::size_t) noexcept { ::operator delete(p); }
};
std::vector<int, CountingAlloc<int>> v;
v.resize(100);
💡 Small object heavy workloads benefit from pooling; combine a container with an allocator that batches requests.

std::pmr and polymorphic resources

Polymorphic memory resources decouple container type from allocation policy at runtime. Use std::pmr::monotonic_buffer_resource for arena style growth, std::pmr::unsynchronized_pool_resource for small block pooling, and wrap containers in their pmr aliases.

std::byte buf[4096];
std::pmr::monotonic_buffer_resource pool{buf, sizeof buf};
std::pmr::vector<std::pmr::string> names{&pool};
names.emplace_back("Ada");
names.emplace_back("Bjarne");

Choosing a resource strategy

Pick monotonic arenas for build once, destroy together lifetimes; pick pool resources for steady churn of small objects; fall back to the default new delete resource when allocations are infrequent. Measure real code with realistic data; allocation profiles often surprise.

Chapter 18: Algorithms and Ranges

The algorithms library is a treasure chest of generic building blocks. You provide iterator or range boundaries and a policy, then the algorithm performs well tested operations that respect complexity guarantees. The ranges library extends this with composable views that form pipelines and evaluate lazily; the result is expressive code that stays efficient and predictable.

Classic algorithms and complexity

Classic algorithms operate directly on iterator pairs and follow well understood asymptotic rules. Sorting and partitioning are logarithmic or linearithmic; searches and transforms are linear; constant time operations apply to moves and swaps. Understanding these rules helps avoid quadratic traps and guides container choices.

Sorting, partitioning, and searching

std::sort is the workhorse with average complexity O(n log n); std::stable_sort preserves order of equal elements with higher space cost. Partitioning splits ranges based on predicates. Binary search family functions (such as lower_bound) work on sorted ranges and give logarithmic time for lookups.

std::vector<int> v{3,1,4,1,5,9};
std::sort(v.begin(), v.end());
auto it = std::lower_bound(v.begin(), v.end(), 4);
💡 Sorting followed by many lookups often beats repeated insertions into associative containers; cache locality matters.

Transformations and accumulations

Transform algorithms apply functions or functors element by element. Accumulations reduce a range into a single value using an initial seed. These operations act on value types and avoid manual loops while keeping complexity linear.

std::vector<int> v{1,2,3};
std::transform(v.begin(), v.end(), v.begin(),
               [](int x){ return x * x; });
int sum = std::accumulate(v.begin(), v.end(), 0);

In place reordering

Rotations, shuffles, swaps, and reversal algorithms modify the ordering of elements. They provide both readability and correct complexity so you can adjust structure without reinventing the operations.

std::vector<int> v{1,2,3,4};
std::rotate(v.begin(), v.begin() + 2, v.end());
std::reverse(v.begin(), v.end());

Range views and adaptors

Views are lightweight range facades that compute elements on demand. They do not own storage; instead they transform, filter, or slice underlying data. Adaptors combine views in pipeline expressions, giving clear structure over sequences without materializing intermediate containers.

Core views: filter, transform, take, drop

Using views follows the pattern input | adaptor1 | adaptor2 …. Filter keeps elements that satisfy a predicate; transform applies a function lazily; take and drop select leading or trailing segments; all operate with constant storage.

auto odds = std::views::iota(1, 20)
           | std::views::filter([](int x){ return x % 2; })
           | std::views::transform([](int x){ return x * 2; })
           | std::views::take(5);
⚠️ Many views keep references into underlying ranges; ensure the base data lives long enough for the composed view.

Slicing and joining

std::views::drop_while, take_while, slide, and join offer finer structure. Sliding windows expose fixed width frames across the range; joining flattens nested ranges into a single stream of elements.

std::string s = "hello";
for (auto w : s | std::views::slide(2)) {
  // each w is a two character view …
}

Projections and custom comparators

Many algorithms accept projections, which extract a member or compute a surrogate value before comparison. This replaces hand written lambdas and keeps intent close to the algorithm call. Custom comparators let you define ordering rules for sorting or searching that match your domain.

Projections for cleaner algorithms

Projections let algorithms sort or search by fields without restructuring data. The projection applies to each element before the comparator; typically you use a pointer to member or a simple lambda.

struct Item { int id; std::string name; };
std::vector<Item> items{{2,"beta"},{1,"alpha"}};
std::ranges::sort(items, std::less<>{}, &Item::id);
💡 Projections work with both iterator and range based algorithms; they remove the need for temporary comparison wrappers.

Comparators for order and equality

A comparator is a strict weak ordering. It defines what counts as less than and must be consistent for algorithms to behave. Equality under a comparator does not imply bit equality; it only means neither element is less than the other.

std::ranges::sort(items,
  [](auto& a, auto& b){ return a.name < b.name; });

Pipelines and lazy evaluation

Pipelines chain adaptors and algorithms to express flowing transformations. Because views evaluate lazily, only the elements demanded by the sink are touched. This enables infinite sequences, on the fly computation, and efficient filtering.

Lazy processing with views

Most views do not allocate; they create small wrapper objects that pass through elements when iterated. Effects like transformation or filtering happen during iteration rather than at composition time. This keeps memory use low and keeps pipelines predictable.

auto squares = std::views::iota(1)
             | std::views::transform([](int x){ return x * x; });

for (int x : squares | std::views::take(4)) {
  // x visits 1,4,9,16 …
}

Composing pipelines

You can mix algorithms and views in expressive chains. Algorithms like std::ranges::find operate directly on view pipelines without breaking the lazy structure. This style makes dataflow clear and minimizes intermediate work.

auto data = std::views::iota(0, 100)
          | std::views::filter([](int x){ return x % 3 == 0; });
int first = *std::ranges::find(data, 33);

Parallel algorithms

Parallel algorithms use execution policies to run work across threads. They keep interfaces similar to sequential counterparts while enabling speedups on large data sets. Not every algorithm benefits from parallelism; overhead and cache effects still matter.

Execution policies and behavior

Policies such as std::execution::par and std::execution::par_unseq control concurrency and vectorization. Sequential policies preserve order; unsequenced ones allow aggressive reordering and require operations to be free of data races. Results are defined when the policy permits them; nondeterministic ordering is possible under parallel policies.

std::vector<int> v(1'000'000);
std::iota(v.begin(), v.end(), 0);
std::sort(std::execution::par, v.begin(), v.end());
⚠️ With parallel policies, element operations must be independent; side effects like pushing to a shared container lead to data races.

When parallelism helps

Parallel sorts, transforms, and reductions show gains when data sizes are large and operations are simple. Memory bandwidth becomes the limiter. Measure with realistic workloads and hardware; small ranges often run faster with sequential algorithms.

Chapter 19: Dynamic Memory and Smart Pointers

Dynamic storage lets programs create objects whose lifetime is not tied to scope; this gives flexibility and also risk. This chapter shows how to use new and delete safely, how to prefer smart pointers such as unique_ptr, shared_ptr, and weak_ptr, how to construct with make_… helpers and custom deleters, and how to build object pools or plug in custom allocators when you need tighter control.

new, delete, and placement new

new acquires storage and constructs an object; delete calls the destructor and releases the storage. Correct pairing is essential. Placement new constructs an object in a supplied memory buffer; this is useful for arenas and custom allocators. When you use placement new, you must also destroy the object manually, since delete does not know about the buffer you provided.

Using new and delete

new T(args) returns a pointer to a newly constructed T. Use delete p only for pointers that came from a matching nonarray new, and delete[] p only for pointers that came from new T[n]. Mixing these is undefined behavior. Prefer smart pointers so that destruction is automatic.

// Basic use (prefer smart pointers in real code)
struct Widget {
  std::string name;
  Widget(std::string s) : name(std::move(s)) {}
};

Widget* p = new Widget("w1");
delete p;

// Arrays require delete[]
int* a = new int[4]{1,2,3,4};
delete[] a;
⚠️ If a constructor throws, the memory from new is released automatically; you do not call delete. If construction succeeds but later code throws, smart pointers help you avoid leaks.

Placement new and manual destruction

Placement new constructs at a specific address. You are responsible for storage lifetime. Destroy with an explicit destructor call, then release or reuse the buffer yourself.

#include <new> // std::align_val_t and placement new

alignas(Widget) unsigned char buf[sizeof(Widget)];
Widget* pw = new (buf) Widget("in-place");  // construct in buf
pw->~Widget();                               // manual destruction
// buf remains allocated on the stack; nothing to delete

Overloads, alignment, and std::nothrow

Types can provide class-specific operator new to route allocations; do this only for measured reasons. For over-aligned types, the implementation will request suitable alignment. new(std::nothrow) T returns nullptr on failure; most code should prefer regular new with exceptions enabled.

💡 A simple rule: write code that rarely touches raw new and delete; construct objects with smart pointers or containers instead.

unique_ptr for exclusive ownership

std::unique_ptr<T> represents sole ownership. The pointed object dies when the unique_ptr is destroyed. It is movable and not copyable; move transfers ownership. Use it for clear lifetimes and to express that sharing is not allowed.

Basics and custom deleters

unique_ptr can store a custom deleter type which runs at destruction time. This is how you manage resources that need functions other than delete for cleanup.

#include <memory>
#include <cstdio>

struct FileCloser {
  void operator()(std::FILE* f) const { if (f) std::fclose(f); }
};

std::unique_ptr<std::FILE, FileCloser> file(std::fopen("data.txt","r"));
// file closes automatically in its destructor

Factory helpers and ownership transfer

Create with std::make_unique<T>(args); this is safer than calling new directly. To pass ownership out of a function, return the unique_ptr or use std::move.

std::unique_ptr<Widget> make_widget() {
  return std::make_unique<Widget>("w");
}

auto up = make_widget();    // ownership lives in up
auto sink = std::move(up);  // transfer; up becomes empty
⚠️ Do not create two unique_ptr objects that manage the same raw pointer; this double-deletes. Let the type system enforce the rules instead.

Using unique_ptr in containers

Standard containers store unique_ptr cleanly because the pointer is movable. This expresses ownership of heterogeneous objects while keeping value semantics for the container.

std::vector<std::unique_ptr<Widget>> v;
v.push_back(std::make_unique<Widget>("a"));
v.push_back(std::make_unique<Widget>("b"));

shared_ptr and weak_ptr for shared lifetime

std::shared_ptr<T> maintains a control block with strong and weak counts. When the last shared_ptr to an object goes away, the object is destroyed; when the last weak_ptr also vanishes, the control block is freed. Use weak_ptr to break cycles and to observe without extending lifetime.

Creation and control blocks

Prefer std::make_shared to construct the control block and the object in one allocation. If you need a custom deleter or a custom allocator for the control block, build the shared_ptr from a unique_ptr or use std::allocate_shared.

#include <memory>

auto sp = std::make_shared<Widget>("pooled");
std::weak_ptr<Widget> wp = sp;       // observation without ownership

if (auto locked = wp.lock()) {
  // use *locked safely
}

Cycles and enable_shared_from_this

Two shared_ptr owners that point at each other create a cycle; the counts never drop to zero. Convert one side to weak_ptr. When a type needs to hand out a shared_ptr to itself, inherit from std::enable_shared_from_this<T> and call shared_from_this() on an instance already managed by a shared_ptr.

struct Node : std::enable_shared_from_this<Node> {
  std::vector<std::shared_ptr<Node>> children;
  std::weak_ptr<Node> parent;
  std::shared_ptr<Node> add_child(std::shared_ptr<Node> c) {
    c->parent = shared_from_this();   // break the cycle with weak_ptr
    children.push_back(std::move(c));
    return children.back();
  }
};

Aliasing and thread safety

The aliasing constructor lets a shared_ptr share ownership of one control block while pointing at a subobject. Reference count updates are atomic; object mutation still needs your own synchronization.

auto spw = std::make_shared<Widget>("root");
auto name_view = std::shared_ptr<std::string>(spw, &spw->name); // aliasing
PointerOwnershipCost patternTypical use
unique_ptrExclusiveMove only; zero overhead on countClear single owner
shared_ptrSharedAtomic refcount; possible extra allocGraphs with multiple readers
weak_ptrNoneControl-block onlyBreak cycles; observe
💡 When you do not need sharing, choose unique_ptr. This reduces contention and improves locality.

make functions and custom deleters

std::make_unique, std::make_shared, and std::allocate_shared help with exception safety and speed. They construct the object and its manager in one expression; this avoids temporary raw pointers between steps and in many cases reduces allocations.

Why to prefer make_unique and make_shared

Allocation and construction happen together; if construction throws, nothing leaks. For shared_ptr, make_shared creates the object and control block together; this typically improves cache locality.

auto a = std::make_unique<Widget>("safe");
auto b = std::make_shared<Widget>("compact");

Custom deleters

Use custom deleters for resources that need special cleanup; store them in the smart pointer so they run automatically. For unique_ptr, the deleter type is part of the pointer type. For shared_ptr, the deleter is kept in the control block.

// unique_ptr with lambda deleter
auto close_fd = [](int* fd){ if (fd) { /* close(*fd); */ } };
std::unique_ptr<int, decltype(close_fd)> fd_holder(new int(42), close_fd);

// shared_ptr with custom deleter
auto deleter = [](Widget* p){ /* custom release */ delete p; };
std::shared_ptr<Widget> sp(new Widget("x"), deleter);
⚠️ make_shared does not accept a deleter; if you need a custom deleter, build the shared_ptr with new or convert from unique_ptr, or use allocate_shared with a custom allocator for the control block when that is the real goal.

allocate_shared and memory resources

std::allocate_shared uses an allocator to obtain both the control block and the object storage. This is useful when you want all related allocations to come from a pool or a monotonic arena.

#include <memory_resource>

std::byte arena[4096];
std::pmr::monotonic_buffer_resource mbr(arena, sizeof(arena));
auto sp = std::allocate_shared<Widget>(std::pmr::polymorphic_allocator<Widget>{&mbr}, "pmr");

Object pools and custom allocators

Pooling trades generality for speed and predictability. A pool reserves a large region once, then hands out fixed-size or variable-size blocks quickly. Standard containers can use pools through the allocator interface, and the <memory_resource> toolbox offers ready-to-use arenas for pmr containers.

A tiny fixed-size pool example

This sketch shows a free-list pool for same-sized nodes. It uses placement new for construction in preallocated storage; destruction returns the block to the free list.

struct Node { int x; Node* next; /* … */ };

class NodePool {
  std::vector<unsigned char> storage;
  Node* free_list = nullptr;

public:
  explicit NodePool(std::size_t n) : storage(n * sizeof(Node)) {
    for (std::size_t i = 0; i < n; ++i) {
      auto p = reinterpret_cast<Node*>(&storage[i * sizeof(Node)]);
      p->next = free_list;
      free_list = p;
    }
  }

  template<class... Args>
  Node* create(Args&&... args) {
    if (!free_list) throw std::bad_alloc{};
    Node* p = free_list;
    free_list = free_list->next;
    return new (p) Node{std::forward<Args>(args)...};
  }

  void destroy(Node* p) {
    if (!p) return;
    p->~Node();
    p->next = free_list;
    free_list = p;
  }
};
💡 Pooling helps when allocations are many, small, and frequent; it also reduces fragmentation and improves cache locality.

pmr containers with arena resources

std::pmr lets you provide a memory resource to containers at runtime. A monotonic_buffer_resource grows by carving from upstream only when needed; release happens in one shot when the resource is destroyed or release() is called.

#include <memory_resource>
#include <string>
#include <vector>

std::byte buf[8192];
std::pmr::monotonic_buffer_resource arena(buf, sizeof buf);
std::pmr::vector<std::pmr::string> names{&arena};
names.emplace_back("alpha");
names.emplace_back("beta");

Writing a standard-conforming allocator

Allocators supply and free raw storage for objects of a given type. In modern C++, prefer pmr to custom allocator classes unless you need compile-time allocator types for legacy APIs. If you do write one, implement the required nested aliases and allocate/deallocate with correct alignment.

template<class T>
struct BumpAlloc {
  using value_type = T;
  std::byte* cur;
  std::byte* end;

  BumpAlloc(std::byte* b, std::size_t n) : cur(b), end(b + n) {}
  template<class U> BumpAlloc(const BumpAlloc<U>& other) : cur(other.cur), end(other.end) {}

  T* allocate(std::size_t n) {
    std::size_t bytes = n * sizeof(T);
    if (cur + bytes > end) throw std::bad_alloc{};
    T* p = reinterpret_cast<T*>(cur);
    cur += bytes;
    return p;
  }

  void deallocate(T*, std::size_t) { /* bump alloc frees in bulk */ }
};
⚠️ A bump allocator does not free individual objects; it frees everything at once. Use it for lifetimes that end together, such as parsing phases.

Choosing the right tool

If ownership is single, use unique_ptr. If ownership is shared, use shared_ptr with weak_ptr to avoid cycles. If allocation hot paths show up in profiles, consider make_shared or arena-backed pmr containers. If you need deterministic reuse for fixed-size objects, write or use a pool. Measure before and after each change to confirm the benefit.

Chapter 20: Error Handling and Robustness

Programs fail in many ways; robust code expects this and guides control flow cleanly. C++ offers exceptions for exceptional situations, status objects such as error_code, and value-oriented flow with optional and expected. This chapter explains these tools, shows how to combine them with RAII for safety, and gives design guidance so failures stay contained and understandable.

Exceptions

Exceptions transfer control from the point of failure to a matching handler. Construction unwinds stack frames; destructors run for all fully constructed objects, which is why RAII makes code resilient. Use exceptions for cases that are not part of the normal path, such as contract violations or resource exhaustion that you cannot handle locally.

Throwing and propagating

Throw an exception object with throw. The runtime finds the nearest matching catch; if none is found, the program calls std::terminate(). Prefer throwing by value and catching by reference to avoid slicing.

#include <stdexcept>

int parse_int(const std::string& s) {
  if (s.empty()) throw std::invalid_argument{"empty string"};
  // … perform parsing or throw on bad format
  return 42;
}

int value = 0;
try {
  value = parse_int("17");
} catch (const std::invalid_argument& e) {
  // handle bad input
}

Catch specificity and ordering

Place more specific handlers before general ones. Catching ... handles anything; use this sparingly and rethrow with throw after logging if you cannot recover locally.

try {
  // …
} catch (const std::out_of_range& e) {
  // handle bounds issues
} catch (const std::exception& e) {
  // fallback for standard exceptions
} catch (...) {
  // last resort; log and rethrow
  throw;
}
💡 Keep throw sites close to the detection of the problem; keep catch sites close to the policy that decides what to do about it.

RAII and unwinding safety

During unwinding, local objects are destroyed in reverse order of construction. Ensure destructors never throw; if they must report errors, store state for later queries or use non-throwing callbacks.

struct Guard {
  ~Guard() noexcept { /* release resource; never throw */ }
};

void work() {
  Guard g;
  // any throw between here and the end still releases via ~Guard
}

noexcept and exception safety guarantees

noexcept expresses that a function will not throw. This enables optimizations and is required for some library moves. Exception safety describes what your function promises when an exception occurs: basic, strong, or no-throw. State these promises in documentation and test them.

The meaning of noexcept

A function marked noexcept must not emit exceptions; if it does, the program calls std::terminate(). Use conditional noexcept to mirror the guarantees of operations you call.

template<class T>
void swap_safe(T& a, T& b) noexcept(noexcept(std::swap(a, b))) {
  std::swap(a, b);
}

The three standard guarantees

These guarantees guide design for strong robustness. Choose the weakest guarantee that still fits the use case; stronger guarantees often need extra copies or staging buffers.

GuaranteePromiseTechniqueExample
BasicInvariants hold; no leaks; state may changeRAII; commit-or-rollback for critical partsPush back that may grow capacity
StrongAll-or-nothing; if throw then no observable changeCopy-and-swap; build-then-commitTransaction-like updates
No-throwOperation will not thrownoexcept moves; preallocationDestructors and move swaps
⚠️ Mark moves noexcept when possible. Containers choose move over copy during reallocation only if the move cannot throw; this improves performance.

Providing strong guarantees

Stage new state off to the side; commit with non-throwing operations. If commit can fail, you do not have the strong guarantee, so revise the plan or document the weaker promise.

struct Bag {
  std::vector<int> xs;

  void append_all(const std::vector<int>& more) {
    auto tmp = xs;                 // strong guarantee staging
    tmp.insert(tmp.end(), more.begin(), more.end());
    xs.swap(tmp);                  // commit is no-throw
  }
};

Error handling

std::error_code is a lightweight status object with a numeric value and a category pointer. std::error_condition groups codes into portable meanings such as errc::permission_denied. std::system_error is an exception that carries an error_code. Use codes for expected failures on hot paths; use exceptions for rare failures or when you need stack unwinding.

Working with error_code

APIs can report into an out-parameter of type std::error_code&. Callers test and branch without throwing.

#include <system_error>
#include <fstream>

bool read_file(const std::string& path, std::string& out, std::error_code& ec) {
  std::ifstream f(path);
  if (!f) { ec = std::make_error_code(std::errc::no_such_file_or_directory); return false; }
  out.assign(std::istreambuf_iterator<char>(f), {});
  ec.clear();
  return true;
}

std::string text;
std::error_code ec;
if (!read_file("cfg.txt", text, ec)) {
  // handle using ec.value() and ec.category()
}

Mapping to conditions

Categories map to conditions so portable code can branch on common meanings. This separates platform details from logic that decides how to react.

using std::errc;
std::error_code ec = std::make_error_code(errc::permission_denied);
if (ec == std::make_error_condition(errc::permission_denied)) {
  // request elevated rights or skip
}
💡 Prefer returning a rich status that includes context such as path or operation; combine this with error_code so logs tell a clear story.

Throwing system_error with codes

When policy favors exceptions, wrap an error_code inside std::system_error. The handler can still inspect the code while enjoying stack unwinding and RAII cleanup.

void open_or_throw(const std::string& path) {
  // imagine a failing open that produced ec
  std::error_code ec = std::make_error_code(std::errc::permission_denied);
  throw std::system_error{ec, "open failed: " + path};
}

Monadic flow

Value-centric error handling keeps control flow explicit. std::optional<T> carries either a value or no value; this fits lookups and parses where no further context is needed. std::expected<T, E> (C++23) carries either a value or an error type; this fits pipelines that need per-step error information without exceptions.

Using optional for absence

Return optional when failure has no details to report. Callers check has_value() or use boolean tests; then either use the value or choose a fallback.

#include <optional>

std::optional<int> to_int(const std::string& s) {
  // … parse; return {} on failure
  return 17;
}

auto oi = to_int("17");
int v = oi.value_or(0);

Composing steps with expected

expected supports expressive flows. Use and_then to continue on success and or_else to handle errors. The error type can be error_code or a bespoke struct with context.

#include <expected>
#include <system_error>

using R = std::expected<int, std::error_code>;

R read_config_value() {
  // … return std::unexpected(make_error_code(std::errc::io_error)) on failure
  return 7;
}

R double_value(int x) { return x * 2; }

R pipeline() {
  return read_config_value()
    .and_then(double_value);
}

auto r = pipeline();
if (r) {
  // use *r
} else {
  // inspect r.error()
}
⚠️ expected moves or copies values as they pass through steps. Keep values small or store them via smart pointers to avoid heavy copies when needed.

When to choose value flow over exceptions

Prefer optional or expected when failure is common and part of normal control flow, such as cache misses or user cancellation. Prefer exceptions when failure is rare and you want to separate the hot path from error plumbing. Mixing is valid; confine strategies at boundaries and translate once.

Designing APIs for failure

APIs should make failure behavior obvious. Choose one primary error strategy per layer; document it clearly. Preserve invariants at all times; never leak. Make common success paths simple and branch-free; make failure paths descriptive and cheap to check.

Pick a clear strategy per boundary

Libraries that cross process or thread boundaries often prefer status returns for predictability. In-process libraries for business logic often prefer exceptions for clarity. Translate at the edge and keep the interior consistent.

Attach context to errors

Error messages without context waste time. Include operation, parameters, and environment hints. Use small structs for expected<T,E> errors so callers can act programmatically.

struct IoError {
  std::error_code ec;
  std::string path;
};

using RFile = std::expected<std::string, IoError>;
💡 Treat log lines as mini bug reports; include enough detail that a future reader can reproduce the problem.

Keep destructors and swaps non-throwing

Destructors should be noexcept. Swaps and moves should be conditionally noexcept so containers can optimize. If a destructor must detect an error, record it for later inspection rather than throwing during unwinding.

struct Channel {
  ~Channel() noexcept { /* flush best effort; record status if needed */ }
  Channel(Channel&&) noexcept = default;
  Channel& operator=(Channel&&) noexcept = default;
};

Document guarantees and test them

State the exception safety level for each public operation. Write tests that force throws from collaborators to verify you kept the promise. Inject failures with test doubles or fault points.

// Pseudocode for a fault injection test
// arrange collaborator to throw during write …
// call operation and assert the object's observable state is unchanged

Choose low-friction defaults

Make the easy path safe by default. Use make_unique and make_shared. Prefer string_view for read-only parameters. Avoid surprise allocations in error paths. Keep handler code short, local, and consistent across the codebase.

Chapter 21: Concurrency and Parallelism

Modern C++ offers portable building blocks for running work in parallel, communicating safely between threads, and reasoning about memory effects. This chapter orients you around the core primitives and patterns, then shows techniques for composing higher level task flows. The aim is correctness first, performance second, and clarity always.

Threads and thread management

A thread is an independent path of execution that runs alongside others. In C++, std::thread launches a function concurrently, while std::jthread (C++20) adds cooperative cancellation with std::stop_token and automatically joins in its destructor. Prefer std::jthread for simpler lifetime management; use std::thread when you need precise control and will remember to join or detach.

Creating and joining threads

Construct a thread with a callable and arguments. Always finish with join (wait for completion) or detach (let it run independently), since an unjoined std::thread at destruction is a program error. With std::jthread you get RAII joining automatically, which reduces footguns.

#include <thread>
#include <iostream>

void work(int id) {
  std::cout << "hello from " << id << "\n";
}

int main() {
  std::thread t(work, 1);
  // do other things...
  t.join();  // required for std::thread
}

Cooperative cancellation

Cooperative cancellation uses tokens. A thread checks stop_token periodically and exits promptly when requested. This avoids forcibly terminating threads and keeps invariants intact.

#include <thread>
#include <stop_token>
#include <chrono>

void ticking(std::stop_token st) {
  while (!st.stop_requested()) {
    // do a unit of work
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
  }
}

int main() {
  std::jthread jt(ticking);
  // later...
  jt.request_stop();  // cooperative
} 
💡 Prefer std::jthread when possible. It joins automatically, accepts a stop_token in the first parameter slot, and removes a common class of lifetime bugs.

Thread affinity, counts, and structure

Use std::thread::hardware_concurrency() as a heuristic for the number of worker threads. It reports a hint, not a promise. Structure your design so that thread counts are configurable, since the best number depends on I/O versus CPU mix and the runtime environment.

Mutexes, locks, and condition variables

Shared data accessed by multiple threads needs synchronization. A mutex provides mutual exclusion. Locks manage mutex ownership using RAII. Condition variables allow threads to sleep until a predicate becomes true, which avoids busy waiting.

Protecting shared state with a mutex

Wrap reads and writes to shared data with a lock. Use std::scoped_lock for simple critical sections or std::unique_lock when you need to unlock and relock around waits.

#include <mutex>
#include <vector>

std::mutex m;
std::vector<int> data;

void push(int x) {
  std::scoped_lock lk(m);
  data.push_back(x);
}

Waiting with a condition variable

A condition variable pairs with a mutex and a predicate. Always check the predicate in a loop, since spurious wakeups can occur. Use wait or wait_for with a lambda that inspects the current state.

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

std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool done = false;

void producer() {
  for (int i = 0; i < 10; ++i) {
    {
      std::lock_guard lg(m);
      q.push(i);
    }
    cv.notify_one();
  }
  {
    std::lock_guard lg(m);
    done = true;
  }
  cv.notify_all();
}

void consumer() {
  for (;;) {
    std::unique_lock lk(m);
    cv.wait(lk, [] { return !q.empty() || done; });
    if (!q.empty()) {
      int v = q.front(); q.pop();
      lk.unlock();
      // process v
    } else if (done) {
      break;
    }
  }
}
⚠️ Never hold a lock while calling into unknown code. Keep critical sections small to reduce contention and the risk of deadlocks.

Deadlocks, ordering, and std::scoped_lock

Deadlock arises when threads acquire multiple locks in different orders. Either enforce a global lock ordering or acquire multiple mutexes at once using std::scoped_lock a{m1, m2} which uses a deadlock avoidance protocol. Release locks before expensive work to shorten contention windows.

Futures, promises, and std::async

Futures carry results from asynchronous operations. A std::promise sets the value, and a paired std::future retrieves it. std::async creates a future-producing task without manual thread management. Futures propagate exceptions to the waiting side, which simplifies error handling.

One-shot result channels

Use a promise and future when a single producer needs to deliver a single value (or exception) to a consumer. The consumer calls get() to block until the result is ready, or uses timed waits with wait_for.

#include <future>
#include <string>

std::string compute() { return "result"; }

int main() {
  std::promise<std::string> p;
  std::future<std::string> f = p.get_future();
  std::thread t([pr = std::move(p)]() mutable {
    try { pr.set_value(compute()); }
    catch (...) { pr.set_exception(std::current_exception()); }
  });
  auto s = f.get();  // throws if the worker threw
  t.join();
}

std::async launch policies

std::async may run the task on a new thread or defer execution until you call get(). The policy is a hint unless you pass std::launch::async (guarantees concurrent execution) or std::launch::deferred (runs in the waiting thread). Treat std::async as a convenience for fire and wait workflows.

#include <future>
#include <numeric>
#include <vector>

long sum(const std::vector<int>& v) { 
  return std::accumulate(v.begin(), v.end(), 0L); 
}

int main() {
  std::vector<int> v(1'000'000, 1);
  auto fut = std::async(std::launch::async, sum, std::cref(v));
  long s = fut.get();
}
💡 Futures are move only. Store them in containers by moving, and avoid copying which is intentionally disabled.

Packaging work

std::packaged_task wraps a callable so that invoking it sets a future. This is handy for thread pools and work queues, since you can move the task into a worker and later wait on the paired future for the result.

Atomics and memory order

Atomics allow threads to read and write shared variables without data races. Each operation has a memory ordering that constrains how operations appear across cores. When in doubt, use the default memory_order_seq_cst, which is simple and correct. Weaker orders can improve performance when used with care.

Basic atomic operations

Integral atomics support load, store, read-modify-write operations like fetch_add. The default ordering is sequentially consistent. Start there, measure, then consider relaxed or acquire-release only if you fully understand the visibility rules.

#include <atomic>

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

void inc() { counter.fetch_add(1); }
int get() { return counter.load(); }

Acquire, release, and fences

Acquire on loads pairs with release on stores to establish happens-before edges. This ensures that writes before the release become visible to reads after the acquire. Fences are lower level barriers; prefer operation-specific orderings on atomics rather than manual fences unless you have a proven need.

OrderEffectTypical use
memory_order_relaxedNo ordering, atomicity onlyIndependent counters
memory_order_acquirePrevents later reads or writes from moving beforeLoad side of publish
memory_order_releasePrevents earlier reads or writes from moving afterStore side of publish
memory_order_acq_relAcquire on read and release on writeRead-modify-write
memory_order_seq_cstTotal order across threadsSafe default
⚠️ Do not mix atomic and non-atomic accesses to the same object. That is a data race and yields undefined behavior.

Lock-free queries and progress

is_lock_free() tells you whether an atomic is implemented without locks on the current platform. Lock free does not mean wait free; there may still be livelock when many threads contend. Choose algorithms with clear progress guarantees where it matters.

Task based parallelism and executors

Task based design focuses on units of work and dependencies rather than manual thread lifetimes. In C++ today you can build task graphs with a thread pool, package callables as tasks, and connect them via futures or queues. Standard algorithms with the std::execution policies run many loops in parallel without custom threading.

Parallel algorithms

Pass an execution policy to eligible algorithms to enable parallel or vectorized execution. Use par for multithreaded execution and par_unseq to allow parallelism and unsequenced vectorization when safe. The body must be free of data races and side effects that depend on order.

#include <algorithm>
#include <execution>
#include <vector>

int main() {
  std::vector<int> v(1'000'000, 1);
  std::for_each(std::execution::par, v.begin(), v.end(), [](int& x){ x += 2; });
}

A minimal thread pool for tasks

A thread pool amortizes thread creation by keeping a fixed set of workers that pull tasks from a queue. Push packaged tasks into the queue, then wait on their futures. This pattern scales well for many short tasks and keeps control centralized.

#include <vector>
#include <thread>
#include <future>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>

class thread_pool {
  std::vector<std::jthread> workers;
  std::mutex m;
  std::condition_variable_any cv;
  std::queue<std::function<void()>> q;

public:
  explicit thread_pool(std::size_t n = std::thread::hardware_concurrency()) {
    for (std::size_t i = 0; i < n; ++i) {
      workers.emplace_back([this](std::stop_token st){
        for (;;) {
          std::function<void()> job;
          {
            std::unique_lock lk(m);
            cv.wait(lk, st, [this]{ return !q.empty(); });
            if (st.stop_requested()) return;
            job = std::move(q.front()); q.pop();
          }
          job();
        }
      });
    }
  }

  ~thread_pool() {
    for (auto& w : workers) w.request_stop();
    cv.notify_all();
  }

  template <class F, class... A>
  auto submit(F&& f, A&&... a) {
    using R = std::invoke_result_t<F, A...>;
    std::packaged_task<R()> task(std::bind(std::forward<F>(f), std::forward<A>(a)...));
    auto fut = task.get_future();
    {
      std::scoped_lock lk(m);
      q.emplace([t = std::move(task)]() mutable { t(); });
    }
    cv.notify_one();
    return fut;
  }
};

Pipelines, batching, and backpressure

Real systems often form pipelines: stage A parses, stage B transforms, stage C writes. Use bounded queues between stages to prevent overload. When a queue fills, block producers or shed load to maintain latency targets. Batching small tasks reduces overhead and cache misses.

💡 Measure before tuning. Concurrency bugs hide behind timing, and performance myths are common. Add timing, counters, and contention metrics to verify that changes help.

Executors and senders & receivers

Various proposals define executors and senders & receivers, which standardize how work is submitted and how completion is signaled. Toolkits and libraries already provide similar abstractions. The direction favors explicit scheduling and typed completion. When these interfaces land in your toolchain you can adapt pools and algorithms to them with small wrappers.

Concurrency in C++ rewards discipline: pick simple primitives, keep regions small, publish state carefully, and prefer tasks over manual thread choreography for most designs.

Chapter 22: Coroutines

Coroutines in C++ suspend and resume functions at well defined points so that work can be interleaved without extra threads. The language keywords co_await, co_yield, and co_return pair with user types that model the protocol. This chapter explains the building blocks, then shows how to design libraries that feel natural and safe.

The coroutine model and promise types

When a function uses co_await, co_yield, or co_return, the compiler transforms it into a state machine. The return type defines an inner promise_type that controls creation, suspension, completion, and exception flow. A std::coroutine_handle<P> is the opaque control handle for a coroutine whose promise is P.

Lifecycle and involved types

The transformed function allocates a coroutine frame that stores locals and bookkeeping. The promise is constructed first; the initial suspend point runs next; then the body executes and may suspend many times. On final suspend the coroutine either destroys itself or stays alive until the handle is destroyed. Exceptions are routed to promise_type::unhandled_exception.

ElementRole
return_objectConcrete object returned by the coroutine function
initial_suspend()Choose to start suspended or run immediately
final_suspend()Choose to suspend at end so observers can react
get_return_object()Construct the object the caller receives
return_void or return_valueSupply the result at completion
unhandled_exception()Capture exceptions to deliver later

A minimal generator<T> shell

This sketch shows the promise hooks and how co_yield communicates a value. Real implementations add iteration support and memory management; the outline highlights the moving parts.

#include <coroutine>
#include <optional>

template <class T>
struct generator {
  struct promise_type {
    std::optional<T> current;

    generator get_return_object() { return generator{handle_type::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    std::suspend_always yield_value(T v) noexcept { current = std::move(v); return {}; }
    void return_void() noexcept {}
    void unhandled_exception() { ex = std::current_exception(); }

    std::exception_ptr ex{};
  };

  using handle_type = std::coroutine_handle<promise_type>;

  explicit generator(handle_type h) : h(h) {}
  generator(generator&& o) noexcept : h(std::exchange(o.h, {})) {}
  ~generator() { if (h) h.destroy(); }

  bool next() {
    if (!h || h.done()) return false;
    h.resume();
    if (h.promise().ex) std::rethrow_exception(h.promise().ex);
    return !h.done();
  }

  T& value() { return *h.promise().current; }

private:
  handle_type h{};
};
💡 The return type owns the std::coroutine_handle by policy. Decide who calls destroy() and document that clearly.

Handles and frame ownership

A handle is a lightweight pointer to the frame; it does not own memory by itself. Destruction policy lives outside the handle. Choose one of two styles: the return object destroys in its destructor, or an external scheduler stores handles and controls lifetime. Both styles are valid when the contract is explicit.

Generators and async workflows

Generators produce a sequence lazily with co_yield. Async workflows represent pending results that complete later. Both are coroutines, yet they use different promise contracts and await behaviors.

Iterating a generator

A friendly generator supports range based for. The iterator adaptor drives resume() on increment and reads the yielded value on dereference. The example emits squares on demand.

#include <coroutine>
#include <utility>

generator<int> squares(int n) {
  for (int i = 1; i <= n; ++i) {
    co_yield i * i;
  }
}

A simple task<T> for async

An async task holds either a result or an exception and can be awaited by other coroutines. The await_suspend hook decides how to schedule the continuation; for now we resume inline to keep the example compact.

#include <coroutine>
#include <exception>
#include <optional>

template <class T>
struct task {
  struct promise_type {
    std::optional<T> value;
    std::exception_ptr ex;

    task get_return_object() { return task{handle::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_value(T v) noexcept { value = std::move(v); }
    void unhandled_exception() noexcept { ex = std::current_exception(); }
  };

  using handle = std::coroutine_handle<promise_type>;
  explicit task(handle h) : h(h) {}
  task(task&& o) noexcept : h(std::exchange(o.h, {})) {}
  ~task() { if (h) h.destroy(); }

  bool await_ready() const noexcept { return false; }
  void await_suspend(std::coroutine_handle<> cont) noexcept { cont.resume(); }
  T await_resume() {
    if (h.promise().ex) std::rethrow_exception(h.promise().ex);
    return std::move(*h.promise().value);
  }

private:
  handle h{};
};

task<int> compute() {
  co_return 42;
}
⚠️ Resuming continuations inline is only for teaching. Real systems hand off to a scheduler so that blocking work does not wedge the caller.

Composing async coroutines

Awaiting a task<T> yields a value and propagates exceptions. This makes request pipelines read like straight line code. Add timeouts, cancellation, and retries around awaits to complete the story.

task<int> doubled() {
  int v = co_await compute();
  co_return v * 2;
}

Awaitables and schedulers

An awaitable is any type that provides the await trio: await_ready, await_suspend, and await_resume. A scheduler decides where and when a suspended coroutine continues. Together they route work to threads, event loops, or I/O completion ports.

The awaitable contract

The contract is small and powerful. If await_ready returns true, the coroutine does not suspend. Otherwise await_suspend receives the awaiting handle and can arrange resumption later. await_resume returns the result or throws to signal failure.

struct immediate_int {
  int v;
  bool await_ready() const noexcept { return true; }
  void await_suspend(std::coroutine_handle<>) noexcept {}
  int await_resume() const noexcept { return v; }
};

Posting continuations to a thread pool

Schedulers usually keep a queue of continuations. When an event fires, the scheduler calls resume() on the saved handle from a worker thread. The small adapter below posts the continuation through a user supplied function.

#include <functional>

struct post_awaitable {
  std::function<void(std::coroutine_handle<>)> post;

  bool await_ready() const noexcept { return false; }
  void await_suspend(std::coroutine_handle<> h) const { post(h); }
  void await_resume() const noexcept {}
};
💡 A scheduler can be passed by reference, pointer, handle, or captured in the awaitable. Pick one convention and keep it consistent across your codebase.

Bridging callbacks to co_await

Legacy APIs signal completion via callbacks. Wrap them by capturing a std::coroutine_handle<> and resuming it inside the callback. Deliver results through shared state that await_resume reads. Add an atomic flag to guard against double resume and to handle cancellation correctly.

Designing coroutine friendly libraries

Libraries should expose ergonomic types that fit the coroutine protocol and integrate with scheduling. Focus on clear ownership, predictable cancellation, structured completion, and explicit context switching. Prefer value oriented APIs with small awaitables over giant framework objects.

Ownership and cancellation

Decide who owns the coroutine frame and who can cancel. Provide a token or stop source that propagates through nested awaits. When cancelled, complete the awaitable promptly and report a distinct error so callers can separate cancellation from failure.

Error handling and results

Coroutines can propagate exceptions through await_resume, or they can return tagged results such as expected<T,E>. Choose one style for a library and apply it everywhere; mixing strategies increases cognitive load.

Scheduler aware APIs

Accept an execution context parameter or capture one at construction. Document where continuations run. Many bugs arise from code that resumes on surprising threads. Provide schedule() or via(...) helpers to move execution between contexts when needed.

Interoperability contracts

Ensure awaitables are await-transform friendly. Provide operator co_await so callers can customize behavior. Keep awaiters trivially movable and noexcept where possible so that compilers generate lean state machines.

Testing and diagnostics

Add deterministic tests by using fake schedulers that run continuations in a controlled order. Log suspend and resume with handle addresses to trace flows. Provide counters for posted tasks, in flight operations, and cancellations to spot leaks early.

Coroutines offer direct expression of asynchronous logic with fewer layers. With clear ownership, simple awaitables, and scheduler aware design, coroutine based code stays readable and efficient.

Chapter 23: I/O Streams and Files

C++ streams handle text, binary data, and filesystem work through a layered model built on buffers, formatters, and device abstractions. This chapter tours the core stream families, shows how to achieve high performance with buffering, covers simple serialization patterns, and finishes with modern filesystem utilities. The goal is command of the tools that move data between your program and the outside world.

Stream classes and formatting

Stream classes follow a family pattern. std::istream consumes characters, std::ostream produces characters, and std::iostream combines both. Derivatives such as ifstream, ofstream, stringstream, and ostringstream plug different data sources into the same interface. Formatting uses manipulators like std::setw, std::setprecision, and std::boolalpha which adjust output state until changed again.

Text formatting and manipulators

Manipulators work by toggling flags or updating width and precision in the underlying ios_base. Since stream state persists, compose formatting carefully and reset when needed. A few well placed manipulators can turn raw values into readable reports.

#include <iostream>
#include <iomanip>

int main() {
  double x = 3.14159265;
  std::cout << std::fixed << std::setprecision(3) << x << "\n";
}
💡 Use std::format or std::format_to for structured output when streams become cluttered. Format strings keep layout in one place.

Error states and exception modes

Streams track failure through flags such as failbit, eofbit, and badbit. Check good() or inspect specific bits to distinguish end of file from invalid input. Enabling exceptions for failbit or badbit can simplify error paths for strict code.

Buffered I/O and performance

Every stream uses a buffer under the hood. The buffer absorbs small writes, reduces system calls, and improves throughput. std::filebuf handles OS level operations and exposes knobs for tuning. You can replace a buffer to redirect a standard stream or provide custom behavior for logging or filtering.

Understanding streambuf layers

A streambuf defines get and put areas that the stream uses during extraction and insertion. Overriding underflow or overflow allows full control of refills and flushes. This is an advanced path yet essential for specialized devices.

Manual buffering and large reads

When processing files in bulk, prefer read and write on istream and ostream with sizable buffers. Chunking reduces overhead and keeps inner loops tight, especially in binary workloads.

#include <fstream>
#include <vector>

int main() {
  std::ifstream in("data.bin", std::ios::binary);
  std::vector<char> buf(65536);
  while (in.read(buf.data(), buf.size()) || in.gcount() > 0) {
    // process buf.data() … in.gcount()
  }
}
⚠️ Flushing too often slows output. Let buffers flush naturally or call flush() only where correctness demands it.

Binary I/O and serialization basics

Binary I/O writes raw bytes without formatting. You can serialize structs, primitive values, or length prefixed sequences. Be cautious about alignment, padding, and endianness since layouts vary across platforms.

Reading and writing binary blocks

Use read and write for simple blocks. They exchange bytes exactly as they appear in memory. Consider explicit control of endianness when storing for long term or cross platform sharing.

#include <fstream>

struct record {
  int id;
  double value;
};

int main() {
  record r{5, 2.5};
  std::ofstream out("rec.bin", std::ios::binary);
  out.write(reinterpret_cast<char*>(&r), sizeof(r));
}

Simple tagged formats

A tagged format writes a type code or length before data. This makes decoding safer because readers know how many bytes to consume. Even a small tag system prevents misaligned reads and helps versioning.

Text based serialization

Text formats like JSON or CSV use streams easily. You can generate text with std::format or stream operators and parse with tokenized reads. Text is more verbose yet more portable and debuggable than raw binary.

Filesystem paths, iteration, and metadata

The std::filesystem library offers portable paths, directory traversal, and metadata queries. It replaces ad hoc path manipulation with clear operations that respect platform rules. Paths manage separators, encodings, and native conversions automatically.

Working with std::filesystem::path

A path stores components in a normalized form. Use / or operator/= to append segments. Query extensions, filenames, and parents with dedicated functions. Stay in the path domain until absolutely necessary to convert to strings.

#include <filesystem>
#include <iostream>

int main() {
  std::filesystem::path p = "logs";
  p /= "2025";
  p /= "run.txt";
  std::cout << p.string() << "\n";
}

Directory iteration

Use directory_iterator for one level, or recursive_directory_iterator for deep traversal. Entries expose type queries, sizes, timestamps, and permissions. This makes file utilities such as cleaners or indexers straightforward.

#include <filesystem>

int main() {
  for (const auto& e : std::filesystem::directory_iterator(".")) {
    // process e.path() …
  }
}
💡 When walking large trees use options::skip_permission_denied to ignore inaccessible nodes cleanly.

Metadata and filesystem operations

Metadata queries such as file_size, last_write_time, and is_regular_file reveal structure without opening files. Operations such as copy, rename, and remove_all modify trees safely. Wrap calls that can fail with std::error_code overloads when you need to recover from errors without throwing.

Resource safety for I/O

I/O APIs interact with the operating system so resource safety matters. Streams own file descriptors or other handles and release them in destructors. Always check that a file opened successfully and fail fast when it does not. Use RAII wrappers for any raw file handles provided by external libraries.

Ensuring handles close correctly

A file closes automatically when its stream object goes out of scope. This behavior prevents many leaks. For custom handles, wrap the resource in a small struct whose destructor calls the correct close function. This keeps lifetimes aligned with scopes.

Recovering from failures

I/O errors happen for reasons outside your control. Handle them predictably by checking states, using error_code overloads, and validating assumptions such as file size or format signatures. Well structured error paths keep your program robust even in cluttered environments.

Streams, buffers, binary formats, and filesystem utilities form a steady toolkit. With careful handling of state and errors you can build pipelines that process data reliably and efficiently.

Chapter 24: Modules and Header Modernization

C++ modules shorten build times, reduce macro bleed, and give you cleaner interfaces. This chapter shows how modules work, how to write and use them, how to migrate away from headers and include guards, and how to wire everything to CMake so builds stay fast and correct.

Why modules improve builds

Traditional headers are textual inclusion. Every translation unit repeats the same parsing, macro expansion, and template instantiation surface for included headers. Modules flip the model by compiling an interface once into a binary module interface file, then importing that result. This cuts duplicate work, shrinks dependency surfaces, and blocks accidental macro leakage. The effect is faster incremental builds and fewer surprising name collisions.

From textual inclusion to compiled interfaces

With headers, the preprocessor pastes file contents into each consumer. With modules, the compiler reads a compiled description of names and types. Toolchains cache this compiled form so multiple source files can reuse it without reparsing. Templates still instantiate where needed, but discovery and lookups travel through a precompiled graph instead of piles of text.

Isolation and hygiene

Module interfaces export only what you choose. By default, names are hidden. Macros do not cross the module boundary through import. You still can use traditional headers where it makes sense, but keeping public APIs inside modules reduces accidental exposure and tight coupling.

💡 Start by modularizing leaf libraries that many targets include. You get maximum parse-time savings where duplication is highest.

A minimal example

This tiny library builds once, then imports quickly everywhere it is used.

export module math;
export int add(int a, int b) { return a + b; }
import math;
#include <iostream>
int main() {
  std::cout << add(2, 3) << "\n";
}

export, import, and partitions

Module syntax is small and direct. You declare a primary interface unit, decide which declarations to export, and optionally split the interface into partitions for organization. Consumers import the module or a partition by name. Private implementation units can attach to the same module without exporting anything.

Declaring a primary interface unit

A primary interface unit begins with export module and may export declarations selectively. Only exported declarations are visible to importers. Keep implementation details unexported in the same file or in private units.

export module util;
export struct version { int major, minor, patch; };
int helper(); // not exported, only visible within this module unit

Controlling visibility with export

You can mark whole declarations or namespaces as exported. Use narrow exports to keep ABI small and intent clear.

export module strings;
export namespace api {
  export const char* name();
}
namespace detail {
  const char* impl();
}

Importing modules and header units

Consumers use import. Many standard library headers can be imported as header units if your toolchain supports it. Prefer the named standard module when available.

import strings;   // your module
import <vector>;  // header unit form, if enabled
// or, when supported by your standard library:
// import std;    // single import for standard facilities
⚠️ Mixing #include and import requires careful ordering. Perform all import directives before declarations and before includes that depend on imported names.

Interface and implementation partitions

Large modules can be split into partitions. Interface partitions are exported and can be imported by consumers. Implementation partitions are internal and only visible to other units of the same module.

export module img;
// exported interface partition
export module img:io;
export void save_png(const char* path);

// implementation partition
module img:codec;
int compress(const unsigned char* data, int size);

// primary interface imports its partitions
export import :io; // makes img:io part of public API
// consumer of the public partition
import img:io;
int main() { save_png("out.png"); }

Private module fragments

A private module fragment lets you include legacy headers without leaking their macros or names to importers. Declarations in the private fragment are not exported.

export module net;
export struct socket { int fd; };
// start private region
module :private;
#include <sys/socket.h>
#include <sys/types.h>
// … internal glue that stays private

Migrating from headers and include guards

Modernization is smoother when you move one public surface at a time. Keep old headers as thin forwarders during the transition. Replace include guards with module names. Use private fragments for any compatibility includes you still need.

Typical header to module conversion

Suppose you start with a traditional header and source pair. Convert to a primary interface unit and, if needed, a private implementation unit. Maintain the old header as a forwarding shim for downstream code during a deprecation window.

// Before: math.hpp
#ifndef MATH_HPP
#define MATH_HPP
int add(int, int);
#endif
// After: math.ixx (primary interface)
export module math;
export int add(int a, int b);
// After: math_impl.cpp (implementation unit)
module math;
int add(int a, int b) { return a + b; }
// Optional shim: math.hpp
#pragma once
import math;
using ::add;  // surface the same symbol for legacy consumers
💡 Keep module names short and stable. Map names to directory layout, for example image/filters becomes module name image.filters.

Guard rails for a staged rollout

Adopt modules on library boundaries first, then flow inward. Turn on higher warning levels and sanitizers for module units while keeping the rest of the tree unchanged. Add CI tasks that validate that all import directives appear before declarations and that no transitive text includes sneak in via private fragments.

Comparing headers and modules

This compact table summarizes the practical differences that matter during migration.

AspectHeadersModules
Build costReparse in every TUCompile once, reuse
VisibilityEverything includedOnly what you export
MacrosLeak across includesDo not cross import
Order sensitivityHigh with includesimport before declarations
ToolingText basedDependency graph with BMIs

Modules with CMake

CMake can model C++ module dependency graphs and ask the compiler to generate and consume the required artifacts. Modern versions support module file sets so you declare which sources are interfaces, partitions, and implementations, and let the build system schedule them correctly.

Declaring module file sets

Use target_sources with a C++ modules file set to mark interface units and partitions. Keep implementation units in regular sources. This helps CMake generate the right build order and scan dependencies.

cmake_minimum_required(VERSION 3.28)
project(mod_demo LANGUAGES CXX)
add_library(mathlib)

target_sources(mathlib
  PUBLIC
    FILE_SET cxx_modules TYPE CXX_MODULES FILES
      src/math.ixx
      src/img.ixx
      src/img-io.ixx   # exported interface partition
  PRIVATE
    src/math_impl.cpp
    src/img_codec.cpp  # implementation partition unit
)

target_compile_features(mathlib PUBLIC cxx_std_20)

Consuming modules from executables

Executables that import your modules only need a link dependency. CMake wires include paths and module search paths. The compiler will find and consume the produced interface artifacts.

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mathlib)
target_compile_features(app PRIVATE cxx_std_20)
⚠️ Do not hand craft BMI paths. Compilers and CMake coordinate where binary module interface files live. Hardcoding locations makes the build brittle across generators and platforms.

Header units and the standard library

If your toolchain supports header units, you can enable them with the right compile flags and then import system headers like <vector>. Prefer the named standard module when your standard library provides it. Keep such changes within module units to avoid surprising legacy translation units.

Incremental migration strategy in CMake

Keep existing targets and gradually introduce a module-capable library alongside header-based ones. Use interface libraries to bridge compile features and flags. Once consumers switch to import, retire the shim headers and remove the bridge targets cleanly.

Chapter 25: Numerics, Randomness, and Time

C++ gives you precise control over numbers, random sampling, and time measurement. This chapter explains floating point behavior, fixed width integers and limits, robust random number facilities, and the modern <chrono> tools for clocks, durations, time points, and formatting.

Floating point behavior and pitfalls

Floating point types model real numbers with finite precision. Operations round to the nearest representable value, which means algebraic identities do not always hold. Correct code accepts small error, compares with tolerances, and uses the right functions for classification and rounding.

Representation and rounding

float, double, and long double store numbers as sign, exponent, and significand. Each operation rounds to fit that model. Small gaps between adjacent representable values get larger as magnitudes grow, so adding a tiny number to a huge number may do nothing.

double x = 1e16;
double y = 1.0;
double z = (x + y) - x;  // z may be 0.0 because y was lost to rounding

Equality and tolerances

Check closeness, not exact equality. Use absolute tolerances for small magnitudes and relative tolerances for large magnitudes. Blend both for general code.

bool nearly_equal(double a, double b, double rel=1e-12, double abs=1e-15) {
  double diff = std::fabs(a - b);
  return diff <= std::max(rel * std::max(std::fabs(a), std::fabs(b)), abs);
}
💡 Prefer std::fma when available to compute a * b + c with a single rounding; this improves stability in many kernels.

Special values and classification

IEEE arithmetic supports infinities and NaNs. Use classification functions to branch safely and avoid undefined behavior in edge cases.

#include <cmath>
double q = 0.0, r = -0.0;
bool s1 = std::isnan(0.0/0.0);  // true
bool s2 = std::isinf(1.0/0.0);  // true
bool s3 = std::signbit(r);      // true for negative zero
⚠️ NaN never compares equal, not even to itself. Use std::isnan to detect it. Propagation rules vary by operation, so treat NaN as contagious.

Stable summation

Summing many values can accumulate error. A compensated algorithm reduces drift by tracking lost low order bits and feeding them back on the next step.

double kahan_sum(const std::vector<double>& v) {
  double sum = 0.0, c = 0.0;
  for (double x : v) {
    double y = x - c;
    double t = sum + y;
    c = (t - sum) - y;
    sum = t;
  }
  return sum;
}

Numeric limits and fixed width integers

When you need exact sizes and boundaries, use fixed width types and query their properties through std::numeric_limits. This keeps code portable and correct across platforms and compilers.

Choosing integer widths

Pick the smallest type that holds the full range of your data; this reduces memory traffic and keeps intent clear. Use signed types when negative values occur, otherwise prefer unsigned only for bit masks and modular arithmetic.

FamilyHeaderExamplesTypical use
Fixed width<cstdint>int8_t, uint32_t, int64_tBinary formats, hashes
Fast width<cstdint>int_fast16_t, …Speed on given platform
Least width<cstdint>int_least32_t, …Minimum bits with minimal size
Size types<cstddef>size_t, ptrdiff_tIndices and pointer diffs

Querying limits and properties

Use std::numeric_limits<T> to discover min, max, epsilon, and behavior flags. This avoids hardcoded constants and covers floating point details.

#include <limits>
auto maxu32 = std::numeric_limits<uint32_t>::max();
auto epsd  = std::numeric_limits<double>::epsilon();
bool is_ieee = std::numeric_limits<double>::is_iec559;  // IEEE 754?

Safe conversions and saturation

Converting between widths can overflow or truncate. Validate ranges before casting, or apply a saturating clamp for defensive code.

#include <algorithm>
uint8_t to_u8_saturate(int x) {
  return static_cast<uint8_t>(std::clamp(x, 0, 255));
}

Random number engines and distributions

The <random> library separates engines that produce raw bits from distributions that shape values. You seed an engine, then feed it through a distribution that models the desired behavior.

Engines and seeding

std::mt19937 is a common workhorse. Seed once from std::random_device or from a fixed seed for reproducible experiments. Keep engines in objects or thread local storage to avoid global contention.

#include <random>
std::mt19937 make_rng() {
  std::random_device rd;
  std::seed_seq seq{rd(), rd(), rd(), rd()};
  return std::mt19937{seq};
}

Uniform, normal, and discrete choices

Apply distributions to map engine output into meaningful values. Uniform distributions sample ranges, normal distributions model bell curves, and discrete distributions pick from weighted buckets.

auto rng = make_rng();
std::uniform_int_distribution<int>     die(1, 6);
std::uniform_real_distribution<double> unit(0.0, 1.0);
std::normal_distribution<double>       noise(0.0, 1.0);
std::discrete_distribution<int>        pick({1, 3, 1});  // weights
int d = die(rng);
double x = unit(rng);
double n = noise(rng);
int ix = pick(rng);
💡 Prefer one engine per thread. Use thread_local std::mt19937 rng or pass engines down the call chain to keep results predictable and fast.

Reproducibility and serialization

Deterministic runs are vital for tests. Save and restore engine state; then rerun with the same sequence later.

std::mt19937 rng = make_rng();
std::string state;
{
  std::ostringstream os;
  os << rng;
  state = os.str();
}
int a = std::uniform_int_distribution<int>(0, 9)(rng);
// restore
{
  std::istringstream is(state);
  is >> rng;
}
int b = std::uniform_int_distribution<int>(0, 9)(rng); // matches prior path after restore

chrono clocks, durations, and time points

<chrono> measures time with strong types. A clock tells the current time, a duration represents a length of time, and a time_point binds a clock to a specific instant.

Clocks you will use

system_clock tracks civil time and can jump when the system time is adjusted. steady_clock is monotonic and does not jump, which makes it ideal for measuring intervals. high_resolution_clock is an alias to the highest resolution clock available on your platform.

#include <chrono>
using namespace std::chrono;
auto t0 = steady_clock::now();
// work …
auto ms = duration_cast<milliseconds>(steady_clock::now() - t0);

Durations and literals

Durations carry both representation and period. Use suffixes to write readable code. Convert with duration_cast when narrowing or when you need a specific unit.

using namespace std::chrono_literals;
auto wait = 150ms + 2s;
auto us = std::chrono::duration_cast<std::chrono::microseconds>(wait);

Time points and arithmetic

Add durations to time points, or subtract time points to get durations. Convert between clocks only through approved utilities such as clock_cast when available.

auto start = std::chrono::steady_clock::now();
// …
auto elapsed = std::chrono::steady_clock::now() - start; // duration
⚠️ Use steady_clock for benchmarking. system_clock can move backwards or forwards if the system time is adjusted, which breaks elapsed time math.

Date and time formatting

C++20 adds std::format support for time types plus full date formatting in <chrono>. You can print durations and time points using familiar format strings, and you can work with time zones through zoned_time and the IANA database when your standard library provides it.

Formatting durations

Formatters understand durations directly. Choose units with a cast before formatting to control the output.

#include <format>
using namespace std::chrono_literals;
auto d = 12345ms;
auto s = std::format("{} ms", std::chrono::duration_cast<std::chrono::milliseconds>(d).count());

Formatting time points

Convert a time point to calendar form with floor or use std::chrono::format with specifiers that mirror strftime patterns. The braces carry the pattern; options go inside as {…}.

#include <chrono>
#include <format>
using namespace std::chrono;
auto now = system_clock::now();
std::string iso = std::chrono::format("{:%Y-%m-%dT%H:%M:%S%Ez}", now);

Time zones and zoned_time

Bind a clock time to a named zone for correct civil time handling, including daylight saving rules. This requires a time zone database at runtime, which many standard libraries ship.

#include <chrono>
using namespace std::chrono;
zoned_time zt{"Europe/London", system_clock::now()};
auto local = std::chrono::format("{:%F %T %Z}", zt);  // prints date, time, and zone

Parsing dates from strings

Use std::chrono::parse to turn text into time points or durations. Match the pattern to your input exactly to avoid failure.

#include <chrono>
#include <sstream>
using namespace std::chrono;
std::istringstream is{"2025-11-06 20:00"};
sys_seconds tp;
is >> parse("%F %R", tp);  // tp holds a UTC time_point at minute resolution

Chapter 26: Functional Patterns and Callbacks

C++ supports a spectrum of functional styles. You can pass functions as values, compose work through lightweight views, and build callback based APIs that stay predictable and efficient. This chapter explores higher order helpers, std::function compared with templates, composable view pipelines, and callback design that avoids hidden traps.

Higher order utilities and bind

Higher order utilities treat functions as data. You can partially apply a function, adapt signatures, or connect algorithms with small glue objects. In modern code, lambdas usually replace std::bind, but understanding both techniques helps when maintaining older code or integrating with generic interfaces.

Partial application with lambdas

Lambdas capture values and create function objects. This gives you clean partial application without extra machinery. Use captures to bind arguments or context that should travel with the operation.

#include <algorithm>
auto is_shorter_than = [](std::size_t n) {
  return [n](std::string_view s) { return s.size() < n; };
};
std::vector<std::string> v{"alpha", "beta", "gamma"};
v.erase(std::remove_if(v.begin(), v.end(), is_shorter_than(5)), v.end());

Adapting signatures with std::bind

std::bind reorders arguments and fixes parameters to new values. Modern C++ prefers lambdas for clarity, but existing libraries still expose bind friendly hooks. Use placeholders to pick which arguments get forwarded.

#include <functional>
using std::placeholders::_1;
int add(int a, int b) { return a + b; }
auto add_to_10 = std::bind(add, 10, _1);
int r = add_to_10(7);  // r is 17
💡 Keep captures explicit. A lambda that lists its captures in plain sight is easier to audit than a bind expression with several placeholders.

Function adaptors in algorithms

Standard algorithms often accept predicates, projections, or mappers. Writing tiny adaptors keeps pipelines readable and minimizes boilerplate. Use captures to lift constants or shared state into the algorithm.

#include <algorithm>
#include <numeric>
std::vector<int> v{1, 2, 3, 4, 5};
int sum_sq = std::accumulate(v.begin(), v.end(), 0,
  [](int acc, int x) { return acc + x * x; });

std::function vs templates

Passing callables can be dynamic or static. std::function provides type erased wrappers for runtime flexibility, while templates give compile time customization without dynamic overhead. Pick the model that matches your cost and flexibility requirements.

Dynamic dispatch with std::function

std::function stores any callable of a compatible signature. This makes it ideal for plugin points, event systems, and user provided callbacks whose concrete type you cannot know ahead of time.

#include <functional>
std::function<int(int)> f;
f = [](int x) { return x + 1; };
f = [](int x) { return x * 2; };
int r = f(10);

Static dispatch with templates

When performance is critical and the callable type is known at compile time, take the callable as a template parameter. This avoids type erasure and inline barriers, letting the compiler optimize aggressively.

template <class F>
int apply_twice(F f, int x) {
  return f(f(x));
}
int r = apply_twice([](int x){ return x + 1; }, 10);
⚠️ Do not store templated callables directly in containers when types vary per element. Either wrap them in std::function or use variant based storage.

Small buffer optimization effects

std::function often includes small buffer optimization. Many tiny callables fit without heap allocation. Large captures may spill to the heap, so audit captures to keep performance stable.

#include <functional>
int big_array[1024];
std::function<int()> f = [a = big_array]() { return a[0]; };  // likely allocates

View pipelines and composability

Ranges and views give you lazy pipelines. They transform, filter, and group data without building intermediate containers. Each view adapts the previous stage, so the final algorithm pulls only the elements it needs.

Transform and filter

A transform view maps each element to a new value. A filter view keeps only the values that satisfy a predicate. Chain them to express intent without loops or temporary vectors.

#include <ranges>
std::vector<int> v{1, 2, 3, 4, 5, 6};
auto pipeline =
  v
  | std::views::filter([](int x){ return x % 2 == 0; })
  | std::views::transform([](int x){ return x * x; });
for (int x : pipeline) {
  // … use x
}

Chaining with | for clarity

The pipe operator keeps the transformation flow legible. Each step states one intent, then yields to the next. Use descriptive lambdas to guide readers along the path.

auto words = std::views::transform([](int x){ return std::to_string(x); });
auto r =
  v
  | std::views::filter([](int x){ return x > 10; })
  | words;

Custom view adaptors

You can define your own adaptors to slot into pipelines. These combine a factory function and a view type that wraps an underlying range. Follow existing view patterns to integrate with std::views naturally.

#include <ranges>
template <std::ranges::input_range R>
class take_even_view : public std::ranges::view_interface<take_even_view<R>> {
  R base_;
public:
  take_even_view(R base) : base_(std::move(base)) {}
  auto begin() { return std::ranges::begin(base_); }
  auto end()   { return std::ranges::end(base_); }
  // … implement skipping logic
};
💡 Keep custom views simple. Favor composition of existing views over building new iterator types unless you need custom traversal or caching behavior.

Designing callback based APIs

Callbacks notify callers at specific moments such as completion, progress, or error events. A clean design avoids hidden state, captures execution context clearly, and provides strong lifetime rules for every callable you accept.

Style and lifetime of callbacks

Require that callbacks be valid for the duration of their registration. Choose either copy based storage with std::function or move only callables in templates. Document when callbacks are invoked relative to the triggering function.

class downloader {
  std::function<void(double)> on_progress_;
public:
  void set_progress_handler(std::function<void(double)> cb) {
    on_progress_ = std::move(cb);
  }
  void run() {
    for (int i = 0; i <= 100; i += 10) {
      if (on_progress_) on_progress_(i / 100.0);
      // … work
    }
  }
};

Execution context

Explain where callbacks run: same thread, worker threads, or a dispatcher. Misplaced assumptions about threading often cause subtle bugs. Use executors or queues to control context explicitly.

template <class Exec, class F>
void post(Exec& ex, F f) {
  ex.enqueue(std::move(f)); // executor decides where f runs
}

Error reporting and cancellation

Callbacks can signal failures or requests to stop. Use separate callbacks for success and error, or use a result type that carries either value or failure. Keep cancellation cooperative so callers remain in control.

#include <variant>
using result_t = std::variant<std::string, std::error_code>;
std::function<void(result_t)> on_done;

Balancing templates and std::function

For library code, offer both static and dynamic paths. Templates give maximum speed when callers can commit to concrete types. std::function provides flexibility for stored callbacks that may change over time. Matching the callback model to usage patterns keeps APIs predictable and approachable.

Chapter 27: Interoperability and ABI Concerns

C++ interfaces often meet other languages, older binaries, and platform level APIs. Working across these boundaries requires stable linkage, predictable data layout, careful exception policy, and a clear strategy for foreign function interfaces. This chapter explains how C++ cooperates with C, how name mangling affects linkage, how exceptions behave across boundaries, and how to design safe data layouts and portable FFI layers.

Extern "C" and linking with C

C++ normally decorates symbol names to encode types and calling conventions. C does not. extern "C" suppresses that decoration for declared symbols so C and C++ can link together correctly. This is the cornerstone of almost all cross language system interfaces.

Declaring C style interfaces

Wrap C compatible declarations in an extern "C" block. Function signatures must use C compatible types and avoid templates, references, and classes with non trivial behavior.

extern "C" {
  int add_ints(int a, int b);
}

C code can now link against the produced object file. In headers intended for both languages, guard the block with macros so C sees plain declarations and C++ applies the linkage specifier.

#ifdef __cplusplus
extern "C" {
#endif

int add_ints(int a, int b);

#ifdef __cplusplus
}
#endif

Sharing structures with C

Shared structures must contain only C compatible elements. Avoid templates, constructors, destructors, or non standard layout types. Keep alignment predictable by sticking to fundamental types.

struct point {
  double x;
  double y;
};
💡 When passing buffers across boundaries, prefer pointer plus size pairs. This avoids hidden assumptions and keeps memory ownership explicit.

Calling C from C++

You can freely include C headers in C++ code. Many system libraries follow this pattern. When mixing languages in a build, compile C files as C, not as C++.

#include <math.h>
double r = sqrt(2.0);

Name mangling and ABI stability

C++ needs mangled names to encode overloaded functions, namespaces, and types. Compilers differ in their mangling rules, and the language standard does not specify them. As a result, binary interfaces are fragile across compiler families and sometimes across versions of the same compiler.

How mangling works

The compiler builds symbol names that describe the full signature. This enables overloaded resolution at link time. Tools such as nm or objdump show these long strings when inspecting binaries.

// Unmangled form
int compute(int a, double b);

// Mangled form (varies by compiler)
// _Z7computeid

ABI boundaries and stable surfaces

Keep binary interfaces stable by exposing minimal C style entry points or by using dedicated ABI stable layers such as COM like interfaces or C wrappers. Avoid exposing templated or inline heavy functions in a public binary ABI. Prefer header only libraries when the whole interface is inline and template based, so no binary surface exists.

⚠️ Changing a class layout or inline behavior can silently break existing binaries. Recompile all dependents whenever public headers change.

Vendor ABIs and cross compiler issues

Different compilers on the same platform may share a common ABI only for fundamental types. Complex C++ types such as std::string or std::vector often differ. Avoid passing such types across shared library boundaries unless all parties agree on the exact compiler and standard library version.

Exceptions across boundaries

Exceptions must not cross ABI boundaries where the other side cannot catch them. Throwing an exception into foreign code leads to undefined behavior because the runtime model does not match and destructors may not run.

Sealing boundaries

Wrap calls near the boundary to catch any exceptions and translate them into error codes or status values. This keeps foreign callers safe and preserves predictable control flow.

extern "C" int do_work_c(int x, int* out) {
  try {
    *out = do_work_cpp(x);
    return 0;
  } catch (...) {
    return -1;
  }
}

Error reporting models

Combine simple error codes with detailed logs or structured results. For richer detail, write small plain old data structures that cross the boundary cleanly, then interpret them on the C++ side.

Long jumps and foreign runtimes

Languages such as C or Fortran may use setjmp and longjmp. These constructs bypass C++ destructors if they cross into C++ controlled scope. Restrict their use to pure C frames or wrap them with strict guards.

Data layout, padding, and alignment

Interoperability depends on consistent memory layout. Standard layout types have predictable field order and no surprising padding at the start, although compilers may insert padding between fields to satisfy alignment requirements. You must design shared structures so every participant agrees on layout and alignment.

Standard layout rules

A standard layout type has no virtual functions, no virtual base classes, and no multiple inheritance with tricky ordering. Its members follow defined order, and the first member starts at offset zero. This makes it suitable for cross language sharing.

struct header {
  uint32_t magic;
  uint16_t version;
  uint16_t flags;
};

Padding and alignment costs

Compilers align fields so that accesses stay efficient. Insert padding manually only when necessary. Aligning fields by decreasing size often reduces padding.

struct mixed {
  uint64_t a;
  uint32_t b;
  uint16_t c;
  uint8_t  d;
  // … compiler may insert padding at the end
};
💡 Use static_assert(sizeof(T) == expected) to confirm layout stability where ABI matters. This reveals silent padding changes after edits.

Alignment attributes and portability

Attributes such as alignas request specific alignment. Use them sparingly. Over aligned types may conflict with foreign runtimes or serialization formats. Validate alignment on each supported platform.

struct alignas(32) buffer {
  std::byte data[128];
};

FFI strategies and portability

Foreign function interfaces connect C++ with higher level languages such as Python, Rust, Java, or C#. A portable strategy keeps the C++ core in a clean C layer, then wraps that layer in the target language’s binding system.

The C shim model

Keep your C++ logic internal and expose thin extern "C" functions. Bindings in other languages call those functions and pass plain data. This reduces dependency on C++ ABI details.

extern "C" double dot_product(const double* a, const double* b, int n);

Automated binding tools

Many ecosystems contain tools that generate bindings from C headers or metadata. Examples include SWIG, ctypes, cffi, JNI, and P/Invoke. Each tool has its own calling convention assumptions. Use stable C interfaces as input to avoid chasing C++ ABI changes.

Memory ownership conventions

Define ownership precisely. If C++ allocates, provide a corresponding free function. If a foreign language allocates, confirm that C++ keeps only borrowed pointers. Mixing ownership rules invites leaks or double frees.

extern "C" char* make_string_copy(const char* s);  // caller frees
extern "C" void  free_string_copy(char* p);

Marshalling complex data

For more complex shapes such as trees or graphs, marshal data through plain arrays or flat buffers. Avoid sending deep object graphs directly. Many languages convert such buffers into their internal structures quickly and reliably.

⚠️ Do not expose STL containers or C++ only types to other languages. Their layout and exception behavior are not portable, and versions differ across standard libraries.

Testing boundaries

Cross language tests catch assumptions early. Validate basic round trips for primitive types, pointers, arrays, and error cases. On each platform, automate a full cycle of build, link, run, and memory analysis so boundary mismatches surface immediately.

Chapter 28: Debugging, Testing, and Profiling

This chapter is your toolbox for finding problems and proving that your program behaves correctly. You will set up symbol information and work smoothly inside an IDE, write unit tests that catch regressions, unleash fuzzers and property based checks, turn on sanitizers to smoke out undefined behavior, and then chase performance with profilers and hardware counters. Treat these tools as companions during development rather than last minute rescue boats.

Debug info, symbols, and IDE setup

Compilers can include mapping data that links machine code back to source files and line numbers. This information is stored in formats such as DWARF on Unix like systems and PDB on Windows. With symbols present, debuggers can show file names, function names, variable values, and accurate stack traces. Most IDEs expect these symbols; they also assume a build with little or no optimization because highly optimized code can be hard to step through.

Compile with -g, /Zi, and sensible optimization

On GCC and Clang use -g to emit DWARF and pair it with -O0 or -Og during active debugging. On MSVC use /Zi to produce PDB files and pick /Od for minimal optimization. Keep frame pointers with -fno-omit-frame-pointer where possible; call stacks become clearer and sampling profilers often improve.

# GCC or Clang
g++ -g -Og -fno-omit-frame-pointer -std=c++23 main.cpp -o app

# MSVC
cl /std:c++23 /Zi /Od /MDd main.cpp /Fe:app.exe
💡 Use multi-config generators in CMake so you can switch between Debug, RelWithDebInfo, and Release without regenerating builds.

Load symbols and step with gdb, lldb, and Visual Studio

Modern IDEs wrap command line debuggers. VS Code drives gdb or lldb through launch.json. CLion bundles lldb on macOS. Visual Studio uses its own debugger and PDBs. Once attached you can set breakpoints, step, inspect locals, evaluate expressions, and watch values as they change.

# Minimal sessions
gdb ./app
(lldb) target create ./app

# Backtraces when something crashes
gdb -ex 'run' -ex 'bt' --args ./app arg1 arg2

Symbol files, stripping, and crash triage

Shipping builds often omit symbols to reduce size. Keep an external store of symbol files so production crash logs can be symbolicated later. On Unix like systems you can split symbols into a separate file with objcopy --only-keep-debug, then strip the binary and add a link to the external symbols.

# Split and reference debug symbols
objcopy --only-keep-debug app app.debug
strip --strip-debug --strip-unneeded app
objcopy --add-gnu-debuglink=app.debug app
⚠️ Optimizations can inline, reorder, or eliminate code. Expect stepping to feel surprising when using high optimization levels; prefer RelWithDebInfo for performance triage and Debug for logic debugging.

Unit testing libraries and patterns

Unit tests provide fast, deterministic checks that focus on small surfaces of behavior. Good tests are readable, isolated, and stable across refactors. Choose a lightweight framework and keep tests running in seconds so you execute them frequently.

Popular choices: GoogleTest, Catch2, and doctest

GoogleTest offers rich assertions and fixtures. Catch2 focuses on simplicity with a header only design. doctest mirrors Catch2 syntax and aims for tiny compile times. All three integrate with CTest and IDEs.

// GoogleTest example
#include <gtest/gtest.h>
int add(int a, int b) { return a + b; }
TEST(Math, Add){ EXPECT_EQ(add(2, 3), 5); }
int main(int argc, char** argv){ ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
// Catch2 single-file example
#define CATCH_CONFIG_MAIN
#include <catch2/catch_all.hpp>
int mul(int a, int b) { return a * b; }
TEST_CASE("mul works"){ REQUIRE(mul(3, 4) == 12); }

Arrange Act Assert and expressive names

Structure tests with Arrange Act Assert. Prepare inputs, call the function, then assert outcomes. Name tests by behavior so failure messages read like short stories. Parameterized tests reduce duplication by feeding multiple inputs through the same body.

// Parameterized example with GoogleTest
class PowerTests : public ::testing::TestWithParam<std::tuple<int,int,int>> {};
TEST_P(PowerTests, Computes){ auto [b,e,expect] = GetParam(); EXPECT_EQ(std::pow(b,e), expect); }
INSTANTIATE_TEST_SUITE_P(Small, PowerTests, ::testing::Values(
  std::make_tuple(2,3,8), std::make_tuple(3,3,27), std::make_tuple(10,0,1)
));
💡 Wire tests into your build with add_test and use ctest --output-on-failure so continuous integration captures clear logs.

Fixtures, test doubles, and seams

Fixtures manage setup and teardown when many tests share context. For code that touches clocks, I/O, or random sources, inject interfaces and provide fakes in tests. These seams let you verify logic deterministically and keep tests quick.

Fuzzing and property based testing

Fuzzing pushes huge volumes of generated inputs at your code to discover crashes and sanitizer findings. Property based testing states invariants that must hold for broad classes of inputs. Together they catch edge cases humans rarely think about.

Coverage guided fuzzing

libFuzzer integrates with Clang. You write a tiny entry point that consumes bytes and calls your parser or function. The engine mutates inputs and keeps those that increase coverage. AFL uses separate instrumentation and a runner process; both thrive when sanitizers are enabled.

// libFuzzer entry point
#include <cstddef>
#include <cstdint>
int parse(const char* data, size_t n);  // your code
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size){
  parse(reinterpret_cast<const char*>(data), size);
  return 0;
}
# Build and run with sanitizers
clang++ -std=c++23 -g -O1 -fsanitize=fuzzer,address,undefined fuzz.cpp -o fuzz_target
./fuzz_target -max_total_time=30 -artifact_prefix=crashes/
⚠️ Persist a seed corpus directory from interesting real inputs. Fuzzers learn faster with a curated starting set rather than empty seeds.

State properties with RapidCheck or Catch2 generators

Instead of single examples, declare properties like reversibility or idempotence. Generators produce many input combinations and shrink failing cases down to minimal examples that still trigger the bug.

// RapidCheck example
#include <rapidcheck.h>
#include <vector>
std::vector<int> rev(std::vector<int> v){ std::reverse(v.begin(), v.end()); return v; }
int main(){
  rc::check("reverse twice gives original", [](const std::vector<int>& v){
    RC_ASSERT(rev(rev(v)) == v);
  });
}

Minimize crashers and promote to regression tests

When a fuzzer finds a crashing input, reduce it and preserve the artifact in version control. Add a unit test that loads the failing bytes; this prevents the bug from sneaking back during future refactors.

Address, leak, and thread sanitizers

Sanitizers instrument your program to detect classes of undefined behavior. They are quick to enable and often turn silent memory bugs into loud, precise reports that point to the bad line and the allocation site.

Enable ASan, UBSan, TSan, and MSan

On Clang and GCC use -fsanitize=address for buffer issues and -fsanitize=undefined for type and arithmetic violations. Use -fsanitize=thread to detect data races; it slows programs more but pays off for concurrency. MemorySanitizer (-fsanitize=memory) tracks uninitialized reads and usually requires building all dependencies with the same flag.

# Typical development toggles
clang++ -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined app.cpp -o app_asan
clang++ -g -O1 -fno-omit-frame-pointer -fsanitize=thread -static-libstdc++ -static-libgcc app.cpp -o app_tsan
💡 Set ASAN_OPTIONS=halt_on_error=1:detect_leaks=1 and UBSAN_OPTIONS=print_stacktrace=1 to get crisp failures and helpful stacks.

Interpret reports and follow the allocation trail

Sanitizer messages show the invalid access, the stack where it happened, and often the stack where the memory was allocated. Fix the first reported problem before chasing later ones because one overflow can cause many downstream explosions.

// A tiny bug that ASan will catch
#include <cstring>
int main(){ char buf[4]; std::memcpy(buf, "hello", 5); return buf[4]; }

Combine sanitizers with fuzzers for high yield

A fuzzer explores input space while sanitizers turn subtle misbehavior into immediate failures. This combination finds deep parser bugs, off by one mistakes, and rare race conditions that barely show up in manual testing.

Profilers and performance counters

Debugging correctness differs from optimizing speed. Profilers show where time actually goes. Hardware counters reveal cache misses, branch mispredicts, and cycles. Start with sampling to locate hot code, then zoom in with instrumentation where needed.

Sampling vs instrumentation and build types

Sampling profilers interrupt the program at intervals and record stacks. They add small overhead and work well with optimized builds. Instrumentation inserts timing code around functions or blocks and can distort results; use it when sampling lacks resolution. For realistic hotspots prefer Release or RelWithDebInfo so the compiler performs inlining and vectorization.

Workflows with perf, Callgrind, and Visual Studio Profiler

On Linux start with perf. Record a session and open a report to see symbols and percentages. For call graph exploration, run under Valgrind’s Callgrind and visualize with KCachegrind. On Windows use Visual Studio Profiler or Windows Performance Analyzer. On macOS use Instruments for time profiles and allocations.

# Linux sampling
perf record -g -- ./app …
perf report

# Callgrind instrumentation
valgrind --tool=callgrind ./app …
kcachegrind callgrind.out.<pid>
⚠️ Microbenchmarks can lie if the CPU is not warmed up or the compiler optimizes away work. Use realistic inputs, pin CPU frequency if possible, and keep I/O out of inner loops during measurement.

Measure with std::chrono and read counters with perf stat

For ad hoc timing wrap code with std::chrono::steady_clock. For deeper signals use perf stat to collect cycles, instructions, and cache stats. Ratios like instructions per cycle help separate CPU bound code from memory bound code.

// Ad hoc timing
#include <chrono>
#include <iostream>
int main(){
  auto t0 = std::chrono::steady_clock::now();
  // work …
  auto t1 = std::chrono::steady_clock::now();
  std::cout << std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count() << " us\n";
}
# Hardware counters
perf stat -e cycles,instructions,branches,branch-misses,cache-misses ./app …

Create a repeatable performance lab

Stabilize measurements by fixing CPU governor settings, disabling turbo where appropriate, and isolating cores. Run multiple trials, report medians and percentiles, and version control benchmark inputs. Small process changes make results trustworthy.

Chapter 29: Performance, Optimization, and Memory Layout

This chapter looks at how data choices and low level mechanics shape speed. You will organize memory for predictable access, help the compiler inline wisely, play nicely with caches and branches, leverage small object and empty base optimizations, and measure with care. Optimization starts with evidence; change code only after you can explain a slow path and predict the gain.

Data oriented design basics

Data oriented design focuses on how bytes move through the machine. Layout and access patterns often matter more than clever algorithms. Keep hot data packed tightly, avoid pointer chasing inside tight loops, and stream sequentially so hardware prefetchers can help.

Array of Structures vs Structure of Arrays

An array of structures groups fields together per object. A structure of arrays groups the same field across many objects. Choose the layout that matches your access pattern. If a loop touches only one field, a structure of arrays can load fewer bytes and improve cache locality.

Pattern Shape Best when
AoS std::vector<Particle> Each iteration needs most fields
SoA struct { std::vector<float> x,y,z; } The loop touches a subset like x and y
struct Particle { float x, y, z; float mass; };
std::vector<Particle> aos;
// SoA variant
struct Particles { std::vector<float> x, y, z, mass; } soa;
💡 Keep hot and cold fields separate. Split rare debug metadata or large strings into a side table so tight loops touch only hot bytes.

Alignment, padding, and packing pressure

Compilers insert padding to satisfy alignment. Reorder fields to reduce wasted space, but do not fight natural alignment for hot numeric types. Misaligned or packed fields can hurt performance on some targets. Favor naturally aligned arrays for vectorization.

struct Bad { char tag; double value; int count; };   // likely padding after tag
struct Good { double value; int count; char tag; };  // tighter layout
static_assert(sizeof(Good) <= sizeof(Bad));

Contiguous containers and iteration shape

std::vector and std::array offer contiguous storage that plays well with caches and SIMD. Linked structures scatter nodes and add branch cost. If you need stable iterators and frequent inserts in the middle, consider gap buffers or std::deque; otherwise prefer vectors and batch operations.

Inlining, branch prediction, and caching

Modern CPUs are fast when code is predictable. Inlining removes call overhead and exposes optimization opportunities. Predictable branches keep the pipeline full. Cache friendly access patterns avoid stalls. Balance each lever with binary size and instruction cache pressure.

When inlining helps and when it hurts

Inlining small hot functions can enable constant propagation and vectorization. Over inlining bloats text size and increases i-cache misses. Guide the compiler with inline or attributes sparingly; trust -O2 or -O3 heuristics first and confirm with a profiler.

inline float square(float x){ return x * x; }  // simple and likely to inline

Taming branches and using [[likely]]

Hard to predict branches cost cycles. Restructure code to remove branches inside inner loops or group data so common cases cluster. In C++20 you can annotate expected paths with [[likely]] and [[unlikely]]. Treat these as hints that must match measurements.

if (fast_path_condition()) [[likely]] {
  // hot path
} else {
  // rare path
}
⚠️ Hand written branchless tricks can backfire if they increase memory traffic or spill registers. Check assembly size and measure before keeping them.

Cache aware traversal and blocking

Iterate in memory order and block work to fit caches. For matrices store rows in row major order and traverse rows in the inner loop. For bigger kernels use tiling so working sets stay inside L1 or L2 caches during the hottest loops.

// Row major multiply sketch
for (size_t i = 0; i < n; ++i)
  for (size_t k = 0; k < n; ++k) {
    float aik = A[i*n + k];
    for (size_t j = 0; j < n; ++j)
      C[i*n + j] += aik * B[k*n + j];
  }

Small object optimization and EBO

The small object optimization stores tiny payloads directly inside a type to avoid heap allocations. The empty base optimization removes storage for empty base classes within derived objects. Both reduce indirection and can shrink working sets.

Understanding small string optimization

Most std::string implementations keep short text inline. Short names and tokens avoid heap traffic and improve locality. Never rely on a specific threshold because it varies across libraries and platforms.

std::string s = "ok";       // likely inline
std::string big(200, 'x');  // likely heap allocation

Designing small buffer friendly types

Custom wrappers can embed a small buffer. Keep the in place capacity modest to stay within register and cache budgets. Provide a growth path to the heap for larger cases and ensure strong exception safety.

template <size_t N>
class small_vec {
  size_t n_ = 0;
  alignas(std::max_align_t) unsigned char buf_[N * sizeof(int)];
  std::unique_ptr<int[]> heap_;
public:
  void push_back(int v){
    if (n_ < N) { reinterpret_cast<int*>(buf_)[n_++] = v; }
    else {
      if (!heap_) heap_.reset(new int[N * 2]);
      heap_.get()[n_++] = v;
    }
  }
};
💡 Keep object sizes near powers of two when many instances live together. Allocators and caches often behave more predictably with round sizes.

Use empty base optimization correctly

EBO removes storage for empty bases inside derived objects. Mark tag or policy types as empty and inherit privately. Avoid accidental non empty bases by not adding hidden fields like virtual tables or non trivial members.

struct NoState {};
template <class Policy = NoState>
struct Widget : private Policy {
  int id;
}; // sizeof(Widget<>) equals sizeof(int) on typical platforms

Move semantics and copy elision

Move operations transfer resources instead of duplicating them. Copy elision removes whole moves and copies by constructing directly in place. Write types that are cheap to move and let the compiler elide when possible.

Prefer std::move at transfer points

Use std::move when ownership leaves a scope or when you forward temporaries through factory functions. After a move leave the source in a valid but unspecified state so it can be destroyed safely.

std::vector<int> make(){ std::vector<int> v = {1,2,3}; return v; }
// NRVO likely
std::vector<int> a = make();        // elision or move
std::vector<int> b = std::move(a);  // explicit transfer

Implement efficient move operations

For owning types, move pointers and counts, then null out the source. Declare copy and move members explicitly when managing resources. Consider the rule of five if any custom destructor or copy logic exists.

struct Buffer {
  size_t n{};
  int* p{};
  Buffer(size_t n): n(n), p(new int[n]) {}
  ~Buffer(){ delete[] p; }
  Buffer(const Buffer& o): n(o.n), p(new int[o.n]) { std::copy(o.p, o.p+o.n, p); }
  Buffer& operator=(Buffer o){ std::swap(n, o.n); std::swap(p, o.p); return *this; }
  // copy-and-swap
  Buffer(Buffer&& o) noexcept: n(o.n), p(o.p) { o.n = 0; o.p = nullptr; }
};

Trust guaranteed copy elision

In C++17 returning a prvalue can construct directly in the destination. Do not insert unnecessary std::move on named return variables because it can block elision. Keep factories simple and let the compiler place objects.

Buffer make_buf(){ return Buffer{1024}; }  // constructs in caller when possible

Microbenchmarks and pitfalls

Microbenchmarks help isolate the cost of small building blocks. They can also mislead when compilers optimize away work or when environments vary. Use a harness that disables dead code elimination and reports multiple statistics, then validate findings in end to end profiles.

Building reliable harnesses

Use steady clocks and pin measurements to real work. Prevent the compiler from removing computations by observing results or using escape barriers. Warm up before timing and run enough iterations to reduce noise.

# Example with Google Benchmark
BENCHMARK(MyAlgo)->Arg(1024)->Arg(4096);
BENCHMARK_MAIN();

Guard against dead code elimination

Ensure side effects survive optimization. Summaries printed to std::cout or calls to opaque functions can anchor results. Some frameworks provide DoNotOptimize and ClobberMemory helpers that create barriers.

// Pattern sketch
auto r = compute(…);
benchmark::DoNotOptimize(r);
benchmark::ClobberMemory();
💡 Always cross check micro results with a sampling profile of the whole application. If hotspots do not line up, trust the end to end profile.

Report distributions and environment

Single numbers hide variability. Publish medians and percentiles, list CPU model and compiler flags, and include dataset shapes. This context makes results reproducible and prevents cargo cult tuning.

Chapter 30: Patterns, Idioms, and Modern Style

This chapter collects structural techniques that appear across modern C++ codebases. These patterns help you separate interfaces from implementations, build flexible hierarchies, express alternatives cleanly, avoid accidental ownership, and lean on the Guideline Support Library. Treat them as tools that improve clarity when used in the right context rather than rules to follow blindly.

Pimpl, type erasure, and value semantics

Pimpl (pointer to implementation) hides internal details behind a stable interface. Type erasure lifts concrete types into a uniform value like std::function. Both approaches preserve value semantics so users can pass and return objects freely without exposing internal structure.

Build a Pimpl for stable ABI and fast builds

A Pimpl shields header files from heavy implementation details. The public class holds a pointer to a forward declared struct. This reduces rebuild time and gives you freedom to change internals without altering the public interface. Manage lifetime carefully so copy and move operations behave naturally.

// In widget.h
class Widget {
  struct Impl;
  std::unique_ptr<Impl> p_;
public:
  Widget();
  Widget(Widget&&) noexcept;
  Widget& operator=(Widget&&) noexcept;
  Widget(const Widget&);
  Widget& operator=(const Widget&);
  ~Widget();
  void do_stuff();
};

// In widget.cpp
struct Widget::Impl { int x; void work(); };
💡 Use std::make_unique inside constructors and write move members to avoid double allocation when transferring ownership.

Apply type erasure with small value objects

Type erasure wraps any type that satisfies a concept behind a stable interface. std::function, std::any, std::optional, and many iterator wrappers follow this pattern. You get a uniform value type that can hold different concrete implementations while exposing consistent operations.

// A sketch of erased callable
struct callable {
  struct base { virtual ~base(){} virtual int call(int)=0; };
  template<class F> struct model : base {
    F f;
    model(F x): f(x) {}
    int call(int x) override { return f(x); }
  };
  std::unique_ptr<base> ptr;
  template<class F> callable(F f): ptr(std::make_unique<model<F>>(f)) {}
  int operator()(int x) { return ptr->call(x); }
};

Preserve value semantics in public APIs

When designing types, strive for copyable and movable values. Hidden pointers and reference counting can still support value semantics as long as operations behave like independent values. Clear semantics encourage clean interfaces across modules.

CRTP and mixins

The Curiously Recurring Template Pattern lets a base class use the derived class type as a template parameter. You get compile time polymorphism without virtual dispatch. Mixins layer reusable behavior that the compiler can inline and optimize aggressively.

Shape compile time polymorphism

CRTP enables the base to call derived functions statically. This skips virtual tables and opens the door to inlining. It fits well for numeric kernels, policy classes, and static interfaces.

template<class Derived>
struct Base {
  void run(){ static_cast<Derived*>(this)->step(); }
};

struct Algo : Base<Algo> {
  void step(){ /* work */ }
};

Layer behaviors with mixins

Mixins let you compose features without heavy inheritance trees. Each mixin adds a slice of behavior, and your final type assembles them. Keep mixins small so they remain understandable and avoid ambiguous name lookups.

template<class T> struct Logging { void log(const char* m){ /* … */ } };
template<class T> struct Timing  { void tic(){ /* … */ } void toc(){ /* … */ } };

struct Task : Logging<Task>, Timing<Task> {
  void run(){ tic(); log("start"); /* ... */ log("end"); toc(); }
};
⚠️ Deep template layering can produce long diagnostics. Keep mixin hierarchies simple and document intended use patterns.

Policies and static customization points

Policy classes passed as template parameters let you change behavior without runtime overhead. Common examples include allocators, comparison strategies, and threading policies. Keep policy interfaces minimal so substitutions remain easy.

Visitor, variant, and pattern matching idioms

Variants and visitors express alternatives cleanly. With std::variant you can hold one of several types, then use std::visit to pattern match. This replaces scattered conditionals with structured dispatch and keeps state machines tidy.

Use std::variant for tagged unions

std::variant stores a choice among types and tracks which one is active. It replaces manual tag enums and unions, and integrates with move semantics and exception safety.

using Node = std::variant<int, std::string>;
Node n = 42;
n = std::string("hi");

Pattern match with std::visit

std::visit lets you apply a callable to the active alternative. With lambdas you can write simple handlers for each case. This keeps logic grouped and avoids long switch statements on tag values.

std::visit([](auto& v){
  using T = std::decay_t<decltype(v)>;
  if constexpr (std::is_same_v<T,int>) { /* handle int */ }
  else { /* handle string */ }
}, n);

Model sum types and recursive structures

Variants can model recursive data like expression trees. Use std::unique_ptr for recursive members and keep visitors small. This mirrors sum types in functional languages and makes transformations explicit.

struct Expr;
using ExprPtr = std::unique_ptr<Expr>;
struct Expr {
  std::variant<int, std::pair<char, std::array<ExprPtr,2>>> node;
};
💡 Fold visitors with overloaded helper structs to simplify matching. This removes repeated boilerplate and keeps code readable.

Dependency inversion and non owning views

Dependency inversion pushes concrete resources to the edges and depends on abstract interfaces in the middle. Non owning views like std::span or std::string_view express read only access without taking ownership. This reduces coupling and clarifies responsibilities through clean boundaries.

Inject dependencies through constructors

Pass services or resources into objects from the outside. This avoids hidden singletons and makes behavior testable. Store references or pointers only when ownership stays elsewhere, and document expected lifetimes clearly.

struct Logger { void write(const std::string&); };

struct Service {
  Logger& log;
  Service(Logger& l): log(l) {}
  void run(){ log.write("start"); /* … */ }
};

Use non owning views for read only access

std::span and std::string_view describe ranges without owning them. They clarify intent and avoid expensive copies. Ensure the referenced data remains valid while the view is in use.

void process(std::span<const int> s){
  for (int v : s) { /* ... */ }
}

void show(std::string_view sv){ /* … */ }
⚠️ Do not store long lived views into short lived buffers. Violated lifetimes are subtle and can lead to difficult bugs.

Abstract interfaces and plug in implementations

Define narrow interfaces and provide concrete implementations at the boundary. For runtime polymorphism use virtual functions or type erased wrappers. For compile time flexibility use policy classes or CRTP patterns.

Guideline support library notes

The Guideline Support Library (GSL) provides tools that encode common safety idioms. It includes types for non null pointers, spans, narrow conversions, and contracts. Many of these ideas shaped C++20 and C++23 features.

Use gsl::span, gsl::not_null, and gsl::index

gsl::span mirrors std::span on older compilers. gsl::not_null encodes non null pointers for safer APIs. gsl::index expresses sizes and indexes with intent. These helpers reduce accidental misuse and improve clarity.

#include <gsl/gsl>

void draw(gsl::span<const float> xs);
void use(gsl::not_null<Widget*> w);

Narrowing and contract hints

gsl::narrow and gsl::narrow_cast offer explicit conversions between integer sizes. Use them when range constraints matter. The GSL also provides optional contract macros that document preconditions and postconditions, though full contracts remain a future C++ feature.

int small = gsl::narrow<int>(big_value);    // checks range
int wide  = gsl::narrow_cast<int>(big_value); // unchecked

Adopt GSL ideas while remaining idiomatic

Many GSL patterns are reflected in C++20 core features. Use GSL where older compilers need support or where explicit safety gains are clear, but otherwise prefer standard library types. Treat the GSL as a reference of good practices rather than a required dependency in all projects.

Chapter 31: Working With Legacy Code

Legacy code is the deep forest of a codebase. It holds history, risk, and hard won knowledge. Modernizing it requires patience and a sense of respect for the ideas that came before. This chapter explores careful approaches that bring older designs forward without breaking existing users or workflows. Move slowly, measure progress, and let each improvement settle before advancing further.

Incremental modernization

Legacy systems often mix styles from many eras. Touching everything at once is rarely safe. Instead choose narrow targets and improve them piece by piece. Each change should reduce technical debt and leave the surrounding area stable. Adopt new language features only when they make the code simpler, safer, or easier to maintain.

Use narrow refactoring passes

Pick small regions and apply focused improvements. Replace loops with clear algorithms, clean up naming or scoping, or remove duplicated logic. Keep each pass self contained. Review diffs so they remain readable and easy to revert if something unexpected happens.

// Before
for (size_t i = 0; i < v.size(); ++i) acc += v[i];

// After
acc = std::accumulate(v.begin(), v.end(), 0);

Introduce tests around fragile areas

Before altering code, write tests that capture existing behavior. These tests act like guard rails. They help verify that modernizations preserve semantics. Even minimal smoke tests provide confidence during the first few cleanups.

💡 Give tests names that describe behavior rather than implementation. When legacy code evolves, the tests will still express the intended outcome.

Retire unsafe constructs gradually

As you find raw pointer arithmetic, manual resource handling, or risky casts, replace them in small steps. Isolate behaviors in helper functions and work outward. Migrate tricky sections last because they depend on earlier simplifications.

Adapting headers to modules

C++ modules reduce compile times and clarify boundaries, but legacy codebases often rely on sprawling headers. Transitioning to modules works best when done incrementally. Start with stable leaf components and work upward toward higher level modules.

Create thin module wrappers

Keep existing headers intact, then introduce import units that wrap them. This approach allows old includes and new imports to live together during a long transition. Over time you can shift implementation details behind private module partitions.

// widget.ixx
export module widget;
#include "widget.h"
export using widget::Widget;

Split heavy headers into partitions

Large headers often blend declarations and definitions. Move implementation into module:private partitions and expose only the stable surface. This reduces rebuild costs and forms a clearer architecture. Keep splitting until each partition has a narrow purpose.

⚠️ Be consistent with naming when partitioning. Irregular names slow future maintenance and confuse tooling that expects predictable file structure.

Retain backward compatibility while migrating

Leave include paths functional for existing clients. Wrap legacy headers in adapter modules while gradually removing fragile macros or duplicated definitions. When all internal code uses imports successfully, deprecate old include patterns gently.

Replacing raw ownership with smart pointers

Legacy designs often rely on raw pointers for ownership and lifetime. Introducing smart pointers makes code safer and more self documenting. Shift ownership logically and adjust signatures so callers understand who owns what.

Start with unique_ptr for clear ownership

std::unique_ptr expresses exclusive ownership. It fits naturally in factory functions and container elements. Replace new and delete paths one by one. Convert owning parameters to unique_ptr where possible and update callers incrementally.

// Before
Widget* make() { return new Widget; }

// After
std::unique_ptr<Widget> make() { return std::make_unique<Widget>(); }

Use shared_ptr only when ownership is shared

Shared ownership can simplify some designs, but excessive use creates implicit cycles and hidden costs. Apply shared_ptr where lifetime truly spans multiple owners. Introduce weak_ptr for non owning links to break cycles and clarify relationships.

Refactor signature contracts

As you introduce smart pointers choose clear parameter types. Passing by reference expresses no ownership. Passing by unique_ptr transfers ownership. Passing by shared_ptr extends lifetime. These contracts guide readers and reduce guesswork in legacy code.

💡 Document ownership expectations at boundary functions. Even simple comments help future maintainers understand how lifetimes flow.

Refactoring to ranges and algorithms

Legacy loops often hide intention. Modern ranges and algorithms express the shape of a computation directly. Converting old loops improves clarity and may unlock compiler optimizations. Transition carefully so the final code stays readable to everyone.

Replace index loops with range based views

Rewrite loops that walk containers with std::ranges when possible. This removes manual indexing and reveals common patterns like filtering, mapping, and accumulation. Ranges compose naturally which reduces boilerplate.

// Before
for (size_t i = 0; i < xs.size(); ++i)
  if (xs[i] % 2 == 0) sum += xs[i];

// After
sum = std::accumulate(
  xs | std::views::filter([](int v){ return v % 2 == 0; }),
  0
);

Introduce pipelines slowly

Pipelines can grow complex. Start with small steps such as std::views::transform or std::views::filter. Replace one loop at a time. Always check perf with realistic profiles because abstractions sometimes change memory access patterns.

⚠️ Avoid deeply nested pipelines without naming intermediate views. Named subexpressions help with debugging and keep intent readable for maintainers coming from older C++ styles.

Use algorithms for clarity and performance

std::sort, std::transform, std::reduce, and friends hide many pitfalls. They provide well tested behavior and often leverage optimized implementations. Shifting to algorithms reduces error surface and encourages a consistent idiom across the codebase.

Stabilizing interfaces without breaking users

Large systems rely on stable public APIs. Modernizing internals should not disrupt downstream code that expects long lived behavior. Interface stability involves predictable versioning, careful deprecations, and disciplined abstraction boundaries.

Deprecate features gently

Mark outdated APIs with [[deprecated]] so consumers see compile time messages. Provide a clear replacement path. Keep deprecated symbols alive long enough for downstream migrations to complete, especially if the project spans multiple release cycles.

[[deprecated("Use new_api instead")]]
void old_api();

Extend APIs without breaking callers

Add new overloads instead of altering old signatures. Preserve default behaviors and keep binary compatibility for shared libraries. Wrap new features behind optional parameters or traits. Avoid surprising behavior changes that alter semantics silently.

💡 Maintain a small suite of API compatibility tests. These ensure that modernizations do not introduce unintended breaking changes as internal designs evolve.

Use stable abstractions at boundaries

Define narrow, durable interfaces that hide internal shifts. Keep high churn details inside modules or private headers. Present users with simple, predictable functions and types that evolve slowly. When internals change, the boundary surface remains the same, giving downstream code long term reliability.

Chapter 32: Case Studies and Mini Projects

This chapter ties together the core ideas from earlier chapters by building several small but realistic projects. Each case study is scoped to fit in a few pages; each also includes notes on testing, packaging, and trade offs. The goal is to show how modern C++ features can reduce boilerplate and improve clarity when used with care.

A header only library with concepts

Header only libraries are easy to consume and distribute because they do not require a separate link step for users. Concepts let you write readable constraints so that template errors surface as clear diagnostics. We will design a tiny numeric utilities library that works for integral and floating point types and that rejects types which do not meet the required operations.

Layout and naming

Keep the public entry point small and stable. Prefer a single umbrella header that includes internal headers. Avoid macros other than include guards; prefer #pragma once if your toolchain supports it.

// include/numx/numx.hpp
#pragma once
#include <concepts>
#include <type_traits>
#include <limits>

namespace numx {

template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

template<Arithmetic T>
constexpr auto clamp(T v, T lo, T hi) -> T {
  return v < lo ? lo : (v > hi ? hi : v);
}

template<Arithmetic T, std::convertible_to<T> U>
constexpr auto lerp(T a, T b, U t) -> T {
  return static_cast<T>((T{1} - static_cast<T>(t)) * a + static_cast<T>(t) * b);
}

} // namespace numx
💡 Use narrow concepts that describe behavior, not names. Prefer std::regular, std::semiregular, std::sortable, and similar traits when they match your needs.

Testing the interface

Even header only libraries benefit from tests that compile quickly. Use simple unit tests to lock in behavior and error messages where possible.

// tests/numx_tests.cpp
#include "numx/numx.hpp"
#include <cassert>
int main() {
  using numx::clamp;
  assert(clamp(42, 0, 10) == 10);
  assert(clamp(-1, 0, 10) == 0);
  static_assert(noexcept(numx::clamp(1, 0, 10)) == false);
}

Documentation in headers

Small comments at the point of use help readers and tools. Favor short summaries above declarations and longer notes in user guides. Keep examples tiny and focused.

⚠️ Avoid non inline variables or odr violations in headers. If you need static storage, prefer inline variables or function local statics when appropriate.

A coroutine based async pipeline

Coroutines express asynchronous workflows as straightforward code that suspends and resumes. We will sketch a toy pipeline that reads items from a source, transforms them, and writes them to a sink. The example uses a minimal scheduler; production code would integrate with an event loop or thread pool.

Design sketch

The pipeline consists of three parts: a source that produces values, a transform that applies work, and a sink that consumes values. Each stage returns an awaitable type with the right promise_type so the compiler can manage suspension points.

// coro/pipeline.hpp
#pragma once
#include <coroutine>
#include <optional>
#include <queue>

template<typename T>
struct generator {
  struct promise_type {
    std::optional<T> current;
    generator get_return_object() noexcept { return generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    std::suspend_always yield_value(T v) noexcept { current = std::move(v); return {}; }
    void unhandled_exception() { throw; }
    void return_void() {}
  };
  std::coroutine_handle<promise_type> h;
  ~generator(){ if(h) h.destroy(); }
  bool next(){ if(!h.done()) { h.resume(); return !h.done(); } return false; }
  T& value(){ return *h.promise().current; }
};

generator<int> source(int n){
  for(int i = 0; i < n; ++i) co_yield i;
}

generator<int> transform(generator<int> in){
  while(in.next()){
    int v = in.value();
    co_yield v * v;
  }
}

Consuming with co_await

Simple generators do not require co_await to iterate; they use co_yield internally. If you build asynchronous I/O tasks, your awaitables must implement the awaiter protocol with await_ready, await_suspend, and await_resume

// coro/main.cpp
#include "pipeline.hpp"
#include <iostream>
int main(){
  auto out = transform(source(5));
  while(out.next()){
    std::cout << out.value() << "\n";
  }
}
💡 Start with synchronous generators to understand control flow. Add a scheduler and real co_await only when you introduce I/O or timers.

A small CLI with modules and CMake

Modern C++ modules reduce compile time and improve encapsulation. A small command line tool shows how to structure modules and build them with CMake. The tool will read a file, count lines, words, and bytes, and print a summary similar to classic utilities.

Module files

Place your interface units where consumers can find them and keep implementation details in internal partitions if needed. The syntax may vary slightly across compilers; check your toolchain’s module flags.

// src/text.util.ixx
export module text.util;
export namespace text {
  struct counts { long lines; long words; long bytes; };
  counts measure(const char* path);
}
// src/text.util.cpp
module;
#include <cstdio>
#include <cctype>

export module text.util;
export namespace text {
  counts measure(const char* path){
    std::FILE* f = std::fopen(path, "rb");
    if(!f) return {0,0,0};
    long lines = 0, words = 0, bytes = 0;
    int c = 0, prev = ' ';
    while((c = std::fgetc(f)) != EOF){
      ++bytes;
      if(c == '\n') ++lines;
      if(!std::isspace(c) && std::isspace(prev)) ++words;
      prev = c;
    }
    std::fclose(f);
    return {lines, words, bytes};
  }
}

CMake build

Enable languages and set the standard. Recent CMake versions understand module dependency scanning for popular compilers. Keep the target small; expose only what callers need.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.29)
project(tuc LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)

add_executable(tuc
  src/text.util.ixx
  src/text.util.cpp
  src/main.cpp)

target_compile_features(tuc PRIVATE cxx_std_23)
// src/main.cpp
import text.util;
#include <iostream>

int main(int argc, char** argv){
  if(argc < 2){ std::cout << "usage: tuc <file>\n"; return 1; }
  auto r = text::measure(argv[1]);
  std::cout << r.lines << " " << r.words << " " << r.bytes << "\n";
}
⚠️ Module support is still evolving across platforms. If you target older compilers, provide a fallback header that declares the same API and switch with build options.

A range based data processor

Ranges let you express data flows as composable views. We will parse a CSV like stream, filter rows, transform columns, and aggregate results using standard algorithms. The example keeps parsing simple so that the focus remains on the pipeline.

Parsing and views

Represent each record as a small struct and use std::views to slice, filter, and transform. When performance matters, benchmark alternatives before committing to an approach.

// src/pipeline.cpp
#include <string>
#include <vector>
#include <ranges>
#include <charconv>
#include <iostream>

struct row { std::string name; int score; };

static row parse(std::string_view s){
  auto p = s.find(',');
  row r;
  r.name = std::string(s.substr(0, p));
  int v = 0; std::from_chars(s.data() + p + 1, s.data() + s.size(), v);
  r.score = v;
  return r;
}

int main(){
  std::vector<std::string> lines = {"Ada,90","Bjarne,95","Grace,99","Linus,88"};
  auto rng =
    lines
    | std::views::transform(parse)
    | std::views::filter([](const row& r){ return r.score >= 90; })
    | std::views::transform([](const row& r){ return r.score; });

  int total = 0;
  for(int v : rng) total += v;

  std::cout << "total: " << total << "\n";
}
💡 Use std::views::as_rvalue or move aware transforms when large strings or vectors flow through the pipeline and you own them.

Error handling strategy

For production parsing, propagate failures with std::expected<T, E> or a similar type. Keep the hot path free of exceptions if you measure that it helps your workload; the right choice depends on data and platform.

Packaging and distribution

After a project compiles and tests pass, package artifacts so others can use them with minimal friction. Choose an approach that matches your audience. Libraries may ship source packages; CLIs may provide prebuilt binaries; both can register with package managers to reduce setup time.

Source and binary packages

Create tarballs or zip files that contain a top level directory, a license, a short readme, and either include/ headers or bin/ executables. Keep paths stable so scripts can automate updates. Provide a simple cmake --build recipe or a Makefile for portability.

Registries and managers

For C++ libraries, consider vcpkg and Conan. For system packages, provide Debian and RPM specs or Homebrew formulas. Each ecosystem has small differences; document the supported platforms and any caveats clearly.

// conanfile.py … minimal example with name, version, exports, and package() …
⚠️ Do not forget reproducibility. Pin exact versions for dependencies, record the compiler and standard library, and capture your build flags in CI logs so that others can rebuild the same bits.

Versioning and changelogs

Follow semantic versioning when possible. Maintain a human friendly CHANGELOG.md at the root and keep entries short. Readers want to know what changed, why it changed, and how to migrate if there are breaking changes.

Chapter 33: Style Guides and Best Practices

Style is the invisible scaffolding that holds a codebase together. A consistent set of rules reduces mental load, lowers friction in reviews, and helps new contributors match the project’s voice. This chapter collects pragmatic guidance that you can adopt as is or adapt to your team’s needs. Favor rules that prevent bugs and smooth collaboration; avoid rules that only satisfy taste.

Naming, layout, and comments

Names are contracts. Choose names that tell the reader what a thing does, not how it is implemented. Keep layout calm and predictable so that change diffs are small and easy to read. Use comments to explain the why; let code show the how.

Naming rules

Use lowercase_with_underscores for functions and variables, PascalCase for types and concepts, and ALL_CAPS for macros. Prefer verbs for functions and nouns for types. Keep abbreviations rare and obvious (id, URL, CPU). Avoid negative booleans because they invert logic twice in conditions.

struct FileReader { /* ... */ };
auto read_file(std::string_view path) -> std::expected<std::string, std::error_code>;
bool is_ready = true;  // not: ready_flag, not_ready
💡 When a name feels hard to choose, the design may be vague. Sketch the public usage first, then name the pieces to fit that sketch.

Layout and whitespace

Keep lines under a sensible limit, for example 100 or 120 characters. Break long template parameters and argument lists one per line with trailing commas. Indent with spaces consistently. Place braces in a style that your team agrees on, then stick to it.

auto make_map() -> std::unordered_map<std::string, std::vector<int>> {
  return {
    {"alpha", {1, 2, 3}},
    {"beta",  {4, 5, 6}},
  };
}

Commenting guidelines

Write API comments for public interfaces and tricky invariants. Avoid comments that restate the code. Use block comments for high level notes and short end of line comments for delicate conditions. Keep a changelog of design decisions in markdown rather than burying history inside functions.

⚠️ Do not let comments drift. When code changes, update or remove nearby comments in the same commit so that they remain trustworthy.

Header hygiene and #include order

Headers shape build performance and binary hygiene. A clean header includes only what it needs, exposes minimal surface area, and compiles on its own. Include order affects reproducibility and reveals hidden dependencies.

Self containment

Every public header must compile on its own when placed in an empty translation unit. Forward declare where possible to avoid dragging in heavy transitive headers. Prefer including headers in source files rather than headers that include other headers.

// good: forward declaration is enough
namespace foo { class Widget; }
void use_widget(const foo::Widget&);

// needs full definition, so include in the .cpp
// #include "foo/widget.hpp"

#include order

Use a consistent order: the header’s own header first when in a source file, then related project headers, then third party headers, then standard library headers. Separate groups with a blank line. This exposes missing includes quickly and keeps diffs readable.

// in src/bar.cpp
#include "bar.hpp"

#include "core/util.hpp"

#include <fmt/format.h>

#include <vector>
#include <string>

Guards and visibility

Use #pragma once when available, otherwise classic include guards. Do not put non inline variables in headers. Keep inline functions tiny and stable to avoid code bloat across translation units.

💡 Inspect headers with tooling. Run include-what-you-use or similar linters in CI to prevent accidental dependencies from creeping in.

API design principles

Good APIs feel obvious once seen. They prefer value semantics, express clear ownership, and fail in predictable ways. When in doubt, start with the simplest shape that solves a real user task and expand only when real use cases require it.

Surface area and stability

Keep the public surface small. Hide helpers in an internal namespace or in private modules. Mark unstable parts as experimental in names or in documentation. Once released, evolve by addition and deprecation, not by surprise removal.

Ownership and parameters

Use std::string_view for read only string parameters, use std::span<T> for contiguous ranges, and return owning values where practical. Accept sinks by template or concept so callers can pass vectors, back inserters, or streams without friction.

auto tokenize(std::string_view s, char delim) -> std::vector<std::string>;
auto write_all(std::span<const std::byte> data, auto& sink) -> std::error_code;

Errors and contracts

Document preconditions and postconditions in a single sentence near each function. For recoverable problems, use std::expected or std::error_code. Reserve exceptions for programming errors and rare failures where unwinding to a safe boundary improves clarity.

⚠️ Never hide errors. If a function can fail, return a result that forces the caller to inspect success or failure, or make failure impossible by construction.

Safety first defaults

Prefer patterns that prevent bugs by default. Make it hard to use APIs incorrectly. Choose types that encode intent and lifetimes so that mistakes are caught at compile time.

Initialize everything

Use brace initialization for objects and members. Default construct with valid states. Mark constructors explicit when a single argument could surprise readers. Prefer enum class to plain enums so that scopes remain clear.

struct Config {
  int port{0};
  std::string host{"localhost"};
  std::chrono::milliseconds timeout{1000};
  explicit Config(int p) : port{p} {}
};

Constrain templates

Add concept constraints to templates so that misuse stops at the call site. Lean on standard concepts such as std::ranges::range, std::regular, and std::invocable. Provide static asserts with friendly messages for edge cases that concepts do not express well.

template<std::ranges::range R, std::invocable<std::ranges::range_value_t<R>> F>
auto map(R&& r, F f) {
  /* … */
}

Immutable by default

Pass inputs as const& or views and return new values instead of mutating in place unless profiling proves otherwise. Keep shared ownership rare; prefer unique ownership with clear transfer points. Mark functions noexcept only when you can uphold the guarantee.

💡 Combine small safe defaults into checklists in your repository so that newcomers learn the project’s habits without reading a long essay.

Code review checklists

Reviews catch defects, spread knowledge, and keep the style steady. A short checklist turns fuzzy goals into concrete checks that teams can apply quickly. Keep the list brief so that it is used every time.

Suggested checklist

Embed this list in your pull request template and adapt it as your project grows.

AreaChecks
NamingDo names describe purpose; are types and functions consistent with the guide
OwnershipAre lifetimes clear; are unique_ptr and span used where appropriate
ErrorsAre failures reported with std::expected or std::error_code; are contracts documented
HeadersDoes each header compile alone; are includes minimal and ordered; no hidden dependencies
APIsIs the surface minimal; are parameters expressive; is behavior testable
SafetyAre objects initialized; are templates constrained; are conversions explicit
PerfAny obvious needless copies; hot paths measured; allocations visible
DocsComments explain intent; public functions have one sentence summaries
BuildNo new warnings; CI passes; tools such as formatters and linters run clean

Review tone and process

Keep feedback specific and kind. Point at code and outcomes rather than people. Suggest concrete changes with short rationale. Small focused pull requests review faster and merge with fewer conflicts, so prefer incremental changes over large surprise drops.

Appendix A: Language Reference Tables

This appendix gathers compact reference tables that you can consult quickly while working. They summarize essential facts without long explanation. Earlier chapters give full detail; the tables here provide fast reminders during coding or review.

Keywords and reserved identifiers

C++ reserves a small core of keywords for language features and implementation needs. These names cannot be used for your own identifiers. Some are contextual and appear only in templates or special constructs. The list below reflects the common set from current C++ standards.

Core keywords

The following table lists the fundamental keywords that appear in code across the language’s main features. They map to control flow, declarations, types, and basic operators.

KeywordPurpose
autoType deduction for variables and return types
boolBoolean type
breakExit the nearest loop or switch
caseLabel in a switch statement
catchException handler block
charCharacter type
classUser defined class type
constRead only qualifier
continueSkip to the next loop iteration
decltypeInfer type from an expression
defaultDefault label in switch or defaulted functions
deleteDestroy a dynamically allocated object
doStart a do while loop
doubleDouble precision floating point
elseAlternative path in if statements
enumEnumeration type
explicitDisable implicit conversions
exportExpose module interfaces
externExternal linkage specifier
falseBoolean false literal
floatFloating point type
forFor loop construct
friendAccess control exception for classes
gotoJump to a label
ifConditional branching
inlineInline expansion hint
intInteger type
longLong integer type
mutableAllow modification in const objects
namespaceLogical grouping of declarations
newAllocate objects dynamically
noexceptDeclare non throwing functions
nullptrNull pointer literal
operatorDefine overloaded operators
privatePrivate access level
protectedProtected access level
publicPublic access level
returnReturn from a function
shortShort integer type
signedSigned integer qualifier
sizeofSize of an expression or type
staticStatic storage or linkage
structStructure type
switchMulti way branching
templateDefine templates
thisPointer to current object
throwRaise an exception
trueBoolean true literal
tryStart a guarded block for exceptions
typedefAlias a type
typenameType placeholder in templates
unionStorage overlay type
unsignedUnsigned integer qualifier
usingIntroduce names or type aliases
virtualDeclare virtual functions
voidEmpty type
volatilePrevent certain optimizations
whileLoop while a condition holds
💡 Reserved identifiers include any name beginning with an underscore followed by a capital letter or two consecutive underscores. Avoid them entirely to keep code portable.

Operator precedence and associativity

Operator precedence determines how expressions bind when parentheses are absent. Associativity resolves ties at the same level. When expressions become dense, add parentheses for clarity even when precedence rules cover the case.

Precedence table

The following table shows a compact precedence ladder. Rows near the top bind more tightly than rows near the bottom. The associativity column explains the direction rules when operators share a level.

OperatorsAssociativity
::Left to right
++ -- () [] . ->Left to right
! ~ + - * & sizeof noexcept new deleteRight to left
* / %Left to right
+ -Left to right
<< >>Left to right
< <= > >=Left to right
== !=Left to right
&Left to right
^Left to right
|Left to right
&&Left to right
||Left to right
?:Right to left
= += -= *= /= %= &= |= ^= <<= >>=Right to left
,Left to right
⚠️ Operator overloading does not change precedence or associativity. Overloaded operators follow the same rules as their built in versions.

Numeric limits and sizes

The exact sizes of integer and floating point types vary by platform, but common implementations follow patterns. The table provides typical values from platforms that use 8 bit bytes and two’s complement integers. Always confirm with std::numeric_limits<T> at compile time if your code depends on exact ranges.

Common sizes

The table shows representative bit widths and ranges, not strict guarantees. Your platform may differ.

TypeTypical bitsApproximate range
char80 to 255 or -128 to 127
short16-32768 to 32767
int32-2 billion to 2 billion
long32 or 64Platform dependent
long long64About -9e18 to 9e18
float32Approx 1e-38 to 1e38
double64Approx 1e-308 to 1e308
long double64 to 128Platform dependent
std::size_t32 or 64Non negative, depends on pointer size
std::ptrdiff_t32 or 64Signed difference type
💡 Use fixed width types such as std::uint32_t or std::int64_t when exact sizes matter, for example in serialization formats.

Compiler flag crib sheet

Compilers offer flags that control standards, warnings, optimization, sanitizers, and debugging. The tables here list commonly used flags for GCC, Clang, and MSVC. Exact spellings may vary slightly by version.

Language and warnings

Choose a standard version explicitly and enable strong warnings during development. These flags help catch mistakes before runtime.

CompilerFlags
GCC-std=c++23, -Wall, -Wextra, -Wpedantic
Clang-std=c++23, -Wall, -Wextra, -Wpedantic
MSVC/std:c++23, /W4, /permissive-

Optimization and debugging

Switch between optimization levels depending on context. Fast builds use no optimization; release builds use higher levels. Include debugging information during development.

CompilerFlags
GCC-O0, -O2, -O3, -g
Clang-O0, -O2, -O3, -g
MSVC/Od, /O2, /Zi

Sanitizers

Sanitizers catch memory errors, undefined behavior, and threading races. Use them in CI and during development to prevent subtle bugs from reaching production.

PurposeFlags
Address checks-fsanitize=address
Undefined behavior-fsanitize=undefined
Thread safety-fsanitize=thread
Leak detection-fsanitize=leak
⚠️ MSVC does not support the same sanitizers as GCC and Clang. For Windows builds, combine static analysis, runtime checks, and third party tools to achieve similar coverage.

Appendix B: Migration Guides

These guides offer practical paths for upgrading old codebases. Each section focuses on specific transitions that teams commonly face. The aim is to preserve behavior while adopting safer and clearer constructs. Treat each guide as a menu rather than a mandate; choose steps that match your project’s constraints and timelines.

From C to C++ safely

C and C++ share many primitives, yet they model resources and errors in very different ways. A careful migration keeps the low level strengths of C code while lifting it into safer abstractions. The steps below help you avoid brittle rewrites and keep risks contained.

Start with compatible subsets

Begin by compiling your C code as C++ with only minimal changes. Fix the obvious errors first: implicit int, incompatible pointer casts, mixed declarations and statements, and function pointer mismatches. Resolve global name collisions by introducing namespaces and wrapping headers with extern "C" where needed.

// c_compat.hpp
extern "C" {
#include "legacy_api.h"
}
💡 Keep C compilation available during the early phase. Building as both C and C++ makes unintended C++ specific behavior easier to spot.

Adopt RAII for resources

Translate malloc and free pairs into smart pointers or resource wrappers. Use std::unique_ptr with custom deleters for objects that must follow C allocation rules. Replace manual cleanup paths with RAII so errors cannot skip cleanup logic.

using CHandle = std::unique_ptr<CObj, void(*)(CObj*)>;

CHandle make_handle() {
  return CHandle(create_cobj(), destroy_cobj);
}

Encapsulate unsafe interfaces

Wrap raw C APIs in thin C++ layers that hide pointer arithmetic and manual lifetime management. Turn arrays plus lengths into std::span; turn error codes into std::expected. Keep wrappers minimal so that behavior remains close to the original C API.

⚠️ Avoid rewriting logic wholesale until wrappers and tests confirm behavior. Incremental changes reduce regressions.

Introduce value types

Gradually replace ad hoc structs with small classes that enforce invariants. Provide constructors that establish valid states and give each type a clear ownership model. Start with uncontroversial types such as configuration objects and simple data carriers.

From C++11/14 to C++20 and beyond

Moving from older C++ standards to C++20 and later brings major productivity gains. You gain modules, concepts, ranges, constexpr expansion, and clearer concurrency tools. The transition works best when done in waves rather than a single large jump.

Audit build features

Enable the newer standard in your build system and fix any deprecated features that surface. Update third party libraries that assume older dialects. Check that your compilers and CI runners support the required flags and module scanning.

# CMakeLists.txt …
set(CMAKE_CXX_STANDARD 23)

Replace SFINAE with concepts

Concepts provide readable constraints that remove the need for verbose SFINAE expressions. Retire templates that rely on enable_if pyramids or tag dispatch when concepts express intent more clearly.

// old
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
T twice(T x){ return x + x; }

// new
template<std::integral T>
T twice(T x){ return x + x; }

Adopt ranges

Ranges replace iterator boilerplate and encourage pipeline style transformations. Convert simple loops first. Keep conversions small so that behavioral differences are easy to track. Use views and lazy evaluation to avoid needless allocations.

Strengthen constexpr usage

C++20 and later allow more functions and data structures to become constexpr. Identify pure computations that benefit from compile time evaluation and mark them accordingly. This clarifies intent and can improve performance in hot paths.

💡 When migrating, track the ratio of legacy loops and utilities that move to ranges and concepts. Small visible wins help teams commit to the new idioms.

Replacing legacy idioms with modern tools

Modern C++ offers libraries and idioms that improve safety and clarity compared with older patterns. The following swaps help reduce technical debt without altering high level behavior.

Raw pointers to smart pointers

Replace owning raw pointers with std::unique_ptr or std::shared_ptr. Retain raw pointers only for observed non owning access. This clearly separates lifetimes and prevents accidental deletes.

// legacy
foo* make();

// modern
std::unique_ptr<foo> make();

Manual arrays to std::vector and std::array

Use std::vector for dynamic sequences and std::array for fixed size data. These containers manage lifetime, bounds, and exception safety. Pair them with std::span when passing views into algorithms.

Loops to algorithms and ranges

Replace simple loops with std::ranges:: algorithms. This removes bookkeeping, reduces off by one errors, and makes intent clearer. Keep complex loops intact where readability would suffer from a forced algorithm.

⚠️ Never convert loops purely for style points. Ensure that the new form explains the operation more clearly than the old form.

Macros to inline functions and constants

Macros hide types and scopes. Whenever possible, replace them with constexpr variables or inline functions so that the compiler can check types and lifetimes. This improves debugging because breakpoints behave predictably.

Common portability traps

Portability issues arise when code assumes behavior that varies across compilers, platforms, or libraries. Identifying these assumptions early saves time when targeting new systems. The table and notes below highlight frequent trouble spots.

Undefined and implementation defined behavior

Certain constructs behave differently across platforms. Keep these areas in mind when auditing older code.

AreaRisk
Signed overflowUndefined; may wrap, trap, or optimize away
Object aliasingStrict aliasing rules may break pointer casts
Order of evaluationSome nested expressions evaluate operands in different orders
Char signednessPlatform dependent; affects indexing and comparisons
Thread schedulingTiming assumptions differ across OSes

Filesystem and path quirks

Paths vary in separators, case sensitivity, and length limits. Use std::filesystem::path to abstract details where possible. Normalize user input and avoid hardcoded separators in low level code.

Endianness and data formats

Binary protocols and file formats often specify byte order. Convert with explicit functions and avoid writing plain structs to disk unless you control the entire ecosystem. Check for padded fields and alignment differences when exchanging data across languages.

💡 Build on multiple compilers and at least two architectures in CI. Even a small matrix catches assumptions that slip by on a single platform.


© 2025 Robin Nixon. All rights reserved

No content may be re-used, sold, given away, or used for training AI without express permission

Questions? Feedback? Get in touch