diff --git a/book/src/appendix-b.md b/book/src/appendix-b.md index c4e9dae..9a8abe6 100644 --- a/book/src/appendix-b.md +++ b/book/src/appendix-b.md @@ -16,7 +16,7 @@ This appendix maps what you already know from C, Rust, Lisp, and Prolog onto the C code works in C++ through the boundary discipline of chapter 28: `extern "C"` for linkage, spans and string views for C data, and RAII wrappers for C resources. -The pattern is consistent. C++ keeps the C calling conventions and adds ownership. You replace manual resource handling with RAII, and you replace unchecked format strings with compile‑time checked formatting. The C idioms you know map onto a safer C++ equivalent in every row above. +C++ retains C calling conventions, adds ownership, and replaces manual resource handling and unchecked formatting with RAII and compile‑time checked formatting. ## From Rust @@ -30,9 +30,9 @@ The pattern is consistent. C++ keeps the C calling conventions and adds ownershi | Enums with data | `std::variant` plus `std::visit` (ch03) | | Traits | concepts and `requires` clauses (ch17) | -The big difference is that Rust enforces ownership and lifetimes in the language, while C++ gives you the tools and relies on the lifetime profile and disciplined APIs. Chapter 7 makes that trade explicit. +Rust enforces ownership and lifetimes at compile time. C++ provides the same tools but relies on the lifetime‑safety analysis and disciplined APIs (see Chapter 7). -Rust and C++ share the same mental model of ownership. The difference is enforcement. Rust rejects a program that violates the rules at compile time. C++ accepts the program and relies on the lifetime‑safety analysis and on your discipline to catch the violation. +Both languages share the same mental model of ownership. The difference is where a violation is caught. ## From Lisp @@ -44,9 +44,9 @@ Rust and C++ share the same mental model of ownership. The difference is enforce | Functions as data | lambdas, `std::function`, and type erasure (ch14) | | Recursive macros / term rewriting | template metaprogramming and pack expansion (ch16, ch21) | -The mental model carries over almost unchanged: Lisp macros consume source and emit code before the program runs, and C++ templates and `constexpr` consume types and values and emit specialized code before the program runs. +C++ templates and constexpr supply compile‑time code generation analogous to Lisp macros. -The scale differs. Lisp macros rewrite source text. C++ templates rewrite type patterns, and `constexpr` evaluation runs a restricted subset of the language at compile time. Both give you a program that computes before the program runs. +Lisp rewrites source text. C++ rewrites type patterns and evaluates a restricted subset of the language at compile time. ## From Prolog diff --git a/book/src/appendix-d.md b/book/src/appendix-d.md index c3823fc..aa62fdf 100644 --- a/book/src/appendix-d.md +++ b/book/src/appendix-d.md @@ -124,4 +124,4 @@ The warning flags come from a list that includes the lifetime‑safety flag. The ## Try this -Open `cmake/BookExample.cmake` and trace how `book_demo` differs from `book_example`. Explain why a demo compiles with its teaching warning visible while an example must build clean under `-Werror`. Then run `cmake --preset dev -DBOOK_SANITIZE=OFF` and confirm that the generated build still compiles the examples without the sanitizer flags. +Open `cmake/BookExample.cmake`, compare `book_demo` to `book_example`, then run `cmake --preset dev -DBOOK_SANITIZE=OFF` to verify the build succeeds without sanitizer flags. diff --git a/book/src/ch01-toolchain.md b/book/src/ch01-toolchain.md index 5c84cf5..dca1421 100644 --- a/book/src/ch01-toolchain.md +++ b/book/src/ch01-toolchain.md @@ -2,7 +2,7 @@ ## The thesis -This book presents the tooling around C++ as a single entity. The clang++ front‑end, the clang‑tidy lint suite, the path‑sensitive static analyzer, the address and undefined‑behavior sanitizers, and the clangd language server together form the compiler committee. A diagnostic that originates from any of these components is a violation of the language law, not a mere suggestion. The Core Guidelines describe rules that the committee intends to make machine enforceable, and the committee enforces them. +This book treats the C++ toolchain as a single committee: clang++ front‑end, clang‑tidy, path‑sensitive static analyzer, address and undefined‑behavior sanitizers, and clangd. The clang++ front‑end, the clang‑tidy lint suite, the path‑sensitive static analyzer, the address and undefined‑behavior sanitizers, and the clangd language server together form the compiler committee. A diagnostic that originates from any of these components is a violation of the language law, not a mere suggestion. The Core Guidelines describe rules that the committee intends to make machine enforceable, and the committee enforces them. ``` clang++ front-end compiler @@ -12,7 +12,9 @@ ASan / UBSan runtime sanitizers clangd language server ``` -The mental model mirrors the Rust experience where the borrow checker, clippy linter, and cargo‑test runner cooperate to keep code correct. In the Rust ecosystem the borrow checker enforces lifetimes, clippy provides style and correctness lints and cargo test runs the unit test suite. In the C++ world those responsibilities are split across clang++ flags, clang‑tidy checks, the static analyzer and sanitizers, but the book treats them as one cohesive compiler surface. +The mental model mirrors the Rust experience where the borrow checker, clippy linter, and cargo‑test runner cooperate to keep code correct. In the Rust ecosystem the borrow checker enforces lifetimes, clippy provides style and correctness lints, and cargo test runs the unit test suite. + +In the C++ world those responsibilities are split across clang++ flags, clang‑tidy checks, the static analyzer and sanitizers, but the book treats them as one cohesive compiler surface. ## Getting the toolchain @@ -26,7 +28,9 @@ When Nix is unavailable the book falls back to system packages. On macOS the use ## Hello, world, honestly -The book writes `#include ` because the header `` is the modern way to reach `std::println`. The directive `#include` is the build model the book uses, because the classic preprocessor workflow is what real multi‑file projects run today. The preprocessor expands `#include` before compilation. The preprocessor gets full treatment in chapter 23, and modules in chapter 24. +The book writes `#include ` because the header `` is the modern way to reach `std::println`. The directive `#include` is the build model the book uses, because the classic preprocessor workflow is what real multi‑file projects run today. The preprocessor expands `#include` before compilation. + + The preprocessor gets full treatment in chapter 23, and modules in chapter 24. `std::println` replaced `std::cout` and `printf` as the default output mechanism in C++23. It checks format strings at compile time, writes directly to stdout, and returns void. The older facilities still work, but the book uses the modern one from the first example so the reader never has to unlearn a habit. @@ -41,7 +45,7 @@ clang++ -std=c++26 -Wall -Wextra -Wpedantic -c ch01_hello.cpp clang++ -std=c++26 -Wall -Wextra -Wpedantic ch01_hello.o -o ch01_hello ``` -The short command line shows the language version flag, the three warning groups that the book treats as law, and the absence of any additional options. The resulting binary prints `hello, world` on standard output. The function `std::println` formats its arguments using the same machinery as `std::format`, writes to `stdout` and returns `void`. The header `` became part of the standard library in C++23 and is fully supported by the libc++ shipped with the pinned toolchain. +The short command line shows the language version flag, the three warning groups that the book treats as law, and the absence of any additional options. The resulting binary prints `hello, world` on standard output. The header `` became part of the standard library in C++23 and is fully supported by the libc++ shipped with the pinned toolchain. ## Warnings are laws diff --git a/book/src/ch02-values-functions.md b/book/src/ch02-values-functions.md index edc8a79..4a662dd 100644 --- a/book/src/ch02-values-functions.md +++ b/book/src/ch02-values-functions.md @@ -3,9 +3,7 @@ A C++ program is a graph of functions that exchange values. This book treats the function as the atomic unit of design: a named operation on values, written once and called from anywhere, not a method bolted to a class. Classes appear later -and stay rare. If you come from Rust, read "function" as `fn`. If if you come from -C, the difference is that returning rich values (tuples, structs, containers) is -cheap and normal, so C's out-parameters and pointer-returning habits fall away. +and stay rare. If you come from Rust, read "function" as `fn`. If you come from C, returning rich values (tuples, structs, containers) is cheap and normal, so C’s out‑parameters and pointer‑returning habits fall away. ## A function is one logical operation @@ -17,14 +15,7 @@ split, and the one-definition rule that enforces it. The guideline is deliberately narrow: one function does one logical operation. -Designing functions that do one thing makes them easy to test. Each function can be exercised in isolation. This reduces hidden coupling. -`draw_triangle` is a function. `draw_triangle_and_clear_screen_and_log` is not. -The compiler cannot check this. It is the first design rule you apply by hand, -and every later rule (overloading, concepts, error handling) assumes it holds. This habit encourages modular design. - -A function that follows this guideline isolates a single responsibility. It can be reasoned about without considering unrelated code. When the function grows, split it into helper functions. This keeps each piece testable and readable. The compiler can inline small functions, eliminating call overhead. Developers benefit from clear intent and easy maintenance. - -The book adopts this approach consistently, so readers see examples that illustrate the principle in practice. +Design each function to perform a single logical operation. This makes the function easy to test in isolation and reduces hidden coupling. For example, `draw_triangle` is a function, while `draw_triangle_and_clear_screen_and_log` is not. The compiler cannot enforce this rule. It relies on the programmer. When a function grows, extract helper functions so each piece remains testable and readable. Small functions can be inlined, eliminating call overhead. ## Return by value is the default @@ -218,35 +209,31 @@ and the book applies it everywhere. ## Function naming and error handling -Function names must describe the action performed. Use a verb phrase in lower‑case snake_case, for example `read_file` or `calculate_checksum`. Steer clear of generic names like `procedure` unless the surrounding context makes the purpose clear. Consistent naming helps readers locate functionality quickly. - -Error handling follows the book’s policy: use `auto` return types with `std::expected` for recoverable errors and exceptions for unrecoverable failures. When an exception propagates, include a descriptive message that identifies the operation that failed. This approach gives callers the information needed to decide whether to retry, abort, or translate the error to another domain. - -The guidelines encourage validating arguments at the start of a function. Use `if (!condition) return std::unexpected{error_code};` for expected failures, and `throw std::runtime_error("invalid argument")` for programmer errors. This defensive style catches bugs early and makes the code self‑documenting. - -By keeping names precise and handling errors explicitly, functions become reliable building blocks for larger systems. +Name functions with a lower‑case verb phrase that describes the action, e.g., `read_file` or `calculate_checksum`. Avoid generic names. For recoverable errors return `std::expected`. For unrecoverable failures throw an exception that includes a message identifying the failed operation. ## References are aliases, not pointers `int& r = x;` gives `r` a second name for `x`. Reads and writes through `r` reach the same object. A reference is not a pointer you must dereference, and it -cannot be reseated after initialization. In Rust terms it is a reborrow. A -reference parameter hands the caller's object to the callee, which can read or +cannot be reseated after initialization. In Rust terms it is a reborrow. + +A reference parameter hands the caller's object to the callee, which can read or modify it. The full decision of *what* to pass (value, `const` reference, or `std::span`) is chapter 8. For now, the rule is that a reference means "I am" using your object," never "I own a copy. ## Practical tips for writing functions -When you design a function, start by naming the operation you want to perform. Choose a name that describes the effect in one verb phrase. Keep the parameter list short. Prefer passing a `const` reference for large objects and a value for small trivially copyable types. Return a value when the caller needs a result. If the function produces no observable result, make the return type `void`. +When designing a function, choose a clear verb‑phrase name, keep parameters short, pass large objects by `const` reference and small trivially copyable types by value, and return a value when needed. -Document the preconditions in a comment above the signature. Use `static_assert` inside the function to enforce template constraints. Test the function with a variety of inputs, including edge cases. Review the implementation for hidden state modifications. This checklist helps maintain clarity and safety. +Use `void` for functions that produce no observable result. ## Common pitfalls -Steer clear of returning a reference to a local variable. The compiler cannot guarantee the lifetime of the object after the function returns. Do not store a pointer to a parameter that the caller can destroy before the function returns. Be careful with default arguments that depend on mutable global state. They can introduce hidden dependencies. Do not overload functions in a way that obscures the intended call site. Overloads that differ only by const qualification can be confusing. Keep overload sets small and well documented. - -> **WARNING** +* Do not return a reference to a local variable. The reference dangles after the function returns (see the WARNING above). +* Do not store a pointer to a parameter that the caller can destroy before the function returns. +* Avoid default arguments that depend on mutable global state, as they create hidden dependencies. +* Keep overload sets small and well‑documented. Overloads that differ only by const qualification can be confusing. ## Performance considerations diff --git a/book/src/ch03-user-defined-types.md b/book/src/ch03-user-defined-types.md index dbe645c..0240953 100644 --- a/book/src/ch03-user-defined-types.md +++ b/book/src/ch03-user-defined-types.md @@ -41,13 +41,9 @@ public: }; ``` -No inheritance, virtual functions, or dynamic polymorphism appear. The `private` section prevents accidental mutation that breaks the invariant, while the static factory `make` guarantees a correctly normalised instance. This pattern follows the Core Guidelines advice to keep data encapsulation minimal (C.21) and to prefer plain functions over heavy OO machinery. +The `private` section prevents accidental mutation that can break the invariant, while the static factory `make` guarantees a correctly normalised instance. This follows Core Guidelines C.21 to keep data encapsulation minimal and prefer plain functions over heavy OO machinery. The invariant, that a `vec3` constructed via `make` has length 1, is enforced by the factory. Direct use of the private constructor violates the contract, and the factory also checks for a zero‑length input and rejects it, avoiding a NaN result. -The invariant is the contract the type offers. A `vec3` constructed through `make` always has length 1. A caller that constructs one directly through the private constructor can violate that contract, which is why the constructor is private. The factory is the only way in, and it normalises. This is the small, honest use of `private`: not to hide implementation details, but to enforce a relationship the type system cannot express on its own. - -The factory also guards against a zero-length input. When `a`, `b`, and `c` are all zero, `std::hypot` returns zero and the division produces a NaN. A caller that relies on the invariant receives a value that is not a unit vector. The factory can check this case and reject it, because the factory is the single point where a `vec3` enters existence. This shows why a narrow private section is worth the extra syntax: it moves the check to the one place that must hold. - -The type system cannot represent the invariant of length 1. The compiler cannot prove that every path through the program keeps the vector normalised. So the invariant lives in the factory and in the discipline of the callers. This is the honest trade that the book accepts. It prefers a small amount of runtime checking over a large class hierarchy that hides the same fact. +Because the type system cannot express this invariant, the responsibility rests with the factory and disciplined callers. This trades a small runtime check for the complexity of a large class hierarchy. ## Defaulted comparison and the spaceship diff --git a/book/src/ch04-control-flow.md b/book/src/ch04-control-flow.md index 86f43dc..bf22ba9 100644 --- a/book/src/ch04-control-flow.md +++ b/book/src/ch04-control-flow.md @@ -4,19 +4,9 @@ This chapter examines modern control-flow constructs introduced in C++23 and C++ ## Init-statements -C++23 added init‑statements to give the programmer a way to create a temporary variable that lives only for the condition. The variable is constructed, tested, and then destroyed when the block ends. This pattern removes the need for a separate declaration before the `if`. It also makes the scope of the variable obvious, because the reader sees the definition and the test together. +C++23 introduced init‑statements so a temporary variable can be created, tested, and destroyed within the condition of `if`, `while`, or `for`. A simple call yields compact syntax. For more complex setup a helper returning an RAII object can be used directly. -When the initializer is a simple call, the syntax stays short and readable. When more complex setup is required, a helper function can return an object that manages the resource. The object can then be used directly in the condition. This keeps error handling close to the point where the error is detected. - -Init‑statements also appear in `while` and `for` constructs. In a `while` loop the initializer runs once before the first test. This enables a resource to be acquired and then tested for exhaustion. In a range‑based `for` the init‑statement can bind a temporary range object. This ensures the range lives exactly for the duration of the loop. These forms keep the lifetime of temporary objects tightly scoped and avoid accidental reuse outside the loop body. - -Both forms encourage the programmer to keep the creation and consumption of a value together. This reduces the mental load when reading code because the reader does not have to search elsewhere for the definition of the variable. The pattern also helps the compiler to reason about lifetimes, which can enable better optimizations and safer code. - -When the initializer is a simple expression, the syntax remains compact. For more complex resource acquisition, the initializer can call a factory function that returns a RAII object. The resulting object can then be used directly in the condition. This provides a clean separation of concerns. - -The same idea applies to `for` loops that iterate over a range. By binding the range in the init‑statement, the loop body cannot accidentally reference a stale range object. This design aligns with the guideline to declare a variable as close as possible to its point of use. - -These forms also improve readability because the variable’s lifetime is obvious from the surrounding code. The compiler can also apply more aggressive optimizations when it sees that a temporary does not escape the condition. +Init‑statements also appear in `while` and range‑based `for`. In a `while` loop the initializer runs once before the first test, allowing resource acquisition followed by exhaustion testing. In a `for` loop the initializer can bind a temporary range object, ensuring the range lives exactly for the loop’s duration. This keeps lifetimes tightly scoped and prevents accidental reuse outside the loop body. ### Example: init‑statement in an `if` @@ -69,10 +59,6 @@ When performance is critical, a `switch` with contiguous case values can be fast The guidelines suggest preferring `visit` when dealing with sum types and reserving `switch` for simple, closed enumerations where the intent is to map each constant to a distinct branch. -## `switch` need not switch on an enum - -Historically `switch` was tied to integral types and enums. The language lets `switch` on any integral or character value, but the construct is limited: it cannot directly handle discriminated unions or sum types. For such cases C++23 introduced `std::visit`, which dispatches to the appropriate visitor overload based on the active alternative of a `std::variant`. The Core Guidelines (ES.70) advise to favor `visit` over `switch` when dealing with variant-like data because `visit` guarantees exhaustiveness and compiles even when new alternatives are added. - ```cpp std::variant v = 3.14; std::visit([](auto&& arg){ @@ -80,7 +66,7 @@ std::visit([](auto&& arg){ }, v); ``` -For range-based algorithms the idiom is to replace a `switch` that branches on element values with a standard algorithm such as `std::count_if` or `std::transform`. The algorithm expresses *what* it counts or transforms, not *how* it iterates. +For range‑based algorithms replace a `switch` that branches on element values with a standard algorithm such as `std::count_if` or `std::transform`. The algorithm expresses *what* to compute, not *how* to iterate. ## Ranges-`for` is the only loop you write @@ -102,17 +88,11 @@ The FizzBuzz program below runs over the view `iota(1,21)`, which generates the ## `if constexpr` -C++17 introduced `if constexpr`, a compile-time conditional. The branch whose condition evaluates to false is discarded before the program is instantiated, so the discarded code does not need to compile. The construct reads like a regular `if`, but the predicate must be a constant expression. In later chapters (`ch16+`) we unpack this mechanism to select overloads, enable/disable members, and perform type‑level dispatch. Think of it as a Lisp‑style `cond` that resolves during macro expansion. - -The primary advantage of `if constexpr` is that it lets you write generic code that adapts to the properties of its template arguments without requiring separate specialisations. When a condition depends on a type trait such as `std::is_integral_v`, the compiler evaluates the condition at compile time and includes only the matching branch. This means that code that is invalid for some types is never instantiated, eliminating the need for SFINAE tricks. - -A common pattern is to provide a single `print` function that handles both scalar values and ranges. By testing `std::ranges::range` inside an `if constexpr`, the function can either output the value directly or iterate over the range with a range‑based loop. The unused branch is removed, so the binary contains only the relevant logic. - -Because the decision is made at compile time, the optimizer can inline the selected branch and remove any dead code. The result is tighter and faster executables. The source remains clear, as the two alternative implementations are visible side by side, each guarded by a concise predicate. +C++17 introduced `if constexpr`, a compile‑time conditional. The false branch is discarded before instantiation, so it need not compile. This lets generic code adapt to template arguments via constant‑expression predicates such as `std::is_integral_v`. The selected branch can be inlined, eliminating dead code and improving optimisation. -When combined with concepts, `if constexpr` becomes a useful tool for expressing constraints directly in the function body. For example, a function can check `requires { typename T::value_type; }` to decide whether to treat the argument as a container. This keeps the concept checks close to the code that depends on them, improving ease of maintenance. +A typical use tests `std::ranges::range` to choose between printing a scalar value or iterating a range. Concepts can be combined, e.g. `requires { typename T::value_type; }`, to keep constraints close to the code they govern. -Overall, `if constexpr` brings compile‑time decision making into the same syntactic form as a regular `if`. The intent of the code is immediately apparent, and the form preserves type safety and enables aggressive optimisation. +Overall, `if constexpr` provides clear, type‑safe compile‑time dispatch without separate specialisations. ## Raw loops are a smell diff --git a/book/src/ch05-ownership-move-raii.md b/book/src/ch05-ownership-move-raii.md index 26169bf..acae0dd 100644 --- a/book/src/ch05-ownership-move-raii.md +++ b/book/src/ch05-ownership-move-raii.md @@ -2,7 +2,9 @@ ## Value semantics as the default mental model -C++ treats a variable as the sole owner of the value it stores. The compiler creates the value when the variable is defined and destroys it when the variable's lifetime ends. This model matches the Rust model without the borrow checker: a name owns a value. passing the name to a function transfers the value or copies it. The transfer occurs by invoking a move constructor or a copy constructor. The compiler later checks that every move obeys the lifetime-safety rules introduced in Chapter 1. +C++ treats a variable as the sole owner of the value it stores. The compiler creates the value when the variable’s defined and destroys it when the variable’s lifetime ends. This model matches the Rust model without the borrow checker: a name owns a value. + +Passing the name to a function transfers the value or copies it. The transfer occurs by invoking a move constructor or a copy constructor. The compiler later checks that every move obeys the lifetime‑safety rules introduced in Chapter 1. Contrast this model with raw C pointers. A pointer refers to memory that any code path can own. The language does not track that ownership. Programmers conventionally treat the pointer as borrowed, but the compiler cannot enforce that rule. Consequently, dangling pointers and double frees appear frequently in legacy code. @@ -10,21 +12,12 @@ In modern C++ code, the default assumption is **ownership per value**. When a fu The contrast with Rust is direct. Rust's borrow checker proves at compile time that no two owners exist for the same value and that no reference outlives its referent. C++ has no borrow checker, so the same invariants rest on type design: owning types delete copy, non-owning views borrow without duplicating, and the lifetime analysis flags dangling references. The mental model is the same. The enforcement differs. -## The six special member functions - -A type can define up to six special member functions: a default constructor, a destructor, a copy constructor, a move constructor, a copy-assignment operator, and a move-assignment operator. This set is the unit of compiler-generated behavior. - -If a class holds only other owning types, such as `std::vector`, `std::string`, or `std::unique_ptr`, the compiler-generated versions are correct. This is the **Rule of Zero** (CG C.20, R.1): you write no special members and the compiler does the right thing. - -When a class manages a raw resource that the language does not know, such as a C `FILE*`, you must intervene. The `file_guard` type in the first example defines a destructor that calls `std::fclose`. It also deletes the copy operations and provides a move constructor that transfers the pointer. This makes the type a **Rule‑of‑Five** class: destructor, move constructor, move assignment, copy‑deleted, copy‑assignment‑deleted. +## Special member functions and the Rule of Zero/Five -The Rule of Zero is not a suggestion. It is the cheapest correct default. A class that holds only standard-library members gets correct copy, move, and destruction for free, and the compiler-generated code is hard to beat by hand. The Rule of Five is the price of owning a raw resource, and it is worth paying only at a system boundary. Inside the program, the right move is to wrap the resource once and let everything else stay Rule of Zero. +A type can define up to six special member functions: default constructor, destructor, copy constructor, move constructor, copy-assignment operator, and move-assignment operator. If a class contains only owning standard‑library members (e.g., `std::vector`, `std::string`, `std::unique_ptr`), the compiler‑generated versions are correct. This is the **Rule of Zero**: write no special members and rely on the compiler. -## Rule of zero is the goal. rule of five is the fallback +If a class manages a raw resource unknown to the language (e.g., a C `FILE*`), it must provide a destructor, delete copy operations, and implement a move constructor that transfers ownership. This follows the **Rule of Five**. The Rule of Zero is the default, cheapest correct choice. The Rule of Five applies only at system boundaries where a raw resource is wrapped. -The book's philosophy is to write as little boiler plate as possible. When a type contains only standard library members, you rely on the compiler. The only time you write a destructor is when you own a non-C++ resource. The `file_guard` example is the exception you reach for only at the boundary with C. All other types, such as `blob`, `std::vector`, and `std::optional`, remain rule-of-zero. - -If you ever need a custom destructor, you must also decide whether copying a value makes sense. Copying a raw handle usually leads to double close. Therefore you delete the copy constructor and copy assignment. Moving a raw handle is cheap and safe. you implement a move constructor that transfers the pointer and nulls the source. ## Value categories: lvalues and rvalues @@ -59,7 +52,7 @@ The noexcept guarantee is a contract between your type and the container. A type ## RAII as the central idiom -**Resource Acquisition Is Initialization** (RAII) states that a resource is obtained in a constructor and released in the matching destructor (CG R.1, R.3). The `file_guard` struct illustrates this pattern: the constructor calls `std::fopen`. the destructor calls `std::fclose`. +**Resource Acquisition Is Initialization** (RAII) states that a resource is obtained in a constructor and released in the matching destructor (CG R.1, R.3). The `file_guard` struct illustrates this pattern: the constructor calls `std::fopen`. The destructor calls `std::fclose`. Contrast the RAII version with manual management: @@ -104,13 +97,6 @@ That pattern appears only in the explanatory paragraph above. All other code rel The reason is not aesthetic. A raw `new` is a leak waiting to happen: every early return, every exception, and every forgotten `delete` leaks the object. A container or smart pointer ties the deallocation to a destructor, which the compiler guarantees to run. The two lines above are the only place in the book where the raw primitives appear, and they exist to make this argument concrete. -## Move semantics and performance - -Move operations transfer resources without copying data. This reduces time spent allocating memory. A move of a `std::vector` copies three pointers: the address, the size, and the capacity. A copy allocates new storage and copies each element. The cost difference can be orders of magnitude for large containers. - -The standard library marks move constructors as `noexcept` when possible. This allows containers to prefer move over copy during reallocation. Marking a custom move constructor `noexcept` improves container performance. - -Understanding these differences helps write efficient code. Prefer pass-by-value when the argument is cheap to move. Use `std::move` to enable move semantics explicitly. ## Try this diff --git a/book/src/ch06-smart-pointers.md b/book/src/ch06-smart-pointers.md index e0b00c3..429d7f0 100644 --- a/book/src/ch06-smart-pointers.md +++ b/book/src/ch06-smart-pointers.md @@ -1,21 +1,13 @@ # Smart pointers and owning views - ## Unique ownership is the default - The Core Guidelines (R.20, R.21) require that a heap object have exactly one owning smart pointer. `std::unique_ptr` satisfies this rule. It holds a pointer, destroys the object when the `unique_ptr` itself is destroyed, and can be moved but never copied. The move operation transfers the stored pointer and leaves the source empty. - ```cpp {{#include ../../examples/ch06/ch06_tree.cpp}} ``` - In the tree example the root is a `std::unique_ptr`. Each node stores its children in a `std::vector>`. Because every child is owned uniquely, the destruction of the root automatically destroys the whole subtree recursively. No manual `delete` appears, and the program cannot accidentally copy a node-owner. The compiler rejects any copy of a `unique_ptr`. -A `unique_ptr` enforces single ownership at compile time. The type is not copyable, so the compiler rejects any attempt to duplicate the owner. This rule prevents double deletion, a common failure mode of raw pointers. When two raw pointers own the same object, both destructors call `delete` and the program crashes. A `unique_ptr` makes this mistake impossible to express. The move operation is the only way to transfer the stored pointer. After a move, the source holds `nullptr` and the destination owns the object. This transfer is cheap, because the move copies only the pointer value. The guidelines (R.21) require that the owner of a resource be clear and unique. - ## Transfer of ownership by value - A function that receives a `unique_ptr` by value *takes* ownership. The caller must move the pointer into the parameter. After the call the caller’s pointer becomes empty. This pattern appears in many factory functions: - ```cpp std::unique_ptr make_root(int v) { auto p = std::make_unique(); @@ -23,82 +15,43 @@ std::unique_ptr make_root(int v) { return p; // move-return, caller receives ownership } ``` - In the tree example the statement `auto root = std::make_unique()` creates the sole owner. When `main` ends, `root` goes out of scope, the move-return chain unwinds, and the destructor of each `unique_ptr` in the vectors frees the corresponding child. No memory leaks survive past `main`. - ### `std::make_unique` versus `new` - The guidelines (R.23) require that a `unique_ptr` be created with `std::make_unique` rather than a raw `new` expression. `std::make_unique(args...)` constructs the object and wraps it in a `unique_ptr` in one step. This form is shorter and safer than the two-step alternative. The two-step form first calls `new T(args...)` and then passes the result to the `unique_ptr` constructor. If an exception occurs between these two steps, the raw pointer leaks. `std::make_unique` avoids this window, because the construction and the wrapping happen together. The function also deduces the type, so the code does not repeat the type name. Use `std::make_unique` whenever the object is created and owned immediately. Reserve a raw `new` for the rare case where a custom deleter or a pre-existing pointer is required. Move semantics give `unique_ptr` its efficiency. Moving a `unique_ptr` transfers the pointer without copying the pointed-to object. The operation is a simple pointer assignment plus a null-out of the source. It never allocates and never touches the heap object. This property lets a function return a `unique_ptr` by value at no cost. The move-return in `make_root` does not copy the tree. It hands the same object to the caller. The same reasoning applies when a `unique_ptr` moves into a container or into another owner. - ## Shared ownership is a cost-aware choice - `std::shared_ptr` holds a control block with an atomic reference count. Every copy increments the counter. The last copy decrements to zero and destroys the object. The guidelines (R.22) caution that `shared_ptr` must be used *only* when at least two distinct owners truly need to keep the object alive. - The cost model is higher than `unique_ptr`: - | Aspect | `unique_ptr` | `shared_ptr` | |---|---|---| | Allocation | one block (object) | two blocks (object + control) | | Reference count | none | atomic increment/decrement on every copy | | Size of pointer object | ≤ sizeof(void*) | ≈ 2 × sizeof(void*) | | Cache behavior | contiguous access | indirect control block | - -If the program never needs more than one owner, `unique_ptr` is the safe, zero-overhead choice. Switching to `shared_ptr` without a real sharing need adds unnecessary atomic operations and heap fragmentation. - -The cost table makes the trade explicit. `shared_ptr` allocates two blocks: the object and the control block. The control block holds the reference count and the deleter. Every copy performs an atomic increment on the count. Every destruction performs an atomic decrement. Atomic operations on a shared counter are slower than a plain pointer copy, because they synchronize across threads. The size of the pointer object also grows to about twice the size of a raw pointer. The extra indirection through the control block hurts cache locality. - -These costs matter only when sharing is real. The guidelines (R.22) state that a `shared_ptr` must be used only when at least two distinct owners truly need to keep the object alive. When one owner suffices, `unique_ptr` delivers the same lifetime guarantee with none of the overhead. The decision is a trade between safety and speed. A `shared_ptr` adds safety for concurrent access, but it adds cost for every copy. Measure the sharing need before you choose. A single owner never justifies the atomic counter. - +If the program never needs more than one owner, `unique_ptr` is the zero‑overhead choice. `shared_ptr` allocates an extra control block and incurs atomic reference‑count updates on each copy, doubling pointer size and reducing cache locality. Use `shared_ptr` only when at least two owners truly need to keep the object alive. Otherwise the extra cost is unnecessary. ## Weak pointers break cycles - -A common pitfall with `shared_ptr` is the creation of a *reference cycle*: two objects each hold a `shared_ptr` to the other, so the reference counts never drop to zero and the objects leak. `std::weak_ptr` solves the problem by providing a non-owning view. It does not affect the reference count. - -The graph example demonstrates a back-edge that forms a cycle if `parent` were a `shared_ptr`. By declaring it as `std::weak_ptr`, the parent can be observed without extending the lifetime: - +`std::weak_ptr` provides a non‑owning view that does not affect the reference count, breaking reference cycles. Use `lock()` to obtain a temporary `shared_ptr` if the object is still alive. Otherwise `lock()` returns an empty pointer. `expired()` reports whether the object has been destroyed. In the graph example, `weak_ptr` allows the parent link to be observed without extending the child’s lifetime, preventing a cycle. ```cpp {{#include ../../examples/ch06/ch06_graph_weak.cpp}} ``` - -When `main` returns, both `a` and `b` are `shared_ptr`s with a count of 1 (each owned by the variable itself). The `weak_ptr` does not contribute to the count, so the control blocks drop to zero and the destructors run. No leak remains, and the program behaves correctly under AddressSanitizer and LeakSanitizer. - -A `weak_ptr` observes an object without owning it. It cannot access the object directly. To read the object, the program must first call `lock()`. The `lock()` member returns a `shared_ptr` if the object is still alive, or an empty `shared_ptr` if the object has already been destroyed. This check is atomic, so the result is safe to use. The `expired()` member reports whether the object is gone. A `weak_ptr` is the correct tool for a back-edge or a cache that must not extend the lifetime of its target. The graph example uses this pattern for the `parent` link. The parent is observable, but it does not keep the child alive, and the child does not keep the parent alive. The cycle is broken at the source. - ## `gsl::owner` documents raw owning pointers - Sometimes a C-language API requires a raw pointer that **owns** the pointed-to object. The Guidelines Support Library provides `gsl::owner` as a type alias that makes the ownership intent explicit to static analysis tools: - ```cpp void c_api(gsl::owner p); // p must be freed by the caller ``` - -`gsl::owner` does **not** change runtime behavior. It is a documentation aid that enables tools such as the Clang lifetime-safety analysis to warn when an owning pointer escapes its intended scope. The book uses this annotation only in side notes, because the primary examples rely on smart pointers. - -The annotation marks the transfer of ownership. A function that takes a `gsl::owner` parameter declares that it takes ownership of the object. A function that returns a `gsl::owner` declares that it hands ownership back to the caller. Static analysis then checks that every owner is released exactly once. The annotation carries no runtime cost, because it is a plain alias. It exists only for the compiler and for analysis tools. When a C API forces a raw pointer, the annotation keeps the ownership contract visible. It turns an implicit rule into a checked one. - +`gsl::owner` is a type alias that documents raw owning pointers for static analysis tools such as Clang's lifetime‑safety analysis. It carries no runtime cost and does not alter program behavior. A function taking a `gsl::owner` parameter signals that it assumes ownership. A function returning `gsl::owner` signals that it transfers ownership to the caller. Static analysis can verify that each owner releases the object exactly once. ## When pointers are the wrong tool - Ownership and lifetime are not the only reasons to use a pointer. Frequently a value, a `std::span`, or a `std::string_view` conveys the required relationship without any ownership semantics. - * **Value**: use when the object’s lifetime is confined to the current scope and copying is cheap. * **`std::span`**: a non-owning view over a contiguous range. It is ideal for passing array slices to functions. * **`std::string_view`**: a read-only view of a string. It is perfect for read-only parameters where the callee must not modify or own the data. - Choosing a smart pointer when a simple view suffices adds unnecessary indirection and can hide bugs. The guidelines (R.3) advise to prefer plain values and views first. Smart pointers come into play only when the lifetime must outlive the current scope **and** no value can express the relation. - -A value expresses ownership when the object lives only inside the current scope. A value is copied or moved by value, so its lifetime is automatic and its storage is local. A `std::span` expresses a view over a contiguous range without owning the elements. It carries a pointer and a length, and it never deletes the data. A `std::string_view` expresses a read-only view of a string. It is cheap to pass and never copies the characters. - -These types remove the ownership question entirely. The caller and the callee agree that neither one owns the data. The guidelines (R.3) state that a raw pointer is used only when the code must refer to an object that outlives the current scope. Prefer the simplest type that expresses the relationship. Add a smart pointer only when the lifetime truly requires manual control. A view keeps the code fast and clear. A value keeps the code safe and simple. A pointer adds power, but it also adds risk. Choose the least capable tool that still fits the task. - +A value owns its data within the current scope. Copying or moving it transfers ownership automatically. `std::span` provides a non‑owning view of a contiguous range, and `std::string_view` provides a read‑only view of a string. These types express the relationship without ownership overhead and avoid the need for raw pointers. Use them before considering a smart pointer. ## Try this - Take the tree program from the previous section and add a function: - ```cpp int height(const tree_node&); ``` - `height` returns the length of the longest root-to-leaf path (the number of nodes on that path). Write the function recursively, using only the `tree_node` interface. When you run the program, observe that the tree is still freed automatically when `main` exits, even though `height` returns a plain `int`. - *What does `std::unique_ptr` guarantee about the tree’s memory after `height` returns?* diff --git a/book/src/ch07-lifetimes.md b/book/src/ch07-lifetimes.md index 67749f7..97d5584 100644 --- a/book/src/ch07-lifetimes.md +++ b/book/src/ch07-lifetimes.md @@ -2,17 +2,15 @@ ## Storage durations in one breath -C++ gives every object a storage duration. The storage duration decides when the object's memory is allocated and when the program reclaims it. Three durations cover almost all code. +C++ classifies every object by its storage duration, which determines when memory is allocated and reclaimed. The three categories cover nearly all cases: -Automatic objects live inside a block. A local variable and a function parameter are automatic. Control creates the object when it enters the block and destroys it when it leaves. The C++ standard ties this to scope. The lifetime of an automatic object is the time the program spends inside its scope. +- **Automatic** objects are created when control enters their block and destroyed on exit. Their lifetime matches the block's scope. +- **Static** objects exist for the program’s lifetime, created before `main` and destroyed after it returns. +- **Dynamic** objects live on the heap, managed by containers or smart pointers that acquire and release the memory. -Static objects exist for the entire program execution. A variable at namespace scope and a variable marked `static` inside a function both have static storage duration. The program creates them before `main` starts and destroys them after `main` returns. +The contrast with C is sharp. In C, `malloc` allocates heap memory without an automatic scope link. The programmer must call `free` at the exact moment, separating pointer and memory lifetimes. C++ eliminates this gap: containers or smart pointers own heap objects, and those owners have automatic or static lifetimes, so reasoning focuses on scope and ownership rather than matching allocation and deallocation. -Dynamic objects reside on the heap. Containers allocate them and destroy them when the container itself dies. The programmer does not name the heap object directly. A `std::vector` owns a block of dynamic memory, and the vector's destructor returns that memory to the allocator. - -The contrast with C is sharp. In C a heap object allocated by `malloc` has no automatic link to any scope. The programmer must call `free` at exactly the right moment, and the lifetime of the pointer value and the lifetime of the allocated memory drift apart. C++ closes that gap. Modern C++ code rarely names dynamic storage at all. A container or a smart pointer owns the heap object, and that owner itself has an automatic or static lifetime. So the programmer reasons about automatic scopes and ownership transfers, not about pairing allocations with deallocations. - -RAII is the bridge between storage duration and resource lifetime. Because an automatic object dies when its block ends, an automatic object that owns a resource releases that resource in its destructor. The lifetime of the resource follows the lifetime of the automatic object. This is why the language can make lifetime a property the compiler understands. +RAII links storage duration to resource lifetime. An automatic object that owns a resource releases it in its destructor when the block ends, so the resource’s lifetime matches the object’s lifetime. This enables the compiler to understand and enforce lifetimes. ## The dangling taxonomy @@ -59,29 +57,26 @@ The function returns a `std::string_view` that refers to a temporary `std::strin The trap is that `std::string_view` is cheap to return, so it invites returning a view into a value the function just made. The rule is to return a `std::string` when the source is a temporary, and to return a `std::string_view` only when the caller already owns the backing storage for the whole time the view is used. -## Demo: lifetime-bound annotation +## Demo: lifetime‑bound annotation ```cpp {{#include ../../examples/ch07/ch07_lifetimebound.cpp}} ``` -The first version returns a view into its parameter without any annotation. The compiler does not warn when the caller passes a temporary, because nothing tells it the returned view borrows from that argument. Adding `[[clang::lifetimebound]]` to the parameter tells the analysis that the returned view is tied to the argument's lifetime. On the current clang the annotation is accepted but the diagnostic remains silent. The programmer must still respect the rule, because the annotation documents a contract that future compilers will enforce. +The first version returns a view into its parameter without any annotation, so the compiler does not warn when a temporary is passed. Adding `[[clang::lifetimebound]]` to the parameter tells the analysis that the returned view is tied to the argument’s lifetime. Current Clang accepts the attribute but emits no diagnostic. The programmer must still respect the contract, which future compilers will enforce. -This pattern appears wherever a function hands back a handle into memory it was given. Accessors that return a reference or a view to a member, parsers that return a view into their input, and span factories all need the attribute so that the analysis can connect the output to the input. +The attribute can also be placed on the function itself, applying the rule to every return path, and on constructors: a constructor that stores a pointer or reference member must mark the source parameter `lifetimebound` so the member’s lifetime cannot outlive the argument. This links the member to the argument and enables static checks. +The pattern appears wherever a function hands back a handle into memory it was given. Accessors that return a reference or a view to a member, parsers that return a view into their input, and span factories all need the attribute to connect output to input. ## Views and spans are lifetime-transparent `std::string_view` and `std::span` are non-owning handles. They do not own storage. They merely borrow it. A view has no destructor that frees anything, because there is nothing for it to free. Its correctness depends entirely on the owner staying alive. -The rule is simple. Keep the owner alive for at least as long as any view or span that refers to it. A view is a loan. The lender must outlive the loan. The most common mistake is to create the owner and the view in the same expression, so the temporary owner dies before the view is consumed. A named owner that lives in a scope enclosing the view removes the hazard. +Keep the owner alive for at least as long as any view or span that refers to it. A view is a loan. The lender must outlive the loan. Creating the owner and view in the same expression makes the temporary owner die before the view is used. Bind the view to a named owner that lives in an enclosing scope to avoid the hazard. `std::span` generalizes the idea from characters to any `T`. A `std::span` is a non-owning window over a row of `int` values. The same lifetime rule applies. The `std::vector` or array that backs the span must outlive every use of the span. Because a span is so light, functions must accept spans instead of a raw pointer plus a length. That change makes the non-owning intent explicit and removes a whole class of length mismatch bugs. -## `[[clang::lifetimebound]]` - -The attribute can be placed on a function parameter or on the function itself. When placed on a parameter, the analysis treats any return value that refers to that parameter as bound to the argument's lifetime. When placed on the function, the same rule applies to every return path. The attribute is useful for accessors such as `first_word` that return a view into their argument. Even though the current clang does not emit a warning for a call with a temporary, the attribute documents the contract and enables future static checks. -The attribute also helps constructors. A constructor that stores a pointer or reference member must mark the source parameter `lifetimebound` when the member must not outlive the source. Without the mark, the analysis has no link between the member and the argument, and it cannot catch a caller that passes a temporary. ## `gsl::not_null` diff --git a/book/src/ch08-argument-passing.md b/book/src/ch08-argument-passing.md index 1b76455..f82bbae 100644 --- a/book/src/ch08-argument-passing.md +++ b/book/src/ch08-argument-passing.md @@ -50,11 +50,11 @@ struct S { When `inspect` is called on a `const S` object, the compiler guarantees that `value` is not altered. If a `const` reference were bound to a temporary, the temporary becomes immutable for the duration of the reference. The compiler enforces this rule even though the temporary will soon be destroyed. -The same `const` placement rules apply to pointers. A pointer to const (`const T*`) lets the pointer move but not the pointee. A const pointer (`T* const`) cannot be reseated but the pointee stays mutable. A const pointer to const (`const T* const`) fixes both. Chapter 2 covers these variants in full. +The same const placement rules apply to pointers. See Chapter 2. If we modify `ch08_pass_by.cpp` to call a non‑`const` member on a `const` reference, the compilation fails, illustrating that `const` truly prevents mutation. -The discipline pays off through const overloading. A type can offer both `T& at(std::size_t)` and `const T& at(std::size_t) const`. A non-const object binds to the first and a const object, including a temporary, binds to the second. Because a const member function can be called on a temporary, expressions such as `std::string{}.size()` are well formed. This is why standard-library accessors are almost always const. They read state without changing it, so they work on both persistent and fleeting objects. +The discipline pays off via const overloading: a type can provide both `T& at(std::size_t)` and `const T& at(std::size_t) const`. The former binds to non‑const objects, and the latter binds to const objects and temporaries. This enables read‑only access such as `std::string{}.size()`. ## Pass by value then move @@ -87,18 +87,18 @@ The program prints the address of the caller’s object, the parameter, and the ## Return by value Returning a value by copy used to be expensive because the caller received a separate object that required a copy. Modern C++ solves this with copy elision, including the guaranteed elision of prvalues, and with move semantics for named temporaries. - The compiler constructs the return object directly in the caller’s storage (NRVO) or treats the temporary as an rvalue that can be moved. Consequently, returning a `std::vector` or a `std::string` does **not** copy the underlying buffer on the happy path. +The compiler constructs the return object directly in the caller’s storage (NRVO) or treats the temporary as an rvalue that can be moved. Consequently, returning a `std::vector` or a `std::string` does **not** copy the underlying buffer on the happy path. -The rule is simple: *return‑by‑value is cheap because copy elision and move semantics eliminate unnecessary copies*. - -Copy elision is reliable only when the function returns a single local or a prvalue. The standard mandates elision in these cases, even if the copy or move constructor has side effects. When the function holds several locals and returns one chosen at runtime, the compiler cannot always prove which object the caller receives, so named return value optimization is not guaranteed and the move constructor runs. The practical rule is to return one clearly identified local, or to build the result in the return statement, so the cheap path holds. This ties back to Chapter 05, where we saw that moving a container transfers ownership of its internal buffer without copying elements. +The rule is simple: return‑by‑value is cheap because copy elision and move semantics eliminate unnecessary copies. Elision is guaranteed when the function returns a single local or a prvalue. Otherwise the compiler falls back to a move. Return a clearly identified local or construct the result directly in the return statement (see Chapter 05 for container move semantics). ## References are not pointers -A reference is an alias bound to an object at initialization. It cannot be reseated, and it cannot be null. The language guarantees that a reference always denotes a valid object for its lifetime. However, the reference does not extend the lifetime of the object it refers to. If the referent is destroyed while the reference remains alive, any use of the reference yields undefined behavior. This is the same hazard described in Chapter 07. +A reference is an alias bound to an object at initialization. It cannot be reseated and cannot be null. The language guarantees that a reference denotes a valid object for its lifetime. + +The reference does not extend the lifetime of the object it refers to. If the referent is destroyed while the reference remains alive, using the reference yields undefined behavior. This hazard is described in Chapter 07. -A pointer, by contrast, can be null and can be reassigned. Pointers can be checked at runtime, but they provide no guarantee that the pointed‑to object remains alive. Rust’s borrow checker enforces lifetime safety at compile time, rejecting patterns that can produce a dangling reference in C++. C++ places the responsibility on the programmer. The lifetime analysis tool described in Chapter 07 helps detect violations. The rule remains: *a reference must never outlive its referent*. +A pointer can be null or reassigned and offers no guarantee that the pointee remains alive. Unlike Rust’s borrow checker, C++ relies on the programmer. Chapter 07’s lifetime analysis tool can detect violations. Rule: a reference must never outlive its referent. There is one exception that surprises even experienced programmers. Binding a const reference to a temporary extends the temporary's lifetime to match the reference's scope. The temporary is not destroyed at the end of the full expression. It lives until the reference goes out of scope. This extension does not apply when the reference is a member of an object or when it is returned from a function. It is a property of the local binding only. It is why `const auto& x = compute()` avoids a copy of a temporary safely. diff --git a/book/src/ch09-errors-contracts.md b/book/src/ch09-errors-contracts.md index fac2ffb..dfd99e7 100644 --- a/book/src/ch09-errors-contracts.md +++ b/book/src/ch09-errors-contracts.md @@ -2,17 +2,15 @@ ## The C legacy -C reports failure through integer return codes and the global variable `errno`. The caller can ignore the value. Every call site must remember to test the result. A forgotten test produces a silent bug. C provides no automatic cleanup when a failure occurs. +C reports failure via integer return codes and the global variable `errno`. Callers must remember to test each result, otherwise silent bugs arise. -C++ adds mechanisms that make failure harder to ignore. The language encourages the programmer to express error conditions as part of the type system. The compiler can warn when a return value is discarded. Marking a result-carrying type `[[nodiscard]]` turns that warning on for every call site, so a result that encodes failure cannot be dropped silently. - -The shift from C to C++ error handling is a shift in who bears the burden. In C, every call site bears it: the caller must remember to check, and a forgotten check is a silent bug. In C++, the type system bears it: a function that can fail returns a type that encodes the failure, and the caller cannot ignore the type. The compiler does not prevent the caller from discarding the result, but `[[nodiscard]]` makes the discard a warning, and the explicit `if (result)` check makes the intent visible to a reader. +C++ adds mechanisms that make ignoring failures harder: the type system can encode error states, and the `[[nodiscard]]` attribute warns when a result is discarded, ensuring the intent to handle errors is visible. ## Exceptions and unwinding -C++ supports `throw`, `try`, and `catch`. A `throw` ends the current function and searches for a matching `catch`. During the search the runtime unwinds the stack. Each local object is destroyed in reverse order of construction. RAII objects release their resources automatically. No resource leak remains because the destructor runs on every unwind path. +C++ supports `throw`, `try`, and `catch`. A `throw` aborts the current function, triggers stack unwinding, and destroys each local object in reverse order, allowing RAII objects to release resources automatically and preventing leaks. -Exceptions fit cases where the failure cannot be repaired at the call site. Examples include out‑of‑memory, corrupted files, or invalid user input that prevents further progress. +Exceptions apply when the failure cannot be repaired at the call site, such as out‑of‑memory, corrupted files, or invalid user input. The dividing line between exceptions and `expected` is the frequency and the locality of the failure. A parse error is routine: the caller expects it, handles it, and moves on, so it travels as an `expected` value. An out-of-memory error is rare and crosses abstraction boundaries: no individual caller can fix it, so it travels as an exception and unwinds to the nearest handler. Mixing the two is a smell: if every caller wraps a function in a `try`/`catch`, the failure is routine and belongs in an `expected`. @@ -99,9 +97,7 @@ Early C programs used integer return codes and `errno` as the sole mechanism for The table is a decision tree, not a menu. Read the failure mode first, then pick the tool. A function that fails because the input is bad returns `expected`. A function that fails because the system ran out of memory throws. A function that cannot fail marks itself `noexcept`. A function that documents a contract uses an attribute. The four tools cover four failure modes, and the modes do not overlap. -Use the tool that matches the semantic intent. Do not mix strategies without a clear reason. - -A library that returns `expected` for some functions and throws from others forces the caller to hold two mental models at once. Pick one default for the module and deviate only where the failure profile demands it. A parser returns `expected` because every call can fail. A memory allocator throws because failure is rare and no caller can fix it. The module boundary is where the choice is visible, so it is where the choice must be made deliberately. +Use the tool that matches the semantic intent and avoid mixing strategies without a clear reason. A library that mixes `expected` and exceptions forces the caller to hold two mental models. Choose a single default per module and deviate only when the failure profile demands it. For example, a parser returns `expected` because every call can fail, while a memory allocator throws because out‑of‑memory is rare and unrecoverable. ## Impossible cases diff --git a/book/src/ch10-text.md b/book/src/ch10-text.md index c2129ef..cf333f2 100644 --- a/book/src/ch10-text.md +++ b/book/src/ch10-text.md @@ -52,17 +52,7 @@ Running the program prints a line that contains `Name: Alice`. The `EXPECT` test The example prints a small table. The test harness looks for the token `A` in the output. -These facilities replace `std::cout` and `printf` as the default output mechanism. The three approaches differ in type safety, format checking, buffering, and error handling. -**Type safety and compile‑time format checking.** `std::cout` chains values with the `operator<<` stream insertion operator. Each value is converted to text in turn, and the compiler checks each insertion independently. `printf` takes a format string plus a variable number of arguments, but the compiler cannot verify that the format string matches the argument list, so a mismatch is undefined behavior at run time. `std::print` and `std::println` take a format string plus arguments, and the compiler parses the literal at compile time and rejects any mismatch with a diagnostic. The format string is a compile‑time constant, so a wrong argument type or a missing argument fails to compile. - -**Chaining versus varargs versus format arguments.** `std::cout` builds output by chaining `operator<<` calls, one per value. `printf` passes the values through a varargs list, which loses type information. `std::print` and `std::println` pass the arguments as a typed list that the compiler checks against the placeholders in the format string. - -**Buffering and performance.** `std::cout` is a buffered stream and flushes in a way that can be slow in a tight loop. `printf` is also buffered. `std::print` and `std::println` write directly to `stdout` and avoid the stream machinery, and the implementation parses the format string at compile time when it is a constant. The result is comparable to hand‑written `printf` for simple cases, without the safety cost. - -**Error handling.** `std::cout` sets a stream state flag on failure, which the caller can inspect. `printf` returns a negative count on error and leaves the caller to check the return value. `std::print` and `std::println` throw `std::format_error` when the format string is ill‑formed, and they report write failures through the stream state. - -**When each is appropriate.** Use `std::cout` when you need the full stream machinery, such as flushing control or custom `operator<<` types. Use `printf` only when you must interoperate with an existing C codebase. Use `std::print` and `std::println` for new code, because they give compile‑time checked formatting with direct output. | Facility | Header | Type safety | Format checking | Returns | Use when | |---|---|---|---|---|---| @@ -92,21 +82,21 @@ The specifier syntax mirrors Python’s `format`. The colon introduces a format Performance of `std::format` is comparable to hand‑written `printf` for simple cases. The library avoids temporary allocations for short strings by using a small‑buffer optimisation. -Locale support is optional. By default formatting uses the "C" locale, which provides ASCII digits and period as decimal separator. Users can supply a `std::locale` object to a `std::format` overload to customise digit grouping and decimal marks. + Custom types can be formatted by specialising `std::formatter`. The specialization returns a `format_to` function that writes the representation into the provided output iterator. ```cpp -struct Point { int x int y } +struct Point { int x; int y; }; template<> struct std::formatter { - constexpr auto parse(auto& ctx) { return ctx.begin() } + constexpr auto parse(auto& ctx) { return ctx.begin(); } auto format(Point const& p, auto& ctx) const { - return std::format_to(ctx.out(), "({},{})", p.x, p.y) + return std::format_to(ctx.out(), "({},{})", p.x, p.y); } -} +}; -std::println("{}", Point{3,4}) // prints "(3,4)" +std::println("{}", Point{3,4}); // prints "(3,4)" ``` The same custom formatter works for both `std::println` and `std::format` because they share the formatter protocol. @@ -137,24 +127,23 @@ Custom types can also honour these flags by inspecting the `format_context` and ```cpp struct Money { - int cents -} + int cents; +}; template<> struct std::formatter { - char presentation = 'f' // f = dollars.cents, e = euros + char presentation = 'f'; // f = dollars.cents, e = euros constexpr auto parse(auto& ctx) { - auto it = ctx.begin() - if (it != ctx.end() && (*it == 'e' || *it == 'f')) presentation = *it++ - return it + auto it = ctx.begin(); + if (it != ctx.end() && (*it == 'e' || *it == 'f')) presentation = *it++; + return it; } - auto format(Money const& m, auto& ctx) const { + auto format(Myney const& m, auto& ctx) const { if (presentation == 'e') - return std::format_to(ctx.out(), "€{:.2f}", m.cents / 100.0) + return std::format_to(ctx.out(), "€{:.2f}", m.cents / 100.0); else - return std::format_to(ctx.out(), "${:.2f}", m.cents / 100.0) + return std::format_to(ctx.out(), "${:.2f}", m.cents / 100.0); } -} - +}; std::println("{}", Money{1234}) // prints "$12.34" ``` @@ -218,7 +207,7 @@ std::println(std::format(german, "{:L}", 1234567.89)) // prints "1.234.567,89" Custom formatters, shown earlier, work uniformly with locale‑aware overloads because the formatter receives the locale via its `format` method. -The chapter therefore covers both the safety‑first philosophy and the performance characteristics that make `std::format` a practical replacement for legacy formatting functions. + ## Error handling in formatting diff --git a/book/src/ch11-containers.md b/book/src/ch11-containers.md index 85a6ab1..0e34b54 100644 --- a/book/src/ch11-containers.md +++ b/book/src/ch11-containers.md @@ -13,7 +13,7 @@ Because reallocation can be expensive, two techniques reduce its impact: Both techniques tie back to earlier chapters. Chapter 07 described the lifetime‑safety analysis that flags dangling pointers after a reallocation. Reserving ahead of time eliminates that risk for the most common case. Chapter 08 explained why passing arguments by value or reference matters. `emplace_back` constructs in place, satisfying the principle of “construct where you use”. -The growth strategy is geometric. Each reallocation typically doubles the capacity, so the amortised cost of `push_back` is constant time even without `reserve`. `reserve` only matters when you know the final size and want to avoid the intermediate copies, or when you must prevent invalidation of existing handles. +The vector grows geometrically, typically doubling capacity. This yields amortised constant‑time `push_back`. Use `reserve` when the final size is known to avoid intermediate reallocations and iterator invalidation. Erasing an element from the middle of a `vector` shifts every later element down by one, so removal is `O(n)` in the number of elements after the erased position. `erase` and the C++20 `erase`/`erase_if` free functions return the new logical end. When you only need to drop the last few elements, `pop_back` or `resize` is cheaper. @@ -119,11 +119,7 @@ When you start a new piece of code, follow this decision guide: 5. **Use `std::list` or `std::forward_list`** only when you must insert or erase frequently in the middle of a large sequence and the cache penalty is tolerable. 6. **Use `std::span`** to write generic algorithms that operate on any contiguous view without taking ownership. -The cache‑locality advantage of `std::vector` explains why it becomes the default in most modern code. - -The pattern repeats across the library. Prefer contiguous, owning storage by default, reach for a view when you only observe, and pick a node‑based container only when its specific invalidation or ordering property is required. Remember that `std::string` is itself a sequence container of `char`, so the same contiguous‑storage reasoning applies to text, and the `std::string_view` parameter rule from Chapter 10 carries over. - -`std::pmr` supplies polymorphic allocators and memory resources, so you can switch a container's allocation strategy by changing its allocator template argument rather than its type. A monotonic or pool resource can avoid the heap entirely for short‑lived containers. This is the advanced lever for latency‑sensitive code and is covered in the systems section of this book. +Prefer contiguous, owning containers such as `std::vector` (default) or `std::array` when size is fixed. Use `std::span` for non‑owning views and `std::pmr` allocators to customise allocation without changing the container type. The same cache‑locality reasoning applies to `std::string`, while `std::string_view` follows the view pattern described earlier. ## Try this diff --git a/book/src/ch12-algorithms.md b/book/src/ch12-algorithms.md index b6f05be..3334719 100644 --- a/book/src/ch12-algorithms.md +++ b/book/src/ch12-algorithms.md @@ -2,12 +2,10 @@ A hand-written loop hides intent. Chapter 04 showed that a loop can miss an off-by-one error and can expose lifetime bugs when a pointer is taken to an element that later moves. A named algorithm states exactly what happens: *find the value*, *count the matches*, *transform each element*. The reader understands the code without inspecting the body. -Algorithms also connect to containers. Chapter 11 introduced the iterator pair model. An algorithm works on any pair `[begin, end)`. The same call works for `std::vector`, `std::array`, `std::list`, or any range that provides iterators. This uniformity removes repetitive boilerplate and reduces bugs. +Algorithms operate on iterator pairs, making them generic over containers and element types. The same call works for `std::vector`, `std::array`, `std::list`, or any range providing iterators, and for `int`, `std::string`, or a user type with `operator<`. This uniformity removes boilerplate, reduces bugs, and lets one algorithm serve every container. Every standard algorithm carries a documented complexity guarantee. `std::find` and `std::count_if` run in linear time because they can inspect each element. `std::sort` guarantees `O(n log n)` comparisons in the worst case. Knowing these bounds helps you choose the right tool on a performance-critical path. If you need only the smallest element, `std::min_element` is cheaper than a full sort because it stops after a single linear scan. -The algorithms are generic over element type and iterator category. The same `std::sort` call works on `int`, `std::string`, or a user type that provides `operator<`. This genericity is the core of the Standard Library design. The algorithm expresses the operation, the iterator expresses the traversal, and the container expresses the storage. Keeping these three concerns separate is what lets one algorithm serve every container. - ## The iterator pair model All standard containers expose `begin()` and `end()`. They define a half-open range `[begin, end)`. The range includes the element pointed to by `begin` and excludes the element pointed to by `end`. This convention lets algorithms stop exactly at the last element without an extra check. @@ -102,7 +100,7 @@ The example builds a vector of five integers and computes their sum. ## Algorithmic design patterns -Many tasks map to a composition of standard algorithms. The filter-map-reduce pattern appears often in data processing: filter a range with `std::views::filter`, transform the survivors with `std::views::transform`, and aggregate with `std::accumulate` or `std::ranges::fold_left`. The erase-remove idiom removes unwanted elements without a temporary container. Recognising these shapes helps you write concise code that reuses the library's guarantees. +Common tasks compose standard algorithms. Use filter‑map‑reduce: `std::views::filter`, `std::views::transform`, then `std::accumulate` or `std::ranges::fold_left`. Apply the erase‑remove idiom to discard elements without extra storage. Recognising these patterns yields concise code that leverages library guarantees. ## Parallel policies @@ -115,12 +113,12 @@ std::sort(std::execution::par, v.begin(), v.end()); ## Common pitfalls -* **Binary search on an unsorted range** yields undefined behaviour. Sort first. -* **Invalidated iterators**: `std::sort` can invalidate every iterator because it rearranges elements arbitrarily. Reacquire iterators after such calls. -* **Iterator category mismatch**: `std::sort` requires random-access iterators. Passing a `std::list` iterator fails to compile. -* **Destination too small**: `std::copy` and `std::transform` write exactly as many elements as the source supplies. Use `std::back_inserter` or size the destination first. -* **Sorting a node-based container**: `std::list` does not provide random-access iterators, so `std::sort` will not compile on it. Use the member `list::sort` instead. -* **Projection side effects**: a projection must be a pure function. One that modifies state can break the algorithm's invariants when it reorders elements. +* **Binary search on an unsorted range** is undefined behaviour. Sort first. +* **Invalidated iterators**. `std::sort` can invalidate all iterators. Reacquire them after sorting. +* **Iterator category mismatch**. `std::sort` requires random‑access iterators. `std::list` iterators fail to compile. +* **Destination too small**. `std::copy` and `std::transform` write exactly as many elements as the source supplies. Use `std::back_inserter` or size the destination first. +* **Sorting a node‑based container**. `std::list` lacks random‑access iterators. Use its member `list::sort` instead. +* **Projection side effects**. A projection must be pure. State‑modifying projections break algorithm invariants. ## Try this diff --git a/book/src/ch13-ranges.md b/book/src/ch13-ranges.md index 915414b..9049ce8 100644 --- a/book/src/ch13-ranges.md +++ b/book/src/ch13-ranges.md @@ -3,9 +3,7 @@ ## What a view is A view is a lightweight, non-owning handle over a sequence. It borrows the elements and never allocates memory. The view does not manage the lifetime of its elements. The same principle underlies `std::string_view` and `std::span` that were introduced earlier. Both types expose a pointer and a length, and they refuse to copy the data. A range is any object that provides `begin()` and `end()` that return iterators. A view is a range that does not own its elements. In other words, every view is a range, but not every range is a view. -Performance and safety are affected by the difference. Because a view never allocates, construction is essentially a constant-time pointer-plus-size operation. The compiler can inline the construction, and the generated code adds no heap traffic. At the same time, the view inherits the lifetime constraints of the underlying storage. If a `std::vector` is destroyed while a `std::span` still refers to its data, the span becomes dangling, and the lifetime analysis of the compiler (chapter 07) warns about the misuse. This mirrors the earlier discussion in chapter 08 about passing a container by view instead of by value to avoid copies. - -A view also conveys intent to the reader. When a function parameter is declared as `std::span`, the caller knows that the function will read the elements without taking ownership. The same signal appears with `std::string_view` for read-only text. This intent-driven design reduces accidental copies and clarifies ownership boundaries across API surfaces. +Because a view never allocates, construction is a constant‑time pointer‑plus‑size operation. The compiler can inline it and no heap traffic occurs. The view inherits the lifetime constraints of its underlying storage, so a dangling `std::span` results if the source is destroyed, which the compiler warns about. Declaring a parameter as `std::span` also signals that the function reads elements without taking ownership, mirroring `std::string_view` for read‑only text. This combination of zero‑allocation construction and intent‑driven design reduces accidental copies and clarifies ownership boundaries across API surfaces. A view has a precise definition in the standard. A type is a view if it is a range, is cheap to copy or move, and does not own the elements it presents. `std::ranges` even provides `std::ranges::owning_view` to wrap an owning container into the view model when an API demands a view but you must keep ownership locally. The inverse, `std::ranges::ref_view`, wraps a reference to a range you already own. Knowing which wrapper applies prevents both dangling and accidental copies. @@ -30,9 +28,7 @@ The following example builds a pipeline that generates the integers from 1 to 10 It prints the three even squares `4 16 36`. The `EXPECT` string in the build file verifies that these three numbers appear in the output. -Beyond the basic adaptors, the standard library provides combinators such as `views::split` for tokenising a string on a delimiter and `views::reverse` for reverse iteration without copying. These utilities enable expressive one-liner pipelines that replace verbose loops and temporary containers. - -The adaptor set also covers structure, not just filtering. `views::chunk` groups elements into fixed-size subranges, `views::slide` produces overlapping windows, and `views::elements` projects the Nth member of each tuple-like element, so `views::elements<0>` extracts the keys from a range of pairs. For associative containers, `views::keys` and `views::values` expose just the key or mapped type without copying. These turn nested loops into single pipelines. +Beyond the basic adaptors, the standard library provides combinators such as `views::split` for tokenising a string on a delimiter and `views::reverse` for reverse iteration without copying. The adaptor set also covers structure: `views::chunk` groups elements into fixed-size subranges, `views::slide` produces overlapping windows, and `views::elements` extracts the Nth member of each tuple‑like element (e.g., `views::elements<0>` extracts keys from a range of pairs). For associative containers, `views::keys` and `views::values` expose just the key or mapped type without copying. These utilities replace verbose loops and temporary containers with concise pipelines. ## Laziness Views are evaluated on demand. The pipeline does not create a temporary container after each adaptor. The `take(3)` adaptor stops the source after three elements have been produced. This property enables short-circuiting of expensive sources. @@ -47,9 +43,7 @@ The next example constructs an endless `iota` range, squares each value, and the The output contains the first five squares `0 1 4 9 16`. In contrast, a pre-ranges algorithm that first copies the `iota` range into a `std::vector` allocates billions of elements before the program must stop. The lazy view avoids that allocation entirely, saving both time and memory. -Laziness also improves cache behaviour. Because each element is produced, transformed, and consumed in a single pass, the processor can keep the working data in registers. The two-step pattern of generating a container and then iterating it forces a second memory pass that increases latency, especially for large data sets. - -The same laziness applies to conditional stops. `views::take_while` keeps elements while a predicate holds and then halts, and `views::drop_while` discards until the predicate first fails. Because the adaptors are stateless, chaining `drop_while` with `take_while` on a sorted range extracts a contiguous band in one pass without building it. +Laziness also improves cache behaviour. Each element is produced, transformed, and consumed in a single pass, keeping data in registers and avoiding a second memory pass. The same laziness applies to conditional stops: `views::take_while` keeps elements while a predicate holds and then halts, and `views::drop_while` discards until the predicate first fails. Because the adaptors are stateless, chaining `drop_while` with `take_while` on a sorted range extracts a contiguous band in one pass without building it. ## Materialising a view: `std::ranges::to` Sometimes code needs ownership of the elements produced by a view. The helper `std::ranges::to` materialises a view into a concrete container. The syntax is `view | std::ranges::to()`. The container type must be default-constructible and support `push_back` or equivalent insertion. @@ -79,9 +73,7 @@ The example below defines a simple `Point` struct with members `x` and `y`. A `s The output `1 2 3` demonstrates that the projection eliminated the need for a custom comparator. -Beyond sorting, any algorithm that accepts a projection can use the member-pointer form. `std::ranges::find(v, value, &T::key)` searches a range of objects for a specific key without a bespoke lambda. This pattern appears throughout the standard library and reduces boilerplate code across the code base. - -The same member-pointer projection works on associative ranges through `views::values` and `views::keys`. Given a `std::map`, `m | std::views::values` yields the integers directly, so a reduction over the mapped values needs no lambda. Projections and `views::values` together remove the last boilerplate from aggregate processing. +Beyond sorting, any algorithm that accepts a projection can use the member‑pointer form. `std::ranges::find(v, value, &T::key)` searches a range of objects for a specific key without a bespoke lambda. The same member‑pointer projection works on associative ranges through `views::values` and `views::keys`. For example, `m | std::views::values` yields the integers from a `std::map` directly. These patterns appear throughout the standard library and eliminate boilerplate code. ## `std::ranges` algorithms versus pre-ranges algorithms The range algorithms in `std::ranges` operate directly on any range, including views. They return iterators that refer to the original elements, so no copying occurs. The pipe syntax composes algorithms with adaptors. This makes the intent clear and the code compact. @@ -113,9 +105,7 @@ The example below defines a simple adaptor `twice` that multiplies each element The output `2 4 6 8` confirms that the custom adaptor behaved like a built-in view. Readers can extend this pattern to more sophisticated pipelines, such as a `prime_filter` that composes `filter` with a deterministic primality test. Because the closure is a compile-time object, the optimizer can erase the intermediate layers entirely. -One rule governs every view: the view is only as alive as its source. A `std::vector` that dies before a `std::span` or a `views::filter` result leaves that view dangling, exactly as chapter 07 warned. Never return a view into a local container, and never bind a view to a temporary you do not keep alive. The compiler's lifetime analysis catches the obvious cases. The rest is discipline. - -Because the view pipeline composes at compile time, the generated code often collapses multiple layers into a single loop. This results in execution speed comparable to hand‑written loops while preserving readability. The compiler can also inline the adaptor calls, eliminating function‑call overhead. The approach scales to large data sets because each element is processed exactly once. +A view lives only as long as its source. Returning a view to a local container or temporary yields a dangling view, which the compiler warns about (see chapter 07). Because the pipeline composes at compile time, the compiler can collapse multiple adaptor layers into a single loop, achieving speed comparable to hand‑written loops while preserving readability. ## Try this Build a pipeline that takes `std::views::iota(1, n)`, keeps only the multiples of 3, squares each kept value, takes the first 5 results, and prints them. The program must compile with the book's standard settings and must run without allocating an intermediate container. diff --git a/book/src/ch14-callables-type-erasure.md b/book/src/ch14-callables-type-erasure.md index fb1ca4d..5076692 100644 --- a/book/src/ch14-callables-type-erasure.md +++ b/book/src/ch14-callables-type-erasure.md @@ -1,9 +1,11 @@ # Callables and type erasure ## What is a callable -A callable denotes any entity that can be used after the function‑call operator `()`. The C++ standard groups several kinds under this umbrella. A function pointer points to a free function and can be invoked directly. A function object, also called a functor, is a class type that overloads `operator()`. Lambdas are unnamed function objects that the compiler synthesises from a capture list and a body. `std::function` is a type‑erased wrapper that can hold any of the previous forms provided the call signature matches. Finally a pointer to member function or a pointer to data member also qualifies as a callable when used together with an object instance. +A callable denotes any entity that can be used after the function‑call operator `()`. The C++ standard groups several kinds under this umbrella: a function pointer to a free function, a function object (functor) that overloads `operator()`, a lambda (an unnamed function object generated from a capture list and body), `std::function` (a type‑erased wrapper that can hold any of the previous forms when the call signature matches), and a pointer to a member function or data member used with an object instance. + +Generic algorithms (chapter 12) and range algorithms (chapter 13) accept a callable parameter expressed as a template type satisfying the *invocable* requirement, which allows the same algorithm to work with a raw pointer, a capturing lambda, or a `std::function`. This decouples the algorithm from the concrete call target and is central to generic programming. + -The generic algorithms introduced in chapter 12 and the range algorithms of chapter 13 accept a callable as a parameter. The parameter is written as a template type that satisfies the *invocable* requirement. This design allows the same algorithm to work with a raw function pointer, a lambda that captures state, or a `std::function` supplied by the caller. The concept is central to generic programming because it decouples the algorithm from the concrete call target. ## Lambdas Lambdas provide a concise way to create a function object. The syntax starts with a capture list in square brackets, followed by an optional parameter list, an optional mutable specifier, an optional exception specification, and a body. The capture list determines which surrounding variables become members of the closure type. @@ -24,16 +26,12 @@ The following example demonstrates a lambda used as a predicate for `std::ranges The test harness checks that the program prints the line `count = 3`. The lambda illustrates how a small, capture‑less callable integrates directly with a range algorithm. ### Capturing state and performance -When a lambda captures a variable by value, the closure stores its own copy. This makes the lambda safe to copy and move, because the stored data follows the usual value‑semantic rules. Capturing by reference creates a reference member. The closure can be copied, but the copies still refer to the original variable. This nuance matters when the lambda is stored in a container that outlives the referenced variable. - -A capture‑less lambda can be converted to a plain function pointer. The conversion eliminates the indirect call overhead. When a lambda captures state, the compiler usually inlines the call if the closure type is known at compile time. Inlining removes the call indirection entirely. The cost of capturing by copy is the extra copy operation performed at the point of lambda creation. +When a lambda captures by value, the closure holds its own copy, which makes the lambda safe to copy and move. Capturing by reference stores a reference member, so copies still refer to the original variable, which matters when the lambda outlives that variable. A capture‑less lambda can convert to a function pointer, removing indirection. If a lambda captures state and the closure type is known, the compiler can inline the call and eliminate the indirect call. The only overhead is copying captured values at construction. ## `std::function` and type erasure `std::function` is a class template that abstracts away the concrete callable type. Internally it stores a pointer to a type‑erased function object and a pointer to a virtual call dispatcher. When a callable is assigned to a `std::function`, the wrapper can allocate dynamic memory if the object does not fit into the Small‑Object Optimization buffer (typically 2 to 3 pointers). The allocation adds heap traffic and a level of indirection at each invocation. -The trade‑off is flexibility. A `std::function` can hold any callable that matches the required signature, regardless of its type. This enables runtime polymorphism: a program can fill a container with heterogeneous callables, select one based on user input, and invoke it without knowing its exact type. - -In contrast, a template parameter that accepts a callable preserves the concrete type. The compiler can inline the call, eliminate the virtual dispatcher, and avoid any heap allocation. This zero‑overhead approach is the default recommendation for performance‑critical code. +The trade‑off is flexibility: `std::function` can hold any matching callable, which enables runtime polymorphism such as heterogeneous containers and user‑selected callables. In contrast, a template parameter preserves the concrete type, which allows the compiler to inline calls, eliminate the virtual dispatcher, and avoid heap allocation. This zero‑overhead approach is recommended for performance‑critical code. The example below builds a `std::vector>`. It stores three different callables: a free function that multiplies its argument by three, a capturing lambda that adds five, and a `std::bind` expression that multiplies by five. The program iterates the vector, calls each element with the argument `5`, and prints the result. @@ -43,18 +41,17 @@ The example below builds a `std::vector>`. It stores thr The test expects the three lines `15`, `10`, and `25` in that order. The example shows how heterogeneous callables can coexist in a single container. -### Allocation behaviour +### Allocation and when to prefer `std::function` If the stored callable fits into the small‑object buffer, `std::function` does not allocate. In the example the lambda and the bound function are small enough, so no heap allocation occurs. If a callable captures a large `std::vector` by value, the wrapper will allocate to hold the captured data. -### When to prefer `std::function` * When the callable type is not known at compile time, such as when a plugin supplies a callback. * When the callable must be stored in a homogeneous container that outlives the point of creation. * When the API is a boundary that other languages or runtimes will call. ## When to erase, when to template -The choice between type erasure and a template hinges on the point at which the callable is selected. +Choosing type erasure or a template depends on when the callable is known. -* If the algorithm is a library component that the user instantiates, the library must expose a template parameter (or a generic `auto` parameter) for the callable. This yields a bespoke instantiation for each caller, allowing the compiler to inline the call and generate optimal code. +* If the algorithm is a library component that the user instantiates, the library must expose a template parameter (or a generic `auto` parameter) for the callable. This yields a bespoke instantiation for each caller, which allows the compiler to inline the call and generate optimal code. * If the algorithm is part of a runtime system, such as a GUI framework that stores user‑supplied callbacks, a networking library that registers event handlers, or a scripting engine that invokes user code, type erasure via `std::function` or `std::move_only_function` is appropriate. A practical decision rule: @@ -70,7 +67,7 @@ The rule helps avoid accidental performance loss in tight loops while still prov * Pointers to member functions, where the first argument is the object (or a reference, pointer, or smart pointer) on which to invoke the member. * Pointers to data members, where the result is the member value. -`std::invoke_r(f, args…)` adds an explicit return‑type conversion, forcing the result to be converted to `R` before returning. +`std::invoke_r(f, args…)` adds an explicit return‑type conversion, which forces the result to be converted to `R` before returning. The following program defines a free function, a struct with a member function and a data member, and then calls each through `std::invoke`. The output demonstrates how the same helper covers all three cases. diff --git a/book/src/ch15-numerics.md b/book/src/ch15-numerics.md index 9bb27d3..59bc0e1 100644 --- a/book/src/ch15-numerics.md +++ b/book/src/ch15-numerics.md @@ -2,7 +2,7 @@ ## Type‑safe math constants: `std::numbers` -The header `` supplies a family of mathematical constants as `inline constexpr` objects. Each constant is defined for the three fundamental floating‑point types. The primary name, for example `std::numbers::pi`, denotes a `double` value. The template alias `std::numbers::pi_v` yields the same constant expressed in the type `T`. This design removes the need for separate literals such as `M_PI` or user‑defined `constexpr` values. +The header `` supplies `inline constexpr` constants for `float`, `double`, and `long double`. The primary name, e.g. `std::numbers::pi`, denotes a `double`. The alias `std::numbers::pi_v` yields the constant in type `T`. This removes the need for separate literals such as `M_PI` or user‑defined `constexpr` values. The older macro `M_PI` originates from the C header ``. It expands to a literal of type `double` and is not guaranteed to exist on all platforms. Because it is a macro, the constant cannot participate in overload resolution based on the target type. The `std::numbers` objects avoid those problems and can be used in all constexpr expressions. @@ -26,7 +26,9 @@ The header `` implements the classic mathematical functions. All function * `std::floor(x)` returns the greatest integer not larger than *x*. * `std::ceil(x)` returns the smallest integer not smaller than *x*. -The set also contains safer primitives for interpolation and rounding. `std::midpoint(a, b)` returns the value halfway between `a` and `b` without the overflow that `a + (b - a) / 2` can suffer, and `std::lerp(a, b, t)` computes `a + t * (b - a)` with correctly handled endpoints. `std::fma(x, y, z)` computes `(x * y) + z` as a single fused operation, which avoids an intermediate rounding step and improves both speed and accuracy on hardware that supports it. The classification functions `std::isfinite`, `std::isnan`, and `std::isinf` test a value's category without throwing, which is the reliable way to detect a failed computation. +The set also contains safer primitives for interpolation and rounding. `std::midpoint(a, b)` returns the value halfway between `a` and `b` without the overflow that `a + (b - a) / 2` can suffer. `std::lerp(a, b, t)` computes `a + t * (b - a)` with proper endpoint handling. `std::fma(x, y, z)` computes `(x * y) + z` as a single fused operation, avoiding an intermediate rounding step and improving speed and accuracy on supporting hardware. + +The classification functions `std::isfinite`, `std::isnan`, and `std::isinf` test a value's category without throwing, which provides a reliable way to detect a failed computation. In addition, `std::copysign(x, y)` copies the sign of `y` onto the magnitude of `x`, which is the correct way to negate a zero or to preserve a sign across an operation. `std::nextafter(x, y)` steps to the next representable value toward `y`, exposing the discrete nature of floating-point for tolerance and unit-test work. @@ -54,7 +56,7 @@ The program below constructs a complex number `z = 3 + 4i`, prints its real and ## Compile‑time rational numbers: `std::ratio` -`std::ratio` encodes a rational number as two compile‑time integer template arguments. The type can be used in non‑type template parameters, enabling compile‑time arithmetic without additional run‑time cost. +`std::ratio` encodes a rational number as two compile‑time integer template arguments. The type can be used in non‑type template parameters, which enables compile‑time arithmetic without additional run‑time cost. The library provides metafunctions such as `std::ratio_add` and `std::ratio_multiply`. These compute a new `std::ratio` that represents the sum or product of the two operand ratios. The result is available as a nested `type` member. @@ -62,7 +64,7 @@ The library provides metafunctions such as `std::ratio_add` and `std::rati For example, `std::chrono::duration>` is exactly milliseconds: a duration that stores a count of `long` ticks where each tick is one thousandth of a second. Swapping the ratio to `std::ratio<1, 1>` yields seconds and to `std::ratio<60, 1>` yields minutes, all from the same template. -The example defines a base ratio representing one thousandth (`std::ratio<1, 1000>`), prints its numerator and denominator, and then uses `std::ratio_add` to add two such ratios, producing `2/1000`. The test harness searches for the exact string `1/1000`. +The example defines a base ratio representing one thousandth (`std::ratio<1, 1000>`), prints its numerator and denominator, and then uses `std::ratio_add` to add two such ratios, which produces `2/1000`. The test harness searches for the exact string `1/1000`. ```cpp {{#include ../../examples/ch15/ratio_example.cpp}} diff --git a/book/src/ch16-templates-functions.md b/book/src/ch16-templates-functions.md index 85ece8b..7142d2e 100644 --- a/book/src/ch16-templates-functions.md +++ b/book/src/ch16-templates-functions.md @@ -5,11 +5,7 @@ Templates let a single definition work for many types. The compiler creates a co Read templates as term-rewriting rules with unification, in the sense a Prolog programmer knows. A template is a pattern. The compiler unifies the call's argument types against the pattern's parameters, binding `T` to a concrete type, then rewrites the call into a specialized instance. Overload resolution and specialization are rule precedence on top of that unification. This mental model explains why error messages name a failed unification rather than a line in your logic. -Templates also let library authors express conceptual interfaces without fixing a concrete type. A function that works for any iterator type can be written once and used with raw pointers, `std::vector` iterators, or user‑defined iterator classes. The compiler enforces required operations at each instantiation. Missing operations produce errors attached to the instantiated template. This early feedback helps developers locate problems quickly. - -The cost model of templates benefits performance‑critical code. Each specialization is generated at compile time. The optimizer can inline the body, eliminate dead code, and propagate constants. Benchmarks in chapter 29 show that a template version of a numeric algorithm matches or exceeds hand‑written code while staying concise. - -Templates also enable *type‑safe* generic programming. Because substitution occurs before any runtime code exists, the compiler rejects ill‑formed uses at compile time. This contrasts with runtime polymorphism where a missing method is only detected during execution. The combination of early checking and zero‑overhead abstraction makes templates the preferred tool for reusable library components. +Templates let library authors write code that works for any iterator type, raw pointers, `std::vector` iterators, or user‑defined iterators. The compiler enforces required operations at each instantiation and reports errors attached to the generated code, providing early feedback. Because each specialization is generated at compile time, the optimizer can inline, eliminate dead code, and propagate constants, yielding zero‑overhead performance comparable to hand‑written code. This combination of type‑safe generic programming and compile‑time abstraction makes templates the preferred tool for reusable library components. ## Function templates A function template starts with the keyword `template`. The following example defines a generic `max` function. @@ -53,28 +49,6 @@ The example program creates a `Box` and a `Box` and prints eac A member variable inside a class template is instantiated once per distinct template argument. For instance, a class template can define `int count` to provide a separate count for each `T`. This pattern supports compile‑time registries with zero runtime allocation. -## `typename` vs `class` and dependent names -Inside a template parameter list, `typename` and `class` are interchangeable. The following declarations are equivalent. - -```cpp -template -// same as -template -``` -The keyword `typename` also has a distinct meaning inside a template body. When a name depends on a template parameter, the compiler assumes it refers to a value. To indicate that the name denotes a type, write `typename T::iterator`. This disambiguation is required because the same identifier can denote a static data member. - -When a dependent name can refer to either a type or a value, the `typename` keyword disambiguates the type, and the `template` keyword disambiguates a dependent member template. The following example illustrates both cases. - -```cpp -template -void foo(T t) { - // dependent type requires 'typename' - typename T::type *ptr = nullptr; - // dependent member template requires 'template' - t.template bar(); -} -``` -Understanding this dual role prevents common compilation errors and is essential when writing generic libraries. ## Abbreviated function templates (C++20) C++20 introduced a shorthand for simple function templates. The declaration `auto f(auto x)` expands to `template auto f(T x)`. The following program demonstrates both forms. @@ -142,9 +116,7 @@ A matching `template class std::vector` line in another translation unit tr Explicit instantiation also interacts with the one‑definition rule. The definition must appear in exactly one translation unit. Otherwise the linker reports multiple definition errors. This rule reinforces the importance of a clear build structure. -The examples here instantiate at runtime, but a template can also compute at compile time. Chapters 18 and 21 build on this foundation to perform type-level computation and static dispatch, turning the same rewriting machinery into a compile-time evaluator. - -The parameters of a template need not be types. A template can also take values, such as an integer size or a pointer, as a non-type parameter. `std::array` stores its size in the type, which is why its length is known at compile time. Chapter 19 explores non-type parameters in depth. +The examples instantiate at runtime, but templates also compute at compile time. Chapters 18 and 21 use this to perform type‑level computation and static dispatch. Templates can take non‑type parameters, for example an integer size in `std::array`, so the length is known at compile time. Chapter 19 covers this in detail. ## Try this Write a template `clamp` that limits a value to a closed interval. diff --git a/book/src/ch17-concepts.md b/book/src/ch17-concepts.md index 2350790..5fd4d55 100644 --- a/book/src/ch17-concepts.md +++ b/book/src/ch17-concepts.md @@ -3,11 +3,7 @@ ## Why concepts Templates allow algorithms to work with any type that satisfies a set of requirements. Before C++20 those requirements were expressed with SFINAE tricks such as `std::enable_if` and trait metafunctions. The constraints were hidden inside long type expressions, and a failure produced a cascade of template‑instantiation diagnostics that were difficult to read. Concepts replace that style with explicit, named predicates. A concept is part of the function signature. The compiler checks it before it attempts to instantiate the template. If the requirement is not met, the diagnostic cites the concept name and the offending type. The error becomes clear and the intent obvious. -Concepts also serve as documentation that lives in the code. The name of a concept conveys intent: `std::integral` tells the reader that the algorithm works on arithmetic types, `std::range` tells that any pair of iterators is acceptable. Because the constraint appears next to the function declaration, readers need not hunt for a separate `enable_if` block to discover the precondition. - -A concept is itself a compile-time Boolean value. `std::integral` evaluates to `true` and can appear wherever a `bool` constant is expected, such as `if constexpr (std::integral)` or `static_assert(std::integral)`. This means a concept is both a constraint on a template parameter and an ordinary compile-time predicate, so the same name drives overload resolution and branches inside a function body. - -A concept reports failure earlier than a `static_assert`. A `static_assert` inside a template body fires only after the template has been instantiated, so the error points deep inside a library you do not control. A concept fails at the call site, before the body is even considered, so the message names your type and the requirement it broke. This earlier failure is the main readability win. +Concepts also act as in‑code documentation: the concept name (e.g., `std::integral` or `std::range`) states the precondition next to the declaration, removing the need for separate `enable_if` blocks. A concept is a compile‑time `bool` value, usable in `if constexpr` or `static_assert`, and it drives overload resolution and in‑body branching. Because the compiler checks the constraint before template instantiation, failures appear at the call site with the concept name and offending type, providing earlier, clearer diagnostics than a `static_assert` inside the function body. ## The `requires` clause and `requires` expression A *requires clause* follows a template declaration and names one or more concepts that must be satisfied. @@ -62,9 +58,7 @@ When two concepts overlap, the more specific one wins. This rule is called *subs ```cpp {{#include ../../examples/ch17/subsumption.cpp}} ``` -`std::signed_integral` implies `std::integral`. Calls with a signed type select the second overload, while calls with an unsigned type select the first. The deterministic ordering eliminates the ambiguous‑overload errors that were common with SFINAE‑based tricks. - -Subsumption removes ambiguity from overload sets. When two constrained overloads both match, the compiler prefers the one whose constraint subsumes the other, so the most specific overload wins deterministically. This is why a generic `std::integral` overload and a narrower `std::signed_integral` overload coexist without an ambiguity error: the second is strictly more constrained and is chosen for signed arguments. +Subsumption removes ambiguity from overload sets. When two constrained overloads both match, the compiler prefers the overload whose constraint subsumes the other. For example, `std::signed_integral` implies `std::integral`. Calls with a signed type select the `std::signed_integral` overload, while unsigned calls select the generic overload. This deterministic ordering eliminates the ambiguous‑overload errors common with SFINAE tricks. ## Terse syntax C++26 allows constraints to appear directly on a parameter type or on a plain `auto` placeholder. @@ -90,9 +84,7 @@ Standard algorithms already carry concept requirements. `std::ranges::sort` requ ```cpp {{#include ../../examples/ch17/algorithm_concept.cpp}} ``` -The program searches a `std::vector` for a value. Because the container satisfies `std::range`, the call compiles. If we attempted the same call on a raw array, the compiler emits a diagnostic that the `range` concept is not satisfied. - -Because the concepts are part of the algorithm’s signature, users can instantly see the preconditions without consulting external documentation or static assertions. +The program searches a `std::vector` for a value. Because the container satisfies `std::range`, the call compiles. A raw array fails the `range` concept, and the compiler reports that the concept is not satisfied. Since the concepts appear in the algorithm’s signature, users see the preconditions directly without consulting external documentation or static assertions. ## Library concept vocabulary The header `` provides the core building blocks used throughout the standard library: diff --git a/book/src/ch18-compile-time.md b/book/src/ch18-compile-time.md index 2e111b7..3aec9d1 100644 --- a/book/src/ch18-compile-time.md +++ b/book/src/ch18-compile-time.md @@ -2,7 +2,9 @@ ## constexpr deeply -`constexpr` marks a function, variable, or constructor as eligible for constant‑evaluation. The standard defines a *constant expression* as an expression that can be evaluated during translation when all operands are themselves constant. When the compiler sees a call to a `constexpr` function in such a context, it substitutes the computed value directly into the surrounding program. If the call appears where a constant expression is not required, for example inside `main` or as a non‑constant argument to another function, the same definition is compiled as ordinary run‑time code. This *single definition* model removes the duplication that earlier C++ versions forced on the programmer (a `constexpr` overload together with a non‑`constexpr` overload). The function body must obey a restricted set of rules so that the compiler can reason about it without side effects: it cannot contain `asm`, cannot perform I/O, and can only modify objects that have static storage duration or are created within the evaluation itself. +`constexpr` marks a function, variable, or constructor as eligible for constant evaluation. The standard defines a constant expression as an expression that can be evaluated during translation when all operands are themselves constant. When the compiler encounters a call to a `constexpr` function in such a context, it substitutes the computed value directly into the program. + +Since C++14 a `constexpr` function can contain loops, local variables, and `if` statements, so its body resembles ordinary code. The constant‑evaluation engine still enforces a sandbox: no I/O, no `asm`, and no use of the address of a non‑constant object. If a call cannot be evaluated, the compiler either falls back to a runtime call where legal or issues a hard error in a context that requires a constant expression. Since C++14 a `constexpr` function can contain loops, local variables, and `if` statements, so the body looks like ordinary code. The compiler still enforces a constant-evaluation sandbox: no I/O, no `asm`, and no use of the address of a non-constant object. When a call cannot be evaluated, the compiler either falls back to a runtime call where that is legal, or reports a hard error in a context that demands a constant expression. @@ -16,7 +18,7 @@ The static‑assert in the file forces the compiler to evaluate `factorial(5)` a ## consteval (immediate functions) -`consteval` is a stronger guarantee introduced in C++20. An immediate function **must** be evaluated at translation time. Any attempt to call it where a constant expression is not required is ill‑formed. The compiler therefore rejects the program outright, producing a diagnostic that points to the offending call site. Immediate functions are ideal for compile‑time utilities that must never appear in the generated binary, such as compile‑time string hashing, type‑level identifiers, or compile‑time parsing of literals. +`consteval` is a stronger guarantee introduced in C++20. An immediate function **must** be evaluated at translation time. Any attempt to call it where a constant expression is not required is ill‑formed. The compiler therefore rejects the program outright, which produces a diagnostic that points to the offending call site. Immediate functions are ideal for compile‑time utilities that must never appear in the generated binary, such as compile‑time string hashing, type‑level identifiers, or compile‑time parsing of literals. The following example computes a simple additive hash of a string literal. Because the function is declared `consteval`, the call `hash("abc")` is forced into the constant‑evaluation engine. The resulting value is verified with a `static_assert`. The `main` function prints a marker that confirms the program compiled successfully. @@ -82,15 +84,13 @@ A practical consequence is that compile-time data tables can be built with ordin ## Compile‑time as pure evaluation (Lisp framing) -A `constexpr` function behaves like a pure term‑rewriter: given a set of input values it produces an output value without observable side effects. The compiler treats the function as a mathematical function, memoizes the result for identical constant arguments, and folds the value into the surrounding expression. This view mirrors the term‑rewriting semantics introduced for templates in Chapter 16, where a template is a rule that rewrites a type pattern into another type. In both cases the compile‑time engine performs substitution, checks constraints, and produces a result that becomes part of the final program. +A `constexpr` function behaves like a pure term‑rewriter: given inputs it produces an output without observable side effects. The compiler treats it as a mathematical function, can memoize results for identical constant arguments, and folds the value into the program. This mirrors the term‑rewriting semantics of templates introduced in Chapter 16, where the compile‑time engine performs substitution, checks constraints, and yields a result. -When a `constexpr` function is invoked repeatedly with the same constant arguments, the compiler can emit the value once and reuse it, reducing code size and eliminating redundant work. This is analogous to the way a Lisp interpreter evaluates a pure function at compile time and stores the result for later calls. Memoization is not guaranteed, but a compiler is free to reuse a folded value, so the same constant does not pay its evaluation cost twice. This is the same substitution engine the template system uses, seen from the value side instead of the type side. +When invoked repeatedly with the same constant arguments, the compiler can emit the value once and reuse it, reducing code size and eliminating redundant work. This is analogous to a Lisp interpreter evaluating a pure function at compile time and storing the result for later calls. ## The convention flip: static‑assert test suites -Historically, examples in this book printed values and asked the reader to run the program and verify the output manually. With `constexpr` the result is known at compile time, so the natural testing strategy flips to *compile‑time* verification. A `static_assert` encodes the expectation directly in the source file. The compiler checks it during translation. If the assertion fails, the build stops with a clear diagnostic, and the continuous‑integration pipeline reports the failure. - -The pattern used throughout the chapter combines a `static_assert` for correctness and a short `std::cout` marker so that the book’s CTest harness can still confirm that the binary linked and executed. This dual verification satisfies both the compile‑time contract and the run‑time sanity check required by the automated book‑generation workflow. +Historically, examples printed values and asked the reader to run the program and verify the output manually. With `constexpr`, the result is known at compile time, so the testing strategy flips to compile‑time verification: a `static_assert` encodes the expectation directly in the source file. The compiler checks it during translation. A failure stops the build with a clear diagnostic reported by the CI pipeline. The chapter still includes a short `std::cout` marker so the CTest harness can confirm that the binary linked and executed. ## C++26 constexpr frontiers @@ -113,7 +113,7 @@ constexpr int safe_div(int a, int b) { static_assert(safe_div(4,2) == 2); ``` -> **Not yet deployable.** User‑generated `static_assert` messages can incorporate constexpr data, allowing a compile‑time error to report a computed value. Example syntax: +> **Not yet deployable.** User‑generated `static_assert` messages can incorporate constexpr data, which allows a compile‑time error to report a computed value. Example syntax: ```cpp template diff --git a/book/src/ch19-nttp.md b/book/src/ch19-nttp.md index 23eaf6b..4eabb1e 100644 --- a/book/src/ch19-nttp.md +++ b/book/src/ch19-nttp.md @@ -4,7 +4,7 @@ Non‑type template parameters turn compile‑time values into part of the type system. By embedding a constant in a template argument the compiler generates a distinct specialization for each value. This enables zero‑overhead dispatch: the generated code contains only the paths required for the specific constant, and any branches that depend on the value disappear after constant folding. Standard library containers such as `std::array` and `std::span` rely on this technique to expose size information without runtime storage. Compile‑time bounds checking, static‑asserted preconditions, and compile‑time hash tables also become possible when the size or key is an NTTP. -The `auto` NTTP form, `template`, deduces the type of the argument automatically. It accepts an integral, a pointer, a reference, or a structural class object. Generic utilities frequently use this pattern to forward a value to another template without naming its type, reducing boilerplate and improving readability. The compiler records the value in the mangled name of the instantiation, so each distinct argument yields a unique symbol. This can increase binary size if many values are used, but the trade‑off is often worthwhile for the performance gain of eliminating runtime conditionals. +The `auto` NTTP form, `template`, deduces the argument type. It accepts an integral, pointer, reference, or structural class object. This pattern forwards a value without naming its type, reducing boilerplate. The compiler encodes the value in the mangled name, giving each distinct argument a unique symbol. Using many distinct values can increase binary size, but the gain is eliminating runtime conditionals. It accepts an integral, a pointer, a reference, or a structural class object. Generic utilities frequently use this pattern to forward a value to another template without naming its type, reducing boilerplate and improving readability. The compiler records the value in the mangled name of the instantiation, so each distinct argument yields a unique symbol. This can increase binary size if many values are used, but the trade‑off is often worthwhile for the performance gain of eliminating runtime conditionals. When an NTTP participates in overload resolution, the compiler prefers a more specialized non‑type argument. This mirrors concept overload resolution and enables tag‑dispatch based on constexpr values. For example, a function template can provide a fast path for a power‑of‑two size by matching `template requires (N & (N-1)) == 0`. @@ -38,7 +38,7 @@ C++20 extends NTTPs to accept *structural types*. A structural type is a class o `Config` satisfies the structural rules: it contains two data members, both of which are fundamental types, and the struct has no constructors. The template `UseConfig` receives a concrete `Config{3,4.5}` as a compile‑time constant. Inside the specialization the values appear as `constexpr` static data members. The `static_assert`s verify the values during translation, while the program prints a short marker confirming successful runtime execution. -Structural NTTPs enable patterns such as compile‑time configuration tables, policy objects that influence algorithm selection, and domain‑specific languages that pass complete objects as arguments without any runtime overhead. +Structural NTTPs enable compile‑time configuration tables, policy objects, and domain‑specific languages without runtime overhead. The structural‑type rule requires all members to be public and non‑mutable, so the compiler can compare two NTTPs for equality during overload resolution and template deduplication. Two aggregates are equal when their members are equal, which keeps hashing and name mangling well defined. The structural-type rule exists so the compiler can compare two NTTPs for equality during overload resolution and template deduplication. Because every member is public and the class has no user constructor, two aggregate values are equal exactly when their members are equal, which keeps hashing and name mangling well defined. This is what lets a `Config{3, 4.5}` name one unique specialization. diff --git a/book/src/ch20-specialization.md b/book/src/ch20-specialization.md index f780ca5..589b93f 100644 --- a/book/src/ch20-specialization.md +++ b/book/src/ch20-specialization.md @@ -1,7 +1,6 @@ # Specialization, overloading, and customization points This chapter presents the decision rule for choosing concepts versus specialization, then shows the related mechanisms. - ## Class template specialization A class template defines a family of types parameterised by one or more template arguments. A **full specialization** provides a concrete definition for a *specific* set of arguments. A **partial specialization** fixes *some* arguments while leaving others as parameters. @@ -25,7 +24,6 @@ The primary template is selected when the argument list does not match any parti ```cpp {{#include ../../examples/ch20/template_specialization.cpp}} ``` - ### How the compiler decides * **Matching**: the argument list is compared against each specialization’s pattern. @@ -37,9 +35,6 @@ Because specializations are an *out‑of‑band* mechanism, they do not particip A full specialization is written with an empty template parameter list and a concrete argument: `template<> struct Printer { ... };`. It is the only form that can introduce a definition with a completely different set of members, because it no longer depends on any template parameter. Partial specializations must still match the primary template's parameter list in shape, so they can vary the pattern but not the member set arbitrarily. In practice most code needs at most one partial specialization and a handful of full ones. One caution: the primary template must remain well formed even if only specializations are used, because the compiler instantiates the primary in some contexts before consulting specializations. Keep a sensible default body in the primary and treat specializations as refinements. - - - ## The decision rule When you need different behaviour, ask two questions: @@ -49,28 +44,7 @@ When you need different behaviour, ask two questions: In short, *choice is semantic → use concepts*. *Representation changes → specialise*. -Applying the rule consistently avoids scattered overloads and specializations that obscure the library's intent. When a concept describes a semantic property, readers can see the requirement directly in the function signature, while specialization remains reserved for cases where the type's layout or representation differs fundamentally. This discipline simplifies refactoring and improves compile‑time error messages, because the compiler reports a concept failure rather than a mismatched specialization. - -```cpp -template -concept Pointer = std::is_pointer_v; - -template -void foo(T) { std::cout << "concept" << '\n'; } - -int main() { int *p = nullptr; foo(p); } -``` - -```cpp -{{#include ../../examples/ch20/decision_rule.cpp}} -``` - -The `Pointer` concept expresses a semantic property. The function is still a single overload, and the compiler dispatches based purely on the concept satisfaction. - -The rule prevents a common mistake: reaching for specialization when a concept fits. Specializing a template for a property such as "has a size" fragments the implementation across many files and hides the choice from the call site. A concept keeps one generic body and expresses the property inline. Reach for specialization only when the layout of the object itself differs, so that a generic body cannot describe the representation without contorting it. - - - +Applying the decision rule consistently avoids scattered overloads and specializations. When a concept describes a semantic property, the requirement appears directly in the function signature and the implementation stays in a single generic body. Use specialization only when the type’s layout or representation differs fundamentally, such as a raw array versus a `std::span`. This discipline simplifies refactoring and provides clearer compile‑time diagnostics. ## Variable templates Variable templates let you define **compile‑time constants** that depend on a template parameter. They are the value‑side analogue of function templates. @@ -88,14 +62,7 @@ The standard library uses this pattern extensively, e.g. the `_v` suffix for tra {{#include ../../examples/ch20/variable_template.cpp}} ``` -Variable templates are instantiated only when ODR‑used, so they impose no runtime cost. They are also a natural place for *configuration constants* that vary with a type, such as `std::numeric_limits::max()`. - -Variable templates compose. `template constexpr T half_pi = pi / 2;` reuses one variable template inside another, and the compiler folds the arithmetic at translation time. Because they are `constexpr`, a variable template can feed a `static_assert`, an `if constexpr` condition, or a non-type template argument, bridging the value and type worlds that chapter 19 explored. - -Variable templates also serve as the value-side customization point. A library can declare `template struct traits;` and specialize `traits::value`, but a variable template such as `template inline constexpr bool is_trivially_copyable_v` is the concise spelling the standard prefers for such traits. - - - +Variable templates are instantiated only when ODR‑used, so they impose no runtime cost. They also serve as a natural place for configuration constants such as `std::numeric_limits::max()`. They compose, e.g., `template constexpr T half_pi = pi / 2;`, allowing the compiler to fold arithmetic at translation time. Because they are `constexpr`, they can feed `static_assert`, `if constexpr` conditions, or non‑type template arguments, bridging value and type worlds. Variable templates also act as value‑side customization points. A library can declare `template struct traits;` and specialize `traits::value`, while a variable template like `template inline constexpr bool is_trivially_copyable_v` provides the standard‑preferred concise spelling. ## `if constexpr` as in‑body dispatch Sometimes a single function needs two completely different implementations, but you do not want to write separate overloads or specializations. `if constexpr` lets you branch at compile time based on a constant expression. @@ -119,12 +86,7 @@ The compiler discards the unreachable branch, so no ill‑formed code can appear {{#include ../../examples/ch20/if_constexpr_dispatch.cpp}} ``` -Notice that the same `show` function works for both pointer and non‑pointer arguments without any overload set. - -`if constexpr` is the modern replacement for tag dispatch and for the SFINAE tricks that chapter 21 teaches you to read. The discarded branch is not merely skipped at runtime. It is not instantiated at all, so a name used only in that branch need not be valid for every `T`. This is what lets one function body serve types with incompatible operations, as long as each branch is well formed for the types that reach it. - - - +`if constexpr` replaces tag dispatch. The compiler discards the unreachable branch, so the same `show` function works for both pointer and non‑pointer arguments without any overload set. ## Specialising `std::formatter` `std::format` formats arbitrary types using the **formatter** customization point. To make a user‑defined type printable, you specialise `std::formatter` for your type `T`. @@ -152,15 +114,11 @@ The specialization lives in the same namespace as `std` (the only exception allo ```cpp {{#include ../../examples/ch20/formatter_specialization.cpp}} ``` - ### Why specialise instead of overloading? Overloading `operator<<` works only for stream‑based APIs. `std::format` follows a *type‑centric* design: the formatter is a **customisation point object (CPO)** that the library calls. By providing a specialization, you integrate with the whole formatting ecosystem without pulling in iostreams. The `parse` member decodes the part of the format string that follows the colon, and `format` writes the value. Inheriting from an existing formatter such as `std::formatter` is a shortcut when you want default spec handling. For a type with no natural spec, `parse` just returns the begin iterator. The specialization must be visible in the namespace of the type or of `std`, which is why the standard permits specialising `std::formatter` for user types even though it otherwise forbids adding to `std`. - - - ## Specialising `std::hash` `std::unordered_map` and `std::unordered_set` hash their keys through `std::hash`. A user type has no default, so to use one as a key you specialise `std::hash`: @@ -181,9 +139,6 @@ struct std::hash { The specialization must be a complete type with an `operator()` returning `std::size_t`. Combine the hashes of the members with a shift and XOR so the result depends on both fields. Equality must stay consistent with the hash: two keys that compare equal must hash equal, or the container misbehaves. The same pattern powers the customization that `std::unordered_map` relies on for every key type. Because the hash and the equality operator must agree, define them together. If you change the equality later, update the hash in the same change or lookups return wrong results silently. The standard library also lets you supply a custom hasher or comparator as template arguments to `unordered_map`, so a bespoke type can avoid touching `std::hash` entirely when its needs are unusual. - - - ## ADL and hidden friends Argument-dependent lookup (ADL) finds functions in the namespaces of their arguments. When `a == b` is written, the compiler looks for `operator==` not only in the enclosing scope but also in the namespaces of `a` and `b`. A *hidden friend* is an `operator==` defined inside the class body. It is reachable only through ADL, so it never pollutes the global namespace and cannot be called without the right argument types. @@ -195,14 +150,10 @@ struct Point { }; ``` -Because the friend is defined inline, it is a hidden friend and ADL is the only mechanism that finds it. This keeps the operator close to the type, prevents accidental calls on unrelated types, and is the idiomatic way to add a comparison without opening the global namespace. The same reasoning applies to any operator that is only used with its own operand types. - -Hidden friends also give the best diagnostics. Because the operator is tied to the type, a call with a mismatched operand fails with a message that names `Point`, rather than surfacing an unrelated global overload. - +Because the friend is defined inline, it is a hidden friend and ADL is the only mechanism that finds it. This keeps the operator close to the type, prevents accidental calls on unrelated types, and gives the best diagnostics: a call with a mismatched operand names `Point` rather than surfacing an unrelated overload. The same reasoning applies to any operator used only with its own operand types. To summarize, the library can combine concepts, specializations, variable templates, and customization point objects to provide a clear, layered customization strategy that keeps generic code simple and concrete overrides focused. - ## Try this Create a small `struct Color { uint8_t r,g,b }` and write a `std::formatter` that formats the colour as a hex string `#RRGGBB`. Verify the output with `std::format`. diff --git a/book/src/ch21-legacy-tmp.md b/book/src/ch21-legacy-tmp.md index c494787..43a4170 100644 --- a/book/src/ch21-legacy-tmp.md +++ b/book/src/ch21-legacy-tmp.md @@ -22,6 +22,9 @@ SFINAE and `enable_if` are not a separate language. They are template deduction * **Default template parameter**: the template parameter list carries a hidden `enable_if` that activates the overload. * **Parameter type**: the function parameter itself is wrapped in `enable_if`, often for `std::string` arguments. +Immediate-context rule: a substitution failure in the part of a declaration that participates in overload resolution removes the candidate without a diagnostic. This is the basis of SFINAE (Substitution Failure Is Not An Error). It lets `enable_if` expressions in return types, default template parameters, or parameter types silently disable overloads. + + Below is a single file that demonstrates all three signatures. The program prints a message that identifies the selected overload. The modern rewrite, shown as comments, uses concepts to achieve the same overload resolution. ```cpp @@ -54,10 +57,9 @@ string overload Each overload is selected exactly as the concept-based version is, demonstrating a mechanical one-to-one mapping. -The immediate-context rule also explains why certain tricks, such as enabling a member function only when a type provides a nested `value_type`, work reliably. By placing the failing expression in a defaulted template argument or a return-type `enable_if`, the compiler can discard the candidate without emitting a hard error. The overload set then falls back to a generic implementation. -SFINAE stands for *Substitution Failure Is Not An Error*. When the compiler substitutes a template argument into a declaration, any failure that occurs **in the immediate context** (the part of the declaration that participates in overload resolution) does **not** produce a diagnostic. Instead, the candidate is removed from the overload set. The remaining viable overloads are then considered. + ```cpp template ().size())>> diff --git a/book/src/ch22-sql-capstone.md b/book/src/ch22-sql-capstone.md index 4e7777f..e2a5c91 100644 --- a/book/src/ch22-sql-capstone.md +++ b/book/src/ch22-sql-capstone.md @@ -6,7 +6,7 @@ A domain‑specific language (DSL) that runs at compile time gives the same safe The lineage is clear. Deane and Turner demonstrated a constexpr JSON parser that turned a string literal into a `constexpr` data structure. CTParser and the CTRE library later showed how regular‑expression‑based parsers can live in `consteval` functions. More recently, mkitzan’s constexpr‑sql project combined those ideas to build a tiny SQL‑like EDSL. The capstone brings those ingredients together in a single, self‑contained example. -A constexpr EDSL is not a new parser library bolted onto the build. It is the same term-rewriting machinery as the template system, applied to a string literal. The query is data to the compiler, not a command executed at runtime, which is exactly how a Lisp macro consumes source text and emits code before the program runs. The difference is that the result here is a typed value, not text, so the compiler checks every branch as it evaluates. +A constexpr EDSL is not a new parser library bolted onto the build. It uses the same term‑rewriting machinery as the template system applied to a string literal. The query becomes compiler data, not a runtime command, so the compiler checks every branch as it evaluates. A compile‑time failure is the cheapest failure because it occurs before the binary exists and names the exact erroneous query text. Consequently, the compiler acts as the test runner and the query literal serves as the fixture, eliminating the need for a separate runtime test harness. A compile-time failure is the cheapest kind of failure, because it happens before the binary exists and names the exact query text that is wrong. A runtime test needs a runner, an assertion library, and a suite to maintain. Here the compiler is the runner and the query literal is the fixture. @@ -19,9 +19,7 @@ The capstone re‑uses exactly the mechanisms introduced earlier: * **`constexpr` containers**: `std::array` and `std::vector` with transient allocation hold tables and query results entirely at compile time. * **`if constexpr`** selects between the supported comparison operators without generating unreachable code. -No other library is required. The whole program lives in a single source file. - -None of these pieces is exotic on its own. `fixed_string` is a plain array wrapped in a structural type, `consteval` is a one-word qualifier, and `constexpr` containers were enabled by C++20's transient allocation. What the capstone shows is that their combination is enough to build a real, self-checking domain language with no runtime component at all. +No other library is required. The whole program lives in a single source file, and each piece (`fixed_string`, `consteval`, and `constexpr` containers) is a plain, non‑exotic component whose combination yields a real, self‑checking domain language without any runtime component. ## The schema as aggregates diff --git a/book/src/ch23-headers.md b/book/src/ch23-headers.md index 2f42687..0def422 100644 --- a/book/src/ch23-headers.md +++ b/book/src/ch23-headers.md @@ -16,7 +16,7 @@ Because the process is entirely textual, a header can be included many times, fr Modules, introduced in the next chapter, provide a different mechanism that bypasses textual pasting. For now the dominant reality is the include-paste model described above. -Separate compilation has a practical payoff. When one source file changes, only that unit recompiles, and the linker recombines it with the unchanged object files. This is why a large project rebuilds quickly after a single edit instead of recompiling everything. The cost is that the compiler sees each unit in isolation, so it must learn about names from headers rather than from other units. +Separate compilation has a practical payoff. When one source file changes, only that unit recompiles, and the linker recombines it with the unchanged object files. This is why a large project rebuilds quickly after a single edit instead of recompiling everything. The compiler sees each unit in isolation, so it learns names from headers. ## Declarations versus definitions @@ -77,9 +77,7 @@ Headers are the root cause of many ODR violations. A header that contains a full The ODR also applies to types: two different definitions of a class with the same name break the rule, even if the definitions are textually identical. The linker cannot merge class definitions. The program must contain a single authoritative definition. -A type-level ODR violation is the subtlest. Two translation units that disagree about a class layout can compile separately and then produce memory corruption when combined, because the compiler in one unit wrote bytes assuming one layout and the other reads them assuming another. The linker cannot detect this, so the only defence is discipline: define each class exactly once, in one header, and include that header everywhere. - -A common beginner mistake is to put a non-inline function definition in a header for convenience. Every unit that includes the header then carries its own copy of the function, and the linker aborts with a multiple-definition error. The fix is to move the definition to one source file and keep only the declaration in the header. +A type-level ODR violation is the subtlest: differing class layouts across units cause memory corruption, so define each class once in a header and include it everywhere. Likewise, placing a non‑inline function definition in a header creates multiple definitions. Move the definition to a source file and keep only the declaration in the header. ## `inline` as the ODR valve @@ -92,9 +90,7 @@ When a header defines an `inline` function or variable, each translation unit th Because the definitions are required to be identical, the compiler can safely replace a call with the function body (inline expansion) or keep a single out‑of‑line copy if necessary. -The book uses `inline constexpr double pi = 3.141592653589793;` in the header to demonstrate this rule. - -The identical-definition requirement matters. If two translation units define an `inline` function with different bodies, the behaviour is undefined even though each unit compiles. This is why a header with an `inline` definition must never be edited differently for different units, and why macros that change the meaning of a function body inside a header are a hazard. +The book uses `inline constexpr double pi = 3.141592653589793;`. The definition must be identical in every translation unit, otherwise behavior is undefined. ## Linkage and anonymous namespaces @@ -157,7 +153,7 @@ Compiling the three files separately is instructive. `vec2.cpp` compiles `vec2.h ## Header hygiene -A **self‑contained header** compiles on its own. That means it includes every header it needs, and nothing else. +A self‑contained header compiles on its own. That means it includes every header it needs, and nothing else. Never rely on a transitive include from another header. If `vec2.hpp` needs `` it must include it directly, even if another header already includes ``. This prevents surprising compile errors when the header is used elsewhere. @@ -171,7 +167,7 @@ The example library follows these rules: * the source file `vec2.cpp` includes the same header to ensure the declarations match the definitions. * `main.cpp` includes only `vec2.hpp` and the standard `` for output. -Two more rules keep headers reliable. First, every header must be tested in isolation: compile it alone with a trivial source file that includes only it, so a missing include surfaces immediately. Second, prefer forward declarations over full includes for types used only by pointer or reference, because the compiler needs the complete type only when a member is accessed. +Every header must be compiled in isolation to detect missing includes, and forward declarations replace full includes when only pointers or references are used. ## Try this diff --git a/book/src/ch24-modules.md b/book/src/ch24-modules.md index add6cdb..d569ce5 100644 --- a/book/src/ch24-modules.md +++ b/book/src/ch24-modules.md @@ -2,7 +2,9 @@ ## What modules fix over #include -The traditional include mechanism copies the text of a header file into each translation unit that names it. The copy includes every macro definition that appears before the include directive. A macro defined in one header can change the meaning of code that includes a later header. The order in which headers appear therefore influences the program. Errors that originate inside a header are reported at the line that performed the include. This behaviour makes it hard to locate the source of the problem. Because each translation unit receives its own copy of the header, the compiler cannot share work between units. Large projects that include many standard headers experience long compile times and fragile builds. +The traditional include mechanism copies the text of a header file into each translation unit that names it. It also copies every macro definition that appears before the include directive. Because each translation unit receives its own copy of the header, the compiler cannot share work between units, which leads to long compile times for large projects. + +A macro defined in one header can change the meaning of code that includes a later header. Thus, the order of header inclusion influences the program. Errors that originate inside a header are reported at the line that performed the include, making it hard to locate the source of the problem. Modules replace this textual inclusion with a compiled interface. A module’s interface is built once. This build produces a binary module interface (BMI) file. The BMI contains only the declarations that the author chooses to export. Macros are not part of the BMI, so a macro defined in one translation unit cannot affect a module that imports it. Errors that arise inside a module are reported inside the module file itself, giving a clear location. The build system can reuse the BMI for every importer, which reduces compile time dramatically for projects that import the same module many times. In short, modules give isolation, order independence, and faster incremental builds. @@ -58,6 +60,8 @@ The corresponding implementation unit uses the same `module mymod:part;` header. The example files in `examples/ch24/` follow this pattern. They are registered as a gap because the current compiler does not accept the `module` keyword. +Naming modules follows a simple convention: use lower‑case identifiers that reflect the library’s purpose, avoid mixed‑case or digits, and keep the name stable across versions. Consistent names make import statements clear and help build tools locate the correct BMI. + ```cpp {{#include ../../examples/ch24/mymod.cppm}} ``` @@ -92,7 +96,6 @@ The book marks these examples as gaps, but the same CMake patterns work unchange -Modules also improve compile-time diagnostics. Because the compiler sees a single compiled interface, errors are reported in the module source rather than in each importer. This behaviour makes it easier to locate the problem. In addition, the BMI contains template definitions needed for instantiation, so downstream code can instantiate templates without re-parsing the original definitions. This separation of interface and implementation encourages a cleaner architectural boundary. ## Mixing modules and headers @@ -117,7 +120,7 @@ const unsigned char logo[] = { ``` -Naming modules follows a simple convention: use lower‑case identifiers that reflect the library’s purpose, avoid mixed‑case or digits, and keep the name stable across versions. Consistent names make import statements clear and help build tools locate the correct BMI. + The majority of production code still relies on the header-include model. Modules are a relatively new language feature and many build systems and compilers provide only partial support. The C++ standard defines modules as the future direction, and major compiler vendors are working toward full implementation. Readers will encounter both models in the wild. Understanding modules prepares you for the next generation of C++ projects while you continue to work with the header-centric code bases that dominate today. diff --git a/book/src/ch25-concurrency-threads.md b/book/src/ch25-concurrency-threads.md index ffb6200..b188944 100644 --- a/book/src/ch25-concurrency-threads.md +++ b/book/src/ch25-concurrency-threads.md @@ -3,9 +3,7 @@ ## Threads as values A thread is an owning handle that manages a native operating‑system thread. The handle follows move‑only semantics, just like `std::unique_ptr`. When the handle is destroyed the thread is terminated cleanly, either by calling `join()` explicitly (`std::thread`) or automatically (`std::jthread`). The automatic variant joins in its destructor, so the resource is always released. -Threads are not free. Creating a thread has a real cost in system resources and scheduling, so the book treats them as an explicit resource to manage, not a convenience to sprinkle. The `jthread` and `stop_token` model makes ownership and shutdown explicit, which is what lets a program terminate cleanly instead of leaking threads or deadlocking. - -A `std::thread` is move-only because a thread cannot be copied: there is no way to duplicate an OS thread. Moving transfers the handle, so exactly one owner exists at any time, the same invariant `std::unique_ptr` enforces for memory. A `std::thread` that is destroyed while still joinable calls `std::terminate`, which is why the scoped `std::jthread` is the safer default. It joins on destruction, so an early return or a thrown exception in the middle of a function cannot leave an unjoined thread behind. `detach()` exists but abandons the thread, and a detached thread outlives its creator, which makes lifetime reasoning difficult. +Threads are not free. Creating one consumes system resources and scheduling time, so they must be treated as explicit resources. A `std::thread` is move‑only because an OS thread cannot be duplicated. Moving transfers the unique handle, mirroring `std::unique_ptr` semantics. When a `std::thread` is destroyed while still joinable it calls `std::terminate`. The scoped `std::jthread` joins in its destructor, guaranteeing clean shutdown even on early returns or exceptions. ```cpp {{#include ../../examples/ch25/ch25_jthread.cpp}} @@ -32,7 +30,9 @@ Four threads increment a counter 25 000 times each. The final count printed eq The result is deterministic precisely because the mutex serialises the increments. Without it, the final count is less than the expected value, but the exact shortfall differs run to run, which is the signature of a data race. A passing run under one compiler or optimisation level gives no guarantee, which is why the guidelines treat any unprotected access as a defect regardless of whether a particular run appears correct. -`std::scoped_lock` is the variadic form that locks several mutexes at once, avoiding the deadlock that can arise when two threads lock the same two mutexes in different orders. Locking them together with a single `scoped_lock` guarantees a consistent order. The RAII form is mandatory in this book: a bare `lock()`/`unlock()` pair leaks a lock on any early return or thrown exception, and the compiler cannot help. The guideline is to hold a lock only as long as needed and to prefer a value-typed design that needs no shared mutable state at all. +`std::scoped_lock` is the variadic form that locks several mutexes at once. It prevents deadlock that can arise when two threads lock the same two mutexes in opposite orders. Locking them together with a single `scoped_lock` enforces a consistent lock order. + +The RAII form is mandatory in this book. A bare `lock()`/`unlock()` pair leaks a lock on any early return or thrown exception, and the compiler cannot help. Hold a lock only as long as needed and prefer a value‑typed design that eliminates shared mutable state. ## Condition variables and `condition_variable_any` A producer-consumer pattern frequently uses a condition variable so that the consumer sleeps until data is available. `std::condition_variable_any` works with any lock type that satisfies the BasicLockable concept, including `std::scoped_lock`. The consumer waits in a loop because spurious wake‑ups are allowed by the specification. The loop re‑checks the predicate after each wake‑up. @@ -40,7 +40,7 @@ A producer-consumer pattern frequently uses a condition variable so that the con ```cpp {{#include ../../examples/ch25/ch25_prod_cons.cpp}} ``` -The producer pushes integers onto a shared queue and notifies the consumer. Both threads also monitor a stop token, allowing the program to terminate without deadlock. +The producer pushes integers onto a shared queue and notifies the consumer. Both threads also monitor a stop token, which allows the program to terminate without deadlock. The wait must always be a loop around a predicate. A condition variable can wake spuriously, and another thread can consume the data between the notification and the waiter reacquiring the lock. The predicate captures both concerns: `cv.wait(lock, []{ return !q.empty(); })` re-checks the condition after every wake-up and sleeps again if it is still false. The lock passed to `wait` is released during the wait and reacquired before returning, so the predicate sees a consistent view of the queue. @@ -49,7 +49,7 @@ The wait must always be a loop around a predicate. A condition variable can wake The shared state is the contract. The producer sets it, and the consumer reads it exactly once via `get()`. If the provider throws, the exception is captured in the shared state and rethrown when `get()` runs on the consumer side, so errors cross the thread boundary as values. A future is one-shot. Calling `get()` twice is a programming error. `std::async` is a convenience wrapper, but it does not offer the stop tokens, explicit lifetimes, or fine-grained control that `std::jthread` provides, so the book treats it as a quick path, not the general tool. -A common misuse is spawning a thread per small task and blocking on its future, which is slower than running the tasks sequentially because the thread overhead dominates. Threads pay off when work is substantial, independent, and long-running. For lightweight parallel work, the algorithms in chapter 12 with an execution policy are the better fit. +A common misuse is creating a thread for each tiny task and immediately waiting on its future. The thread overhead outweighs the work. Use futures only for substantial, independent tasks. For fine‑grained parallelism, prefer execution policies as in chapter 12. ## Data‑race definition A data race occurs when two threads access the same non‑atomic object, at least one access is a write, and the accesses are not ordered by a *happens‑before* relation. The C++ Core Guidelines (CP.1-CP.8) require that all shared mutable state be either protected by synchronization primitives or be atomic. Violating this rule yields undefined behaviour, which can manifest as corrupted values, crashes, or apparently correct execution that later breaks with a different optimisation level. @@ -73,7 +73,7 @@ The workflow is to run the same program under every sanitizer the platform offer Phase‑synchronisation primitives help coordinate groups of threads. * `std::latch` counts down a fixed number of arrivals and releases waiting threads once the count reaches zero. It cannot be reused. -* `std::barrier` performs the same task but resets after each phase, allowing repeated coordination. +* `std::barrier` performs the same task but resets after each phase, which allows repeated coordination. * `std::atomic_ref` enables atomic operations on an existing non‑atomic object without copying it into an `std::atomic`. The example below creates four threads that announce readiness, then wait on a latch. When all threads have called `count_down()`, the latch releases them simultaneously. @@ -83,9 +83,7 @@ The example below creates four threads that announce readiness, then wait on a l ``` The final line confirms that all threads completed their work. -`std::latch` fits one-time coordination: wait until N threads have arrived. `std::barrier` fits repeated phases, such as a loop where every thread must finish iteration K before any starts iteration K+1. `std::atomic_ref` is the low-level tool: it views an existing `int` or `bool` as atomic for the duration of the ref, which lets threads share a non-atomic object without copying it, as long as every access goes through the ref. - -The latch in the example does not impose order. It only guarantees that every thread has reached the barrier point before any proceeds, which satisfies the typical requirement that all threads reach the barrier before any thread proceeds to start a batch of workers together. Reaching the latch is a synchronization event that establishes happens-before between the releasing thread and the released threads. +`std::latch` fits one‑time coordination: it releases waiting threads once N arrivals occur. `std::barrier` resets after each phase, which allows repeated coordination. `std::atomic_ref` provides atomic operations on an existing non‑atomic object without copying. The example's latch does not impose order. It merely ensures all threads reach the barrier before any proceeds. This establishes a happens‑before relation between the releasing thread and the released threads. ## Try this Build a two‑stage pipeline. A producer `std::jthread` generates integers and pushes them into a thread‑safe queue. A consumer `std::jthread` removes items from the queue and prints them. When the producer finishes, it requests stop via a shared `std::stop_source`. The consumer must observe this request and exit without leaving items in the queue or deadlocking. Verify that the program terminates cleanly and that no thread remains blocked. diff --git a/book/src/ch26-concurrency-atomics.md b/book/src/ch26-concurrency-atomics.md index a028b19..fc9423f 100644 --- a/book/src/ch26-concurrency-atomics.md +++ b/book/src/ch26-concurrency-atomics.md @@ -1,17 +1,14 @@ # Concurrency II: atomics ## Memory model for experts -The C++ memory model defines the rules that determine when an operation performed by one thread becomes visible to another thread. Within a single thread the compiler must preserve **sequenced‑before** order, meaning that each statement follows the preceding statement in program order. Between threads the model introduces the concept of **happens‑before**. A *release* operation on a synchronization object creates a synchronisation point. A matching *acquire* operation on another thread observes that release. All writes that occur before the release become visible to every operation that occurs after the acquire. +The C++ memory model defines when an operation performed by one thread becomes visible to another. Within a single thread the compiler must preserve **sequenced‑before** order, so each statement follows the preceding statement in program order. Between threads the model introduces **happens‑before**: a *release* on a synchronization object creates a synchronisation point, and a matching *acquire* on another thread observes that release. All writes that occur before the release become visible to every operation that occurs after the acquire. -The same rule applies to a mutex. The lock operation acts as an acquire. The unlock operation acts as a release. A mutex therefore establishes a happens‑before edge between the thread that unlocks and the thread that locks next.. The default ordering for every atomic operation is **sequentially consistent**. That ordering builds a single total order that respects program order and therefore provides the strongest guarantee of visibility. Sequential consistency also guarantees that if two threads both observe each other’s writes, the observations will appear in a consistent order. +The same rule applies to a mutex: `lock` acts as an acquire and `unlock` as a release, establishing a happens‑before edge from the unlocking thread to the next locking thread. By default every atomic operation uses **sequentially consistent** ordering, which builds a single total order respecting program order and thus provides the strongest visibility guarantee. Sequential consistency also ensures that if two threads observe each other’s writes, the observations appear in a consistent order. -### Why the default matters -Because the default is the strongest, library code that does not explicitly specify a weaker ordering can be reasoned about without having to track subtle reorderings. When performance requires a weaker ordering, the programmer must consciously choose `memory_order_relaxed`, `memory_order_acquire`, or `memory_order_release` and understand the consequences. - -Without a synchronization edge, two threads observing the same variable have no guarantee about which value they see or in what order, even if the writes happened in a sensible order. Sequentially consistent ordering is the safe default because it gives a single total order over all atomic operations, which is what most reasoning assumes. The cost is a full memory barrier, which on a weakly ordered CPU costs more than an acquire or release. That cost is why the weaker orderings exist and why they are chosen deliberately. The rule is to reason about the default first and to weaken an ordering only after a measured bottleneck. +Because sequential consistency is the strongest ordering, library code that does not specify a weaker order can be reasoned about without tracking subtle reorderings. When performance requires a weaker ordering, the programmer must explicitly choose `memory_order_relaxed`, `memory_order_acquire`, or `memory_order_release` and understand the consequences. Without a synchronization edge, two threads observing the same variable have no guarantee about which value they see or in what order, even if the writes occurred in a sensible order. The cost of the sequentially‑consistent barrier on weakly‑ordered CPUs motivates the existence of weaker orderings, which can be used only after a measured bottleneck. ## Acquire/release -The classic lock/unlock pair can be expressed directly with atomics. A thread stores a flag with `memory_order_release`. The waiting thread loads the same flag with `memory_order_acquire`. The release publishes the stored value together with any writes that occurred before it. The acquire reads the stored value and any writes that happened‑before the matching release. As a result every write that precedes the release becomes visible after the acquire. This pattern is the foundation of many lock‑free algorithms, for example a single‑producer single‑consumer queue that uses a release store to publish a pointer to a new node and an acquire load to retrieve it. +The classic lock/unlock pair can be expressed directly with atomics. A thread stores a flag with `memory_order_release`. The waiting thread loads the same flag with `memory_order_acquire`. The release publishes the stored value together with any prior writes. The acquire reads the stored value and any writes that happened‑before the matching release. Consequently every write that precedes the release becomes visible after the acquire. This pattern underlies many lock‑free algorithms, such as a single‑producer single‑consumer queue that releases a pointer to a new node and acquires it on the consumer side. Relaxed ordering does not create a synchronisation edge. It is safe only when a program does not rely on ordering between threads, for example a simple counter that is incremented without any other thread observing the intermediate values. In that case each increment can be performed with `memory_order_relaxed` because the final value is the only observable result. @@ -20,14 +17,14 @@ Relaxed ordering does not create a synchronisation edge. It is safe only when a The example compiles with -std=c++26 and demonstrates the discussed ordering behavior. ``` -Acquire and release are asymmetric in a useful way. A release store can be paired with many acquire loads, each of which gets a consistent view of everything published by the release. This is the pattern behind a lock: the unlock is a release, and every subsequent lock is an acquire, so the lock protects all the writes inside the critical section. A common mistake is to use relaxed for the flag itself and then expect ordering, which silently drops the synchronization edge and reintroduces the race it was meant to prevent. +A release store can be paired with many acquire loads, each obtaining a consistent view of everything published by the release. This asymmetry is the basis of a lock: the unlock is a release, and every subsequent lock is an acquire, protecting all writes inside the critical section. A common mistake is to use relaxed ordering for the flag itself while expecting ordering. This silently drops the synchronisation edge and re‑introduces the race it was meant to prevent. ## std::atomic and std::atomic_ref `std::atomic` provides lock‑free operations on a single word of memory. Functions such as `fetch_add`, `exchange`, and `compare_exchange_strong` modify the stored value without acquiring a mutex. When the data fits in a single machine word, atomics are typically faster than a mutex because they avoid kernel calls and context switches. They also avoid priority‑inversion problems that can arise when a high‑priority thread blocks on a mutex held by a low‑priority thread. `std::atomic_ref` creates an atomic view of existing storage. This is useful when code already has a plain variable that must be accessed atomically in a few places without converting the whole object to `std::atomic`. The reference does not own the storage. It merely adds atomic operations on top of it. -`compare_exchange_strong` is the workhorse of lock-free code. It updates a value only if it still equals an expected value, atomically, and reports whether the update happened. It is the basis of retry loops that build a new value from the current one. Whether an `std::atomic` is truly lock-free is a runtime property reported by `is_always_lock_free`. On the platforms this book targets, word-size atomics are lock-free in practice. +`compare_exchange_strong` is the workhorse of lock‑free code. It updates a value only if it still equals an expected value, atomically, and reports whether the update happened. It is the basis of retry loops that build a new value from the current one. Whether an `std::atomic` is truly lock‑free is a runtime property reported by `is_always_lock_free`. On the platforms this book targets, word‑size atomics are lock‑free in practice. The following example increments a shared counter with `fetch_add`. The counter is declared as `std::atomic`. Ten threads each perform one hundred thousand increments. The final value printed by the program equals the product of the thread count and the per‑thread increment count, demonstrating that no increments are lost. @@ -40,27 +37,22 @@ The following example increments a shared counter with `fetch_add`. The counter The spinlock implementation shown below is simple and portable. It demonstrates the core idea without any back‑off or pause instructions. The spinlock protects a shared integer that two threads increment many times. The final value matches the expected total, confirming correctness. -The drawback of a spinlock is that while a thread waits it consumes CPU cycles. On oversubscribed systems this can degrade performance compared with a mutex that puts the waiting thread to sleep. For low‑contention short critical sections a spinlock can still be the right choice. - -A spinlock is only sensible when the critical section is very short and contention is low, because a waiting thread burns a whole CPU core. On a single core, a spinlock can deadlock if the owning thread is preempted while holding the lock, because the spinners cannot run. A mutex avoids both problems by blocking the thread. In this book a mutex is the default and a spinlock is an illustration of what atomics make possible rather than a recommended production lock. +The drawback of a spinlock is that while a thread waits it consumes CPU cycles. On oversubscribed systems this can degrade performance compared with a mutex that puts the waiting thread to sleep. A spinlock is sensible only for very short critical sections with low contention. Otherwise a waiting thread burns a whole CPU core and can even deadlock on a single core if pre‑empted while holding the lock. In this book a mutex is the default. The spinlock illustrates what atomics make possible rather than a recommended production lock. ```cpp {{#include ../../examples/ch26/ch26_spinlock.cpp}} ``` ## std::async in the rear‑view mirror -`std::async` launches a function and returns a `std::future`. It is convenient for occasional parallelism because the caller does not need to manage thread objects directly. However the abstraction hides the underlying thread creation and therefore makes it hard to control scheduling, thread pool usage, or cancellation. It also does not compose with other asynchronous primitives such as continuations or I/O operations. +`std::async` launches a function and returns a `std::future`. It is convenient for occasional parallelism because the caller does not need to manage thread objects directly. However the abstraction hides the underlying thread creation, which makes it hard to control scheduling, thread‑pool usage, or cancellation. It also does not compose with other asynchronous primitives such as continuations or I/O operations. -The language and library community view `std::async` as a legacy convenience. Modern code prefers the *sender/receiver* model defined by P2300 because it separates the description of work from the mechanism that runs it. - -`std::async` also ties the task to a specific `std::future`, so the result must be consumed by name and there is no way to express a graph of dependent computations without nesting calls. The eager thread creation means a fire-and-forget call can launch far more threads than a machine has cores. These limitations are why the standard is moving toward senders, which describe the graph first and let the runtime decide how to execute it. +The language and library community view `std::async` as a legacy convenience. Modern code prefers the *sender/receiver* model defined by P2300 because it separates the description of work from the mechanism that runs it. `std::async` also ties the task to a specific `std::future`, so the result must be consumed by name and there is no way to express a graph of dependent computations without nesting calls. The eager thread creation can launch far more threads than a machine has cores, which is why the standard is moving toward senders that describe the graph first and let the runtime decide how to execute it. ## The async future: std::execution (P2300) The sender/receiver framework decouples task creation from execution. A **sender** describes a computation without actually running it. A **receiver** supplies callbacks for success, error, or cancellation. Operators such as `schedule`, `then`, and `sync_wait` compose senders into pipelines. The pipeline remains lazy. No thread is created until a terminal operator such as `sync_wait` forces execution. This design differs from `std::future`, which typically spawns a thread when the `future` is created. > **Not yet deployable.** - The pipeline reads as a single expression: `schedule(sched) | then(f) | sync_wait()`. Nothing runs until `sync_wait` forces it, so the description and the execution are separate. This separation lets a scheduler choose a thread pool, a GPU, or an event loop without changing the pipeline, which is the property `std::future` cannot offer. A sender graph can branch and join, so two independent computations can be scheduled together and combined, something a single future cannot express. ```cpp @@ -74,9 +66,4 @@ Lock‑free data structures need safe memory reclamation because a thread can re Read‑copy‑update (``) offers another reclamation strategy. Writers create a new version of a data structure while readers continue to access the old version. After a *grace period* during which all pre‑existing readers have finished, the old version can be reclaimed. RCU works well for read‑heavy workloads because readers incur almost no synchronization cost. However it requires a mechanism to detect when all readers have reached a quiescent state. -The danger reclamation solves is subtle. If a thread frees a node while another thread still reads it, the reading thread dereferences freed memory, which is undefined behaviour. Waiting for a reference count or hazard pointer avoids that, but each scheme has a cost: hazard pointers require announcing access, and RCU requires a grace period before reuse. Hand-rolling either is a common source of bugs, which is why the standard is adding them as libraries and why the book does not implement one. - -> **Not yet deployable.** - -## Try this -Write a litmus test that uses two relaxed stores to two atomic flags and two relaxed loads of the opposite flag. Run the test many times and observe whether both loads can see the initial value of the opposite flag simultaneously. Record the conclusion about the guarantees provided by relaxed ordering. +The danger reclamation solves is subtle. If a thread frees a node while another thread still reads it, the reading thread dereferences freed memory, which is undefined behaviour. Waiting for a reference count or hazard pointer avoids that, but each scheme has a cost: hazard pointers require announcing access, and RCU requires a grace period before reuse. Hand‑rolling either is a common source of bugs, which is why the standard is adding them as libraries and other ... (truncated) \ No newline at end of file diff --git a/book/src/ch27-coroutines.md b/book/src/ch27-coroutines.md index 6a99668..a6a0300 100644 --- a/book/src/ch27-coroutines.md +++ b/book/src/ch27-coroutines.md @@ -2,11 +2,9 @@ ## Suspension as a captured continuation -A coroutine is a function that can pause its execution and later continue from the exact point where it stopped. When the compiler encounters a suspension point, such as `co_yield`, `co_await`, or `co_return`, it rewrites the entire function body into a collection of *blocks*. Each block ends with a store of the current local variables and a jump to the next block. The stored state is a **continuation**, a callable object that represents "the rest of the work". Resuming the coroutine calls that continuation, which restores the saved variables and proceeds to the following block. The transformation is analogous to a Lisp macro that expands a form into a closure that captures its environment. The programmer writes a linear sequence of suspension statements. The compiler generates the state machine that passes control back and forth between the caller and the continuation. +A coroutine can pause and later resume at the same point. The compiler transforms the function into a state machine of blocks, each ending with a stored continuation that captures the current locals. This continuation is a callable object representing the remaining work. The compiler allocates a heap‑based coroutine frame for locals that survive suspension. -The continuation model gives the compiler full control over lifetimes. All locals that survive across a suspension point are moved into the coroutine frame, which lives on the heap. When the coroutine is destroyed, the frame is reclaimed, guaranteeing that no dangling references remain. This design also enables optimisations such as eliding the frame when the coroutine never actually suspends. Moreover, the compiler can apply escape‑analysis to detect when the frame can be allocated on the stack, enhancing performance for short‑lived coroutines. - -The state machine the compiler generates is invisible to the programmer. Each suspension point becomes a label, and the local variables that must survive a suspension are stored in a heap-allocated frame rather than on the stack. This is why a coroutine frame lives on the heap, unlike an ordinary function's stack frame, and why the compiler, not the programmer, manages that allocation. Reading a coroutine as linear code is correct because the transform is mechanical. +The compiler can optimise away the frame or allocate it on the stack when possible. The generated state machine is invisible to the programmer, allowing the coroutine to be read as linear code. ## The protocol: promise, awaiter, handle @@ -18,15 +16,13 @@ The **awaiter** is a temporary object produced by the promise when a `co_await` The **handle** (`std::coroutine_handle`) is a thin pointer to the suspended coroutine’s frame. It is the only object that can be stored, moved, or destroyed by user code. The handle provides `resume`, `destroy`, and `done`. End users normally manipulate only the handle returned by the promise. Library authors implement the promise and awaiter to expose a convenient API. The handle remains a low‑level plumbing artifact. -The three parts interact as follows: The promise creates the coroutine frame and returns a handle. The awaiter decides whether execution must pause. When the awaiter signals suspension the handle is stored inside the awaiting context. Later a call to `handle.resume()` invokes the continuation that was captured earlier. This separation of concerns lets library writers specialise behaviour (e.g., asynchronous I/O) without exposing the low‑level mechanics to typical code. +The promise creates the coroutine frame and returns a handle. The awaiter decides whether to suspend and, if so, stores the handle in the awaiting context. A later `handle.resume()` invokes the captured continuation. This separation lets library writers specialise behaviour (e.g., asynchronous I/O) without exposing low‑level mechanics. A practical illustration is the standard `std::generator`. Its promise type stores the most recent yielded value and implements `yield_value` by saving that value and returning `std::suspend_always`. The awaiter in this case is trivial: every `co_yield` forces a suspension, and the handle is resumed by the range‑for iterator each time it requests the next element. ## Libraries own the machinery -The C++ standard supplies a minimal protocol but does not expect most programmers to interact with it directly. Instead the standard library and third‑party libraries provide ready‑made abstractions such as `std::generator`, `std::task`, or `std::async`. These wrappers hide the promise, awaiter, and handle behind a clean interface. The guideline is to consume a coroutine by using a library type and to write a new promise type only when you need a custom behaviour that no existing library supplies. This keeps user code readable and limits the amount of boiler‑plate that must be maintained. - -Because the library types are themselves templates, they can be combined with other generic facilities. For example, a `std::generator` can be wrapped in `std::ranges::view_interface` to expose the full range adaptor API. This composability is a cornerstone of modern C++ design: write the low‑level plumbing once, then reuse it through higher‑level abstractions. +The C++ standard supplies a minimal protocol but does not expect most programmers to interact with it directly. Instead the standard library and third‑party libraries provide ready‑made abstractions such as `std::generator`, `std::task`, or `std::async`. These wrappers hide the promise, awaiter, and handle behind a clean interface. The guideline is to consume a coroutine by using a library type and to write a new promise type only when you need a custom behaviour that no existing library supplies. Because the library types are templates, they can be combined with other generic facilities. For example, a `std::generator` can be wrapped in `std::ranges::view_interface` to expose the full range adaptor API. This composability is a cornerstone of modern C++ design: write the low‑level plumbing once, then reuse it through higher‑level abstractions. Additionally, the standard library provides utility awaiters such as `std::suspend_never` and `std::suspend_always`, as well as types derived from `std::suspend_always`, which integrate with the executor model introduced in later standards. Library authors can build higher‑level primitives, such as asynchronous file reads, by defining a custom promise that stores the I/O state and an awaiter that registers the operation with an event loop. @@ -56,9 +52,7 @@ This illustration is for reading only. Production code must prefer `std::generat ## Coroutines and ranges -A `std::generator` satisfies the input‑range requirement, therefore it can be piped through any range adaptor defined in chapter 13. For instance `std::views::take(5) | std::ranges::to()` will materialise the first five values of a generator into a vector. This composition shows that coroutines can serve as a natural source of lazy data for the modern range pipeline. - -Because generators are lazy, they can be combined with other lazy views without incurring intermediate storage. A pipeline such as `std::views::filter(is_even) | std::views::transform(square)` applied to a `std::generator` will compute each value only once, on demand, and propagate the result directly to the final consumer. This property is particularly useful when dealing with expensive computations or I/O‑bound sources, where materialising the whole sequence is prohibitive. Piping a generator into `std::ranges::to()` materialises it once, at the boundary where ownership is needed, matching the chapter 13 rule. +A `std::generator` satisfies the input‑range requirement and can be piped through any range adaptor from chapter 13. Because it yields values lazily, combining it with other lazy views incurs no intermediate storage. Each value is computed on demand. For example, `std::views::take(5) | std::ranges::to()` materialises the first five values, while `std::views::filter(is_even) | std::views::transform(square)` processes each element once. Materialisation occurs only at the boundary where ownership is required, matching the chapter 13 rule. Another practical scenario is streaming data from a file or network socket. A coroutine can `co_await` an asynchronous read operation, `co_yield` each chunk as it arrives, and the surrounding range pipeline can `std::ranges::copy` the elements into a container or process them directly. The composition remains expression‑only, keeping the code concise and adhering to the Core Guidelines emphasis on clear intent. diff --git a/book/src/ch28-speaking-c.md b/book/src/ch28-speaking-c.md index 7e9363a..e830d4f 100644 --- a/book/src/ch28-speaking-c.md +++ b/book/src/ch28-speaking-c.md @@ -2,7 +2,13 @@ ## extern "C" and the ABI -When a C++ translation unit calls a function defined in a C library the programmer adds the `extern "C"` specifier. The specifier directs the compiler to give the declared function C linkage. C linkage disables name mangling. It also forces the compiler to use the calling convention defined by the C ABI for the target platform. The ABI (application binary interface) is a contract between caller and callee. It defines the order in which arguments are placed, the registers that hold return values, and how the stack is cleaned. The ABI also defines how variadic functions pass arguments, how floating point values are promoted, and how structures are laid out in memory. When a function is declared with `extern "C"` the C++ compiler pretends to be a C compiler for those details. This eliminates subtle mismatches that can corrupt the stack or misinterpret data. +When a C++ translation unit calls a function defined in a C library, the programmer adds the `extern "C"` specifier. The specifier directs the compiler to give the declared function C linkage, which disables name mangling and forces the calling convention defined by the C ABI for the target platform. + +The ABI (application binary interface) is a contract between caller and callee. It defines the order in which arguments are placed, which registers hold return values, how the stack is cleaned, how variadic arguments are passed, how floating‑point values are promoted, and how structures are laid out in memory. + +Name mangling exists because C++ encodes a function’s name, namespace, and parameter types into a single symbol so overloading works. C does not mangle names. Therefore a C function such as `fopen` is linked under the plain symbol `fopen`. A C++ declaration must carry C linkage, or the linker will look for a mangled name that does not exist. This is why every C header is wrapped in `extern "C" { … }` behind a guard, ensuring a C++ translation unit receives the correct linkage automatically. + +The ABI is fixed per platform and shared by C and C++. A function with C linkage can be implemented in either language, but signatures must match exactly. Verify signatures when mixing C and C++ code. When a function is declared with `extern "C"` the C++ compiler pretends to be a C compiler for those details. This eliminates subtle mismatches that can corrupt the stack or misinterpret data. ```cpp extern "C" int c_func(int); @@ -14,14 +20,17 @@ int main() { The example compiles with a C library that defines `int c_func(int)`. The function can be called from C++ without any additional glue code. -Name mangling is the reason the specifier exists. C++ encodes a function's name, its namespace, and its parameter types into a single mangled symbol so overloading works. C does no such thing. A `fopen` in a C library is linked under the plain symbol `fopen`, so a C++ declaration of it must carry C linkage or the linker looks for a mangled name that does not exist. This is why every C header is wrapped in `extern "C" { ... }` behind a guard, so a C++ translation unit includes it and gets the correct linkage automatically. -The ABI is fixed per platform and is shared by C and C++ at the boundary, so a function declared with C linkage can be implemented in either language and called from the other. This is what makes a C library usable from C++ and a C++ function callable from C, provided the signatures are compatible. Variadic functions and struct return types are where the two languages can diverge, which is why a C library's headers specify the exact signatures. -When mixing C and C++ code, always verify that the function signatures match exactly to avoid linkage errors. + + ## C data shapes in C++ -C strings are arrays of `char` terminated by a NUL byte. In C++ a borrowed view of such a string can be expressed with `std::string_view`. When the program needs ownership it must copy the characters into a `std::string`. This rule prevents accidental modification of memory that belongs to the C library. C arrays map naturally to `std::span`. The span provides a pointer and a length without giving write access beyond the bounds of the original array. The span type never owns the memory. It merely observes it. File handles such as `FILE*` are raw resources. The book's RAII pattern wraps them in a class whose destructor calls `fclose`. The wrapper owns the handle and therefore must not be copied. Moving the wrapper transfers ownership safely. +C strings are arrays of `char` terminated by a NUL byte. In C++ a borrowed view can be expressed with `std::string_view`. When the program needs ownership, copy the characters into a `std::string`. This avoids modifying memory owned by the C library. + +C arrays map naturally to `std::span`. A span holds a pointer and a length without granting write access beyond the original array bounds. The span does not own the memory. It only observes it. + +File handles such as `FILE*` are raw resources. The book's RAII pattern wraps them in a class whose destructor calls `fclose`. The wrapper owns the handle, forbids copying, and transfers ownership via move semantics. The mapping is not automatic. The programmer states it. A `const char*` from a C function is a borrowed view that is valid only as long as the C side keeps the buffer alive, so wrapping it in a `std::string_view` inherits that lifetime and must not outlive the buffer. Copying into a `std::string` breaks the dependency and is the right move when the value must persist. The same reasoning governs every C pointer: the C++ type you wrap it in must match what the C API actually guarantees. @@ -29,9 +38,7 @@ The mapping is not automatic. The programmer states it. A `const char*` from a C Many C functions report failure by returning a sentinel value such as `-1` and setting the global variable `errno`. The C++ library `std::expected` offers a modern way to model this pattern. The wrapper converts the sentinel return and the `errno` value into a rich error object. The example uses `open` to demonstrate conversion. When `open` fails the wrapper returns `std::unexpected` that contains the error message from `strerror(errno)`. The caller can test the `std::expected` and handle success or failure without consulting a global variable. -`errno` is a thread-local variable, but relying on it is still fragile because a failed call sets it and a later call can overwrite it before the caller reads it. The wrapper captures `errno` immediately at the failure point and turns it into a value, so the error is localised and cannot be clobbered. This is the whole point of `std::expected`: the error travels with the result instead of living in a global the caller must remember to check. The same pattern applies to any C API that reports errors through a sentinel plus a side channel. - -Reading `errno` after a call is a race with any other call in the same thread that runs between the failing call and the read. Capturing it in the wrapper at the failure point is the only safe way to transport the error, which is why the wrapper exists. +`errno` is thread‑local, but reading it later risks a race: another call can overwrite it before the value is captured. The wrapper records `errno` at the failure point, turning it into a value that travels with the result via `std::expected`. This avoids the race and applies to any C API that uses a sentinel plus a side channel. ```cpp {{#include ../../examples/ch28/ch28_errno.cpp}} @@ -53,13 +60,13 @@ The test looks for the substring "hello world" in the program output. ## Ownership at the boundary -The rule of ownership is simple. The C API owns any object that the documentation says the caller must free. A pointer returned by a function is borrowed unless the specification explicitly transfers ownership. The `gsl::owner` annotation can be placed on a pointer type to remind the reader that the program must release the resource. The annotation does not change the code but clarifies the contract for human readers and static analysis tools. +The rule of ownership is simple. The C API owns any object that the documentation says the caller must free. Otherwise a returned pointer is borrowed. The `gsl::owner` annotation marks owned pointers, clarifying the contract for readers and static analysis tools. When ownership is transferred, the caller must release the resource with the matching free function. Borrowed pointers must not be freed. Misusing ownership leads to use‑after‑free or leaks, which the annotation helps prevent. ```cpp // gsl::owner file = fopen("path", "r"); ``` -The distinction between borrowed and owned is the whole of C memory discipline. If a function returns a pointer and the documentation says the caller owns it, the caller must release it, usually with the matching free function. If the documentation is silent, the pointer is borrowed and must not be freed. Mistaking one for the other is a use-after-free or a leak, and neither is detectable by the compiler. `gsl::owner` makes the intent visible so a reviewer or a static analysis tool can check that the owned pointer is eventually released. + ## Spans over C arrays diff --git a/book/src/ch29-fast.md b/book/src/ch29-fast.md index 24aa5d7..306f699 100644 --- a/book/src/ch29-fast.md +++ b/book/src/ch29-fast.md @@ -53,7 +53,7 @@ Running the program yields a line such as `copy: 1 move: 1`. The numbers confirm Move operations are `noexcept` for the standard containers, and that single word unlocks a real optimisation. `std::vector` uses the move constructor during growth only when it is guaranteed not to throw. If the move can throw, the vector has to copy instead, to keep the strong exception guarantee. Marking your own types' move constructors `noexcept` is therefore not ceremony. It is what lets `vector` move them during reallocation rather than copy. -Chapter 5 showed that moving a `Counter` incurs far less work than copying, reinforcing the performance advantage of move semantics. +See Chapter 5 for a detailed comparison of move versus copy costs. ## Copy elision and RVO diff --git a/book/src/preface.md b/book/src/preface.md index 387c6b0..f7cd6ac 100644 --- a/book/src/preface.md +++ b/book/src/preface.md @@ -1,6 +1,6 @@ # Preface -This book teaches C++ as if it has only ever been C++26. No legacy constructs appear except a fixed allow‑list that is taught explicitly and framed honestly: raw pointers and `new`/`delete` to explain RAII, virtual functions in a single section on type erasure, C strings and `errno` in the chapters on headers and C interop, and SFINAE in a reading‑fluency chapter, because a working programmer must be able to read the codebase they inherit even when they write modern code. +This book teaches C++ as if it were always C++26, using only a small allow‑list of legacy constructs taught explicitly: raw pointers and `new`/`delete` for RAII, virtual functions for type erasure, C strings and `errno` for C interop, and SFINAE for reading existing code. The book has four parts and a set of appendices. Part I covers the language. Part II covers the standard library. Part III covers generic and compile‑time programming. Part IV covers systems topics. The appendices hold reference material, including a feature table and a Core Guidelines index. diff --git a/scripts/chapter_lint.py b/scripts/chapter_lint.py index 730d5f3..451759c 100755 --- a/scripts/chapter_lint.py +++ b/scripts/chapter_lint.py @@ -7,14 +7,14 @@ mechanical rules of ASD-STE100 Simplified Technical English (Issue 9), plus a ban on em/en dashes and semicolons. A chapter agent must run this and must NOT declare the chapter done until it exits 0. -Usage: python3 scripts/chapter_lint.py [--no-length] [minwords] +Usage: python3 scripts/chapter_lint.py [--no-length] Exit codes: 0 lint passed 1 a hard check failed (the chapter is not done) 2 usage error (missing file, bad arguments) ---no-length skip the word floor and the sentence/paragraph length checks. +--no-length skip the sentence/paragraph maximum length checks. Front matter (preface, appendices) is exempt from length checks, but it still obeys every other rule. @@ -365,10 +365,9 @@ def main(argv: list[str]) -> int: no_length = "--no-length" in argv argv = [a for a in argv if a != "--no-length"] if len(argv) < 2: - print(f"usage: {argv[0]} [minwords]", file=sys.stderr) + print(f"usage: {argv[0]} ", file=sys.stderr) return 2 path = argv[1] - min_words = int(argv[2]) if len(argv) > 2 else 1700 try: with open(path, encoding="utf-8") as fh: @@ -384,13 +383,7 @@ def main(argv: list[str]) -> int: if not no_length: check_judgment(parsed.prose, parsed.paragraphs, res) - # Word floor. Skipped for front matter, which is exempt from length checks. - if not no_length: - words = len(text.split()) - if words < min_words: - res.hard_failures.append(f"{words} words < {min_words}") - else: - words = len(text.split()) + words = len(text.split()) # Report. for w in res.warnings: