diff --git a/.gitignore b/.gitignore index fcc74de..1c99d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /html /result /.direnv +/__pycache__/ +*.pyc diff --git a/book/src/ch01-toolchain.md b/book/src/ch01-toolchain.md index 244373f..5c84cf5 100644 --- a/book/src/ch01-toolchain.md +++ b/book/src/ch01-toolchain.md @@ -2,7 +2,17 @@ ## The thesis -In this book the tooling around C++ is presented 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 are intended to be machine enforceable, and the committee enforces them. 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. +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. + +``` +clang++ front-end compiler +clang-tidy lint suite +clang-analyzer path-sensitive static analyzer +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. ## Getting the toolchain @@ -41,7 +51,7 @@ The next example illustrates a classic lifetime violation. The file returns a po {{#include ../../examples/ch01/ch01_dangling.cpp}} ``` -The diagnostic that appears in the comment of the example is reproduced here verbatim: +The diagnostic that appears in the comment of the example appears again here verbatim: ``` examples/ch01/ch01_dangling.cpp:9:13: warning: address of stack memory associated with local variable 'x' returned [-Wreturn-stack-address] @@ -49,9 +59,9 @@ examples/ch01/ch01_dangling.cpp:9:13: warning: address of stack memory associate | ^ ``` -The warning is emitted by the `-Wreturn-stack-address` check, which is part of the standard warning set. The book treats this warning as a language law. The book compiles its real examples with `-Werror`, which turns every warning into a hard error, and encourages the reader to do the same. The deliberately wrong demos keep their warning visible to serve as the exhibit, as described under `book_example` and `book_demo` in the section `## CMake As The Contract`. The larger ambition of the committee is to provide a unified lifetime‑safety profile. The flag that represents this profile in current clang builds is `-Wlifetime-safety`. On the pinned clang 22.1.8 this flag does not exist directly. Instead the experimental spelling `-Xclang -fexperimental-lifetime-safety` is accepted, but it produces almost no diagnostics on the cases shown in chapter 7. The book therefore relies on the diagnostics that do fire in the shipped toolchain, while still naming the lifetime‑safety analysis as the law that the committee intends to enforce. +The `-Wreturn-stack-address` check emits the warning, and the check is part of the standard warning set. The book treats this warning as a language law. The book compiles its real examples with `-Werror`, which turns every warning into a hard error, and encourages the reader to do the same. The deliberately wrong demos keep their warning visible to serve as the exhibit, as described under `book_example` and `book_demo` in the section `## CMake As The Contract`. The larger ambition of the committee is to provide a unified lifetime‑safety profile. The flag that represents this profile in current clang builds is `-Wlifetime-safety`. On the pinned clang 22.1.8 this flag does not exist directly. Instead the compiler accepts the experimental spelling `-Xclang -fexperimental-lifetime-safety`, but it produces almost no diagnostics on the cases shown in chapter 7. The book therefore relies on the diagnostics that do fire in the shipped toolchain, while still naming the lifetime‑safety analysis as the law that the committee intends to enforce. -> **Not yet deployable.** The lifetime‑safety analysis is young. The flag `-Wlifetime-safety` will become a full diagnostic source in future clang releases. Until then the analysis is visible only through the experimental option and the traditional warnings such as `-Wreturn-stack-address`. The flag `-Wuninitialized` also catches use of a variable before a value is stored and is enabled by default. +> **Not yet deployable.** The lifetime‑safety analysis is young. The flag `-Wlifetime-safety` will become a full diagnostic source in future clang releases. Until then the analysis is visible only through the experimental option and the traditional warnings such as `-Wreturn-stack-address`. The flag `-Wuninitialized` also catches use of a variable before a value stores into it, and clang enables it by default. ## Sanitizers as Compile Options @@ -61,7 +71,7 @@ Dynamic checking is another member of the committee. The address sanitizer (ASan {{#include ../../examples/ch01/ch01_overflow.cpp}} ``` -The report pasted in the file is reproduced exactly: +The report pasted in the file appears again exactly: ``` ==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000fc @@ -71,7 +81,7 @@ READ of size 4 at 0x6020000000fc thread T0 #8 ... in main ch01_overflow.cpp:13 ``` -The program is compiled with the sanitizer flags `-fsanitize=address,undefined`. The static members of the committee (clang‑tidy and the static analyzer) catch many issues at compile time, while sanitizers catch the remaining runtime bugs. The undefined‑behavior sanitizer reports a range of UB such as signed integer overflow, shift overflow and null‑pointer dereference. It inserts runtime checks that abort execution as soon as the offending operation occurs. The instrumentation stays in the finished binary: the address sanitizer roughly doubles runtime and memory use in typical programs, so sanitizer builds are a test configuration, not the shipping binary. The book treats both classes of diagnostics as equally binding. +The build compiles the program with the sanitizer flags `-fsanitize=address,undefined`. The static members of the committee (clang‑tidy and the static analyzer) catch many issues at compile time, while sanitizers catch the remaining runtime bugs. The undefined‑behavior sanitizer reports a range of UB such as signed integer overflow, shift overflow and null‑pointer dereference. It inserts runtime checks that abort execution as soon as the offending operation occurs. The instrumentation stays in the finished binary: the address sanitizer roughly doubles runtime and memory use in typical programs, so sanitizer builds are a test configuration, not the shipping binary. The book treats both classes of diagnostics as equally binding. ## The Static Analyzer @@ -79,7 +89,7 @@ The clang static analyzer runs a path‑sensitive analysis that discovers bugs t ## The Rulebook -The committee’s rulebook lives in the repository root as `.clang‑tidy`. The file is quoted verbatim below. +The committee’s rulebook lives in the repository root as `.clang‑tidy`. The file appears verbatim below. ```cpp {{#include ../../.clang-tidy}} @@ -91,17 +101,17 @@ The configuration enables a broad set of checks grouped under the headings `bugp * `modernize-use-trailing-return-type`: the book prefers leading return types for readability. * `bugprone-exception-escape`: every example `main` can throw from `std::println`. This check flags all of them without improving safety. -Only the enabled checks form part of the language law for the book. The `HeaderFilterRegex: '.*'` setting applies the checks to every file in the repository. The `WarningsAsErrors: '*'` setting promotes every retained clang-tidy warning to a hard error, so a `book_example` that trips a check fails the build. Compiler warnings are promoted the same way by `-Werror` (see "Warnings are laws"). The deliberately-wrong `book_demo` targets run clang-tidy off, so their teaching diagnostics still reach the reader. +Only the enabled checks form part of the language law for the book. The `HeaderFilterRegex: '.*'` setting applies the checks to every file in the repository. The `WarningsAsErrors: '*'` setting promotes every retained clang-tidy warning to a hard error, so a `book_example` that trips a check fails the build. Compiler warnings become hard errors the same way through `-Werror` (see "Warnings are laws"). The deliberately-wrong `book_demo` targets run clang-tidy off, so their teaching diagnostics still reach the reader. ## CMake As The Contract -The build model for each example is deliberately simple. Every compiled example is wrapped in a CMake macro called `book_example`. The macro adds the source file as a target, attaches the clang‑tidy checks, enables the sanitizers, and registers a test with CTest. The macro also adds the flag `-Wlifetime-safety` when the compiler supports it, ensuring that the lifetime profile is exercised for every example that uses a reference or pointer. When the test runs the program, CTest verifies the expected output or the presence of a diagnostic in the standard output. The macro hides the details of the build system. The reader only needs to understand that each example is compiled once, checked by the committee, and exercised by a test. No deeper CMake teaching is required at this point. A typical invocation looks like this in `CMakeLists.txt`: +The build model for each example is deliberately simple. A CMake macro called `book_example` wraps every compiled example. The macro adds the source file as a target, attaches the clang‑tidy checks, enables the sanitizers, and registers a test with CTest. The macro also adds the flag `-Wlifetime-safety` when the compiler supports it, so the lifetime profile runs for every example that uses a reference or pointer. When the test runs the program, CTest verifies the expected output or the presence of a diagnostic in the standard output. The macro hides the details of the build system. The reader only needs to understand that the committee compiles each example once, checks it, and exercises it with a test. No deeper CMake teaching is necessary at this point. A typical invocation looks like this in `CMakeLists.txt`: ```cmake book_example(ch01_hello.cpp EXPECT "hello, world") ``` -The `EXPECT` argument tells CTest which regular expression to match on the program’s standard output. For the deliberately wrong demos the picture is different. They register a compile target but no test, because their exhibit is the compiler diagnostic itself, which the author pastes into the file's header comment. The macro also defines a `book_gap` variant for features that are not yet supported by the pinned compiler. Those targets are built only when the option `BOOK_ENABLE_GAPS=ON` is passed. +The `EXPECT` argument tells CTest which regular expression to match on the program’s standard output. For the deliberately wrong demos the picture is different. They register a compile target but no test, because their exhibit is the compiler diagnostic itself, which the author pastes into the file's header comment. The macro also defines a `book_gap` variant for features that are not yet supported by the pinned compiler. Those targets build only when the build passes the option `BOOK_ENABLE_GAPS=ON`. ## Try This @@ -109,6 +119,15 @@ The `EXPECT` argument tells CTest which regular expression to match on the progr Write a program that adds two signed 32‑bit integers where the result overflows the representable range. Compile the program with the flags `-fsanitize=address,undefined`. Run the binary and observe that the undefined‑behavior sanitizer emits a diagnostic while the address sanitizer remains silent. Explain which member of the committee produced each message. +```cpp +int main() { + int a = 2'000'000'000; + int b = 2'000'000'000; + int sum = a + b; // signed overflow, undefined behavior + return sum == 0; +} +``` + ### Make clang‑tidy flag a C‑style array Create a tiny source file that defines a C‑style array of ten `int` values and fills it with a loop. Run `clang‑tidy` on the file using the repository configuration. The `modernize-avoid-c-arrays` check must produce a diagnostic suggesting the use of `std::array`. The program still compiles and runs, but the warning demonstrates how the committee enforces modern practices. \ No newline at end of file diff --git a/book/src/ch02-values-functions.md b/book/src/ch02-values-functions.md index b485c6b..edc8a79 100644 --- a/book/src/ch02-values-functions.md +++ b/book/src/ch02-values-functions.md @@ -238,10 +238,16 @@ 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`. 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. +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`. + +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. ## 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** + ## Performance considerations When a function returns a large object by value the compiler can apply return value optimization. This eliminates the temporary copy by constructing the result directly in the caller’s storage. Modern compilers can also apply named return value optimization when the return variable is a named local. The generated code frequently reduces memory traffic and improves cache usage. @@ -254,11 +260,6 @@ Do not include unnecessary branches inside hot loops. Branch prediction failures Profile the code with a sampling profiler to locate bottlenecks. Measure the impact of changes rather than assuming improvement. The guidelines in this chapter aim to produce clear, maintainable code, and the performance impact of each choice must be evaluated in the context of the whole program. - - -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** > Never return a reference to a local variable. The local is destroyed when the > function returns, and the caller receives a dangling name. Chapter 1 showed diff --git a/book/src/ch03-user-defined-types.md b/book/src/ch03-user-defined-types.md index 0621e61..33b38c8 100644 --- a/book/src/ch03-user-defined-types.md +++ b/book/src/ch03-user-defined-types.md @@ -41,7 +41,7 @@ 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 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 could 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 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. ## Defaulted comparison and the spaceship diff --git a/book/src/ch04-control-flow.md b/book/src/ch04-control-flow.md index f2956ce..86f43dc 100644 --- a/book/src/ch04-control-flow.md +++ b/book/src/ch04-control-flow.md @@ -8,11 +8,11 @@ C++23 added init‑statements to give the programmer a way to create a temporary 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, enabling a resource to be acquired and then tested for exhaustion. In a range‑based `for` the init‑statement can bind a temporary range object, ensuring 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. +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, providing a clean separation of concerns. +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. @@ -41,7 +41,7 @@ if (auto [ok, n] = try_parse(s); ok) { } ``` -`try_parse` returns a `std::pair` (or a `std::expected`). The binding extracts the success flag `ok` and the parsed value `n`. The subsequent test `ok` decides whether the body runs. Prior to C++26 the language only permitted a *single* simple declaration inside the condition, so a common pattern was a nested `if` pyramid: +`try_parse` returns a `std::pair` (or a `std::expected`). The binding extracts the success flag `ok` and the parsed value `n`. The subsequent test `ok` decides whether the body runs. Before C++26 the language only permitted a *single* simple declaration inside the condition, so a common pattern was a nested `if` pyramid: ```cpp auto result = try_parse(s); @@ -53,7 +53,7 @@ if (result.ok) { The new form collapses that pyramid into one line, reducing visual noise and keeping the "parse-then-use" logic together. It is the modern replacement for the nested-`if` pattern often used with `std::expected` or `std::pair`. -Using structured bindings in a condition also makes error handling more direct. When a function returns a `std::expected`, the success flag and the value can be examined immediately, enabling the error path to be written without an extra temporary variable. This leads to code that reads like a natural language description of the operation, which matches the goal of modern C++ to be expressive and intent-revealing. +Using structured bindings in a condition also makes error handling more direct. When a function returns a `std::expected`, the success flag and the value can be examined immediately. This enables the error path to be written without an extra temporary variable. This leads to code that reads like a natural language description of the operation, which matches the goal of modern C++ to be expressive and intent-revealing. ```cpp {{#include ../../examples/ch04/ch04_sb_condition.cpp}} @@ -80,7 +80,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* is being counted or transformed, not *how* to iterate. +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. ## Ranges-`for` is the only loop you write @@ -108,19 +108,19 @@ The primary advantage of `if constexpr` is that it lets you write generic code t 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, resulting in tighter and faster executables. The source remains clear, as the two alternative implementations are visible side by side, each guarded by a concise predicate. +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. -When combined with concepts, `if constexpr` becomes a powerful 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. +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. -Overall, `if constexpr` brings compile‑time decision making into the same syntactic form as a regular `if`, making the intent of the code immediately apparent while preserving type safety and enabling aggressive optimisation. +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. ## Raw loops are a smell When an algorithm expresses the intent (e.g., *find the first element greater than 10*), writing a manual loop hides that intent. The guideline (ES.70) urges the programmer to replace a hand‑written loop with an appropriate standard algorithm, such as `std::find_if`, `std::count`, or `std::transform`, so that readers instantly recognise the operation. Chapter 12 will enumerate the algorithm zoo and show how each algorithm maps to a common pattern. -Standard algorithms also bring strong exception‑safety guarantees. Because the library implements the iteration and cleanup logic, edge cases such as early exits or thrown exceptions are handled uniformly. This reduces the likelihood of resource leaks compared with manually written loops that must explicitly manage cleanup. Moreover, many algorithms are annotated for vectorisation, enabling the compiler to generate SIMD instructions automatically when the iterator type supports it. The result is often faster code with the same expressive clarity. +Standard algorithms also bring strong exception‑safety guarantees. Because the library implements the iteration and cleanup logic, edge cases such as early exits or thrown exceptions are handled uniformly. This reduces the likelihood of resource leaks compared with manually written loops that must explicitly manage cleanup. Moreover, many algorithms are annotated for vectorisation. The compiler can then generate SIMD instructions automatically when the iterator type supports it. The result is often faster code with the same expressive clarity. -When performance is critical, the programmer can still profile the algorithmic choice. The standard library provides overloads that accept execution policies, enabling parallel execution without changing the high‑level code. This flexibility means that the same source can be tuned for different hardware targets simply by swapping the policy argument. +When performance is critical, the programmer can still profile the algorithmic choice. The standard library provides overloads that accept execution policies. These enable parallel execution without changing the high‑level code. This flexibility means that the same source can be tuned for different hardware targets by swapping the policy argument. Overall, preferring algorithms over hand‑written loops aligns with modern C++ philosophy: write what you want to achieve, let the library handle how to achieve it. diff --git a/book/src/ch06-smart-pointers.md b/book/src/ch06-smart-pointers.md index bc4b9e4..864bb8c 100644 --- a/book/src/ch06-smart-pointers.md +++ b/book/src/ch06-smart-pointers.md @@ -26,7 +26,7 @@ In the tree example the statement `auto root = std::make_unique()` cr ## 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. +`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`: @@ -41,7 +41,7 @@ If the program never needs more than one owner, `unique_ptr` is the safe, zero-o ## 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. +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: @@ -59,14 +59,14 @@ Sometimes a C-language API requires a raw pointer that **owns** the pointed-to o 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. +`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. ## 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; ideal for passing array slices to functions. +* **`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. diff --git a/book/src/ch07-lifetimes.md b/book/src/ch07-lifetimes.md index 081278c..67749f7 100644 --- a/book/src/ch07-lifetimes.md +++ b/book/src/ch07-lifetimes.md @@ -2,17 +2,17 @@ ## 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 it is reclaimed. Three durations cover almost all code. +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. -Automatic objects live inside a block. A local variable and a function parameter are automatic. The object is created when control enters the block and destroyed when control leaves it. The C++ standard ties this to scope. The lifetime of an automatic object is the time the program spends inside its scope. +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. -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. They are created before `main` starts and destroyed after `main` returns. +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. -Dynamic objects reside on the heap. Containers allocate them and destroy them when the container is destroyed. 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. +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 is destroyed 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 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. ## The dangling taxonomy @@ -20,7 +20,7 @@ The Core Guidelines Lifetime profile enumerates four ways a program can keep usi 1. Return of a local reference or pointer. A function returns the address of a variable that goes out of scope when the function returns. The returned handle points at memory the compiler is about to reuse. 2. Use after end of scope or `delete`. Code reaches through a handle after the object's destructor has run, or after `delete` has freed the memory. The handle is live but the value is dead. -3. Views that outlive their source. A `std::string_view`, an iterator, or a raw pointer keeps being used after the object it observes is destroyed or moved. The view borrows storage that no longer holds the expected value. +3. Views that outlive their source. A `std::string_view`, an iterator, or a raw pointer keeps being used after the object it observes dies or moves. The view borrows storage that no longer holds the expected value. 4. Container invalidation. Inserting or erasing elements can trigger reallocation, which moves the underlying storage. Any pointer, reference, or iterator taken before the operation now points at freed or stale memory. These four cases are not arbitrary. They are the only ways a handle can survive its referent, because a handle can only outlive its referent through a return, through a delayed use, through a non-owning view, or through a container resize. The compiler can model each shape because each shape is a small, local pattern in the code. @@ -87,6 +87,8 @@ The attribute also helps constructors. A constructor that stores a pointer or re The guideline support library provides `gsl::not_null`. It wraps a raw pointer and guarantees that the pointer is never null. The wrapper adds a contract that the analysis can check. In practice, references already enforce non-nullness, so the book uses `gsl::not_null` only when a raw pointer is required for legacy interop. The lifetime rules still apply on top of the nullness rule. A `gsl::not_null` that points at a destroyed object is just as dangling as a plain pointer. +The two contracts are independent. `gsl::not_null` answers the question of whether the pointer is null, while the lifetime rules answer the question of whether the referent is still alive. A handle can satisfy both and still dangle, so the wrapper does not remove the need for the lifetime discipline taught in this chapter. + ## Try this Write a function `std::span middle(std::vector& v)` that returns a span over the elements `v[1]` through `v[n-2]`. State the rule that the caller must keep `v` alive for the span's lifetime. Explain what happens if `v` is a temporary, and show the `[[clang::lifetimebound]]` marking that makes the contract explicit. diff --git a/book/src/ch08-argument-passing.md b/book/src/ch08-argument-passing.md index 6b1455e..1b76455 100644 --- a/book/src/ch08-argument-passing.md +++ b/book/src/ch08-argument-passing.md @@ -17,7 +17,7 @@ The rule of thumb is: * **Pass by `const` reference** when the function only needs to observe the argument. * **Pass by non‑`const` reference** only when the function must change the caller’s object. -Contrast this with C. In C every argument is passed by value. To share an object the programmer writes a pointer parameter (`T* p`). The reader must locate `*` to know that the function can observe or mutate shared state. C++ hides that pointer indirection behind references, making the intent visible in the function signature. +Contrast this with C. In C every argument is passed by value. To share an object the programmer writes a pointer parameter (`T* p`). The reader must locate `*` to know that the function can observe or mutate shared state. C++ hides that pointer indirection behind references. This makes the intent visible in the function signature. Size is not the only driver. A type that is expensive or impossible to copy, such as `std::unique_ptr` or a large `std::vector`, must travel by reference or by rvalue move, never by value copy. For a value that the function only inspects, `const T&` is the default even when `T` is small, because it avoids a copy and works for every argument type. Pass by value is the right default only when the function keeps or modifies a copy of what it was given. @@ -62,7 +62,7 @@ The discipline pays off through const overloading. A type can offer both `T& at( The "sink" idiom accepts a parameter by value and immediately moves it into a member or another container. This design gives two useful behaviors: * When the caller supplies an lvalue, the argument is copied into the parameter and then moved into the destination. The copy costs one construction. The move costs no additional allocation. -* When the caller supplies an rvalue (a temporary), the argument is constructed directly in the parameter slot and then moved, resulting in zero copies. +* When the caller supplies an rvalue (a temporary), the argument is constructed directly in the parameter slot and then moved. This results in zero copies. The diagram below shows both paths. @@ -105,7 +105,7 @@ There is one exception that surprises even experienced programmers. Binding a co ## reference_wrapper and views as parameters -Sometimes a function must store a reference in a container or type‑erase the argument. `std::reference_wrapper` wraps a reference in an assignable object, allowing it to appear in `std::vector` or `std::function`. The wrapper forwards operators to the underlying reference. +Sometimes a function must store a reference in a container or type‑erase the argument. `std::reference_wrapper` wraps a reference in an assignable object. This allows it to appear in `std::vector` or `std::function`. The wrapper forwards operators to the underlying reference. A `std::vector` is ill formed, because a reference is not a complete object that a container can own. `std::reference_wrapper` solves this. It is a copyable, assignable object that stores a pointer to the referent and behaves like the reference in almost every context. This is the standard way to keep a collection of aliases into other objects. diff --git a/book/src/ch09-errors-contracts.md b/book/src/ch09-errors-contracts.md index 15d3f2f..fac2ffb 100644 --- a/book/src/ch09-errors-contracts.md +++ b/book/src/ch09-errors-contracts.md @@ -24,7 +24,7 @@ The specifier `noexcept` promises that a function will not throw. If a `noexcep `noexcept` is a contract, not a hint. The compiler uses it to make decisions: a `std::vector` will move elements during reallocation only if the move constructor is `noexcept`, and the optimizer can elide exception-handling machinery around a `noexcept` call. Breaking the contract by throwing from a `noexcept` function calls `std::terminate`, which is a hard stop. The specifier is therefore a promise you make when you can guarantee it, not a wish. -Marking move constructors as `noexcept` enables the standard library to move objects during vector growth without a fallback copy. The optimizer can also inline the call more aggressively. `noexcept` functions can be inlined without hidden control flow, giving the optimizer full visibility of the call graph. +Marking move constructors as `noexcept` enables the standard library to move objects during vector growth without a fallback copy. The optimizer can also inline the call more aggressively. `noexcept` functions can be inlined without hidden control flow. The optimizer gains full visibility of the call graph. A conditional `noexcept(...)` expression makes the promise precise: the function is `noexcept` only when every operation it calls is `noexcept` itself, so the guarantee evolves with the implementation. @@ -48,7 +48,7 @@ Contrast to exceptions: `expected` returns an object that the caller must inspec The demo parses an integer from a string view. On success it prints the value. On error it prints the error string. -`std::expected` composes. A function that calls another fallible function can pass the error upward with a single expression, using `.and_then` to chain success and `.or_else` to handle failure. This keeps the error path linear and avoids the nested `if`/`else` pyramids that error-code APIs produce. The type carries both the value and the error, so the compiler tracks whether the result has been checked. +`std::expected` composes. A function that calls another fallible function can pass the error upward with a single expression. `.and_then` chains success and `.or_else` handles failure. This keeps the error path linear and avoids the nested `if`/`else` pyramids that error-code APIs produce. The type carries both the value and the error, so the compiler tracks whether the result has been checked. The monadic interface is what makes `expected` scale beyond two calls. Without it, a function that calls three fallible operations in a row needs three nested `if` statements, each checking and forwarding. With `.and_then`, the same logic is a single chained expression that reads left to right. The error short-circuits: the first failure skips the rest of the chain and returns the error to the caller. This is the same shape as Rust's `?` operator, expressed as method calls. @@ -85,7 +85,7 @@ The violation handler decides what happens when a contract breaks. The implement ### Historical perspective on error handling -Early C programs used integer return codes and `errno` as the sole mechanism for reporting failure. The programmer had to check each call manually, and forgetting a check produced silent corruption. C++98 introduced exceptions as a language feature that separates error propagation from the normal control flow. The first implementations used a table‑based “zero‑cost” model that stored unwind information in the object file. Compilers later added the ability to mark functions `noexcept`, allowing the optimizer to elide unwind handling for guaranteed‑non‑throwing code. C++23 added `std::expected` as a standard library type that returns either a value or an error object, making failure an explicit part of the type system. C++26 formalizes contracts, completing the error‑handling toolbox. +Early C programs used integer return codes and `errno` as the sole mechanism for reporting failure. The programmer had to check each call manually, and forgetting a check produced silent corruption. C++98 introduced exceptions as a language feature that separates error propagation from the normal control flow. The first implementations used a table‑based “zero‑cost” model that stored unwind information in the object file. Compilers later added the ability to mark functions `noexcept`. This ability let the optimizer elide unwind handling for guaranteed‑non‑throwing code. C++23 added `std::expected` as a standard library type that returns either a value or an error object. Failure became an explicit part of the type system. C++26 formalizes contracts, completing the error‑handling toolbox. ## Choosing a strategy diff --git a/book/src/ch12-algorithms.md b/book/src/ch12-algorithms.md index 8cb42d7..b6f05be 100644 --- a/book/src/ch12-algorithms.md +++ b/book/src/ch12-algorithms.md @@ -108,6 +108,11 @@ Many tasks map to a composition of standard algorithms. The filter-map-reduce pa Many algorithms accept an execution policy as the first argument. `std::execution::par` asks the implementation to run the work in parallel when it can. Support for parallel policies is optional in the standard and depends on the compiler and its runtime backend. On some toolchains `std::execution::par` is unavailable or falls back to sequential execution. Treat parallel overloads as a performance option, not a correctness feature. When you use them, remember that order-unstable algorithms can reorder equal elements, so tests must check value-level properties such as sums rather than exact sequence. +```cpp +std::vector v = {3, 1, 4, 1, 5}; +std::sort(std::execution::par, v.begin(), v.end()); +``` + ## Common pitfalls * **Binary search on an unsorted range** yields undefined behaviour. Sort first. diff --git a/book/src/ch13-ranges.md b/book/src/ch13-ranges.md index 6b8b830..915414b 100644 --- a/book/src/ch13-ranges.md +++ b/book/src/ch13-ranges.md @@ -3,7 +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, creating a view 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. +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. @@ -69,7 +69,7 @@ The program prints `size = 5`, confirming that the view was copied into a contai ## Projections everywhere Most range adaptors and algorithms accept a *projection*, a callable that extracts a member from each element. Projections let you avoid writing a lambda for a common operation. The syntax `&T::member` is a pointer-to-member that the algorithm treats as a projection. -Projections improve compile-time readability and often enable better inlining because the compiler sees a direct member access instead of a generic lambda capture. They also integrate with concepts that require a `std::indirectly_readable` predicate, allowing the standard library to reason about the operation without executing user code. +Projections improve compile-time readability and often enable better inlining because the compiler sees a direct member access instead of a generic lambda capture. They also integrate with concepts that require a `std::indirectly_readable` predicate. This lets the standard library reason about the operation without executing user code. The example below defines a simple `Point` struct with members `x` and `y`. A `std::vector` is sorted by the `x` coordinate using the projection `&Point::x`. The program then prints the sorted `x` values. @@ -84,7 +84,7 @@ Beyond sorting, any algorithm that accepts a projection can use the member-point 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. ## `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, making the intent clear and the code compact. +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. The pre-ranges algorithmic style often required explicit iterator arguments, temporary containers, and separate calls to `std::find` or `std::count`. The range version can operate on a filtered view without materialising the intermediate sequence. diff --git a/book/src/ch14-callables-type-erasure.md b/book/src/ch14-callables-type-erasure.md index 68e35ab..fb1ca4d 100644 --- a/book/src/ch14-callables-type-erasure.md +++ b/book/src/ch14-callables-type-erasure.md @@ -23,10 +23,9 @@ 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 +### 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. -### Performance considerations 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. ## `std::function` and type erasure diff --git a/book/src/ch17-concepts.md b/book/src/ch17-concepts.md index d85172d..2350790 100644 --- a/book/src/ch17-concepts.md +++ b/book/src/ch17-concepts.md @@ -1,7 +1,7 @@ # Concepts: the constraint language ## 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, making the error clear and the intent obvious. +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. @@ -46,7 +46,7 @@ A concept is a named predicate. It can be written with the `concept` keyword and ``` The `Addable` concept checks that the binary `+` operator is valid for two operands of type `T`. The accompanying `add` function uses the concept as a constraint, so any type that models `Addable` can be added safely. Because the concept name appears directly in the signature, the intent is clear at the call site. -Concepts can also be expressed as Boolean formulas of other concepts, enabling a small vocabulary of high‑level requirements while reusing primitive concepts from ``: +Concepts can also be expressed as Boolean formulas of other concepts. This enables a small vocabulary of high‑level requirements while reusing primitive concepts from ``: ```cpp template concept Number = std::integral || std::floating_point; @@ -76,7 +76,7 @@ The same pattern works for references, forwarding references, and ranges. The ex ```cpp {{#include ../../examples/ch17/terse_syntax.cpp}} ``` -These terse declarations keep the constraint next to the entity it describes, improving readability. Because the constraint travels with the parameter, a constrained lambda passed to `std::ranges::sort` is checked the moment the call is written, not when the algorithm instantiates. They also work inside generic lambdas, enabling concise constraint specifications: +These terse declarations keep the constraint next to the entity it describes, improving readability. Because the constraint travels with the parameter, a constrained lambda passed to `std::ranges::sort` is checked the moment the call is written, not when the algorithm instantiates. They also work inside generic lambdas. This enables concise constraint specifications: ```cpp auto cmp = [] (std::totally_ordered auto const& a, std::totally_ordered auto const& b) { return a < b; @@ -125,7 +125,7 @@ produces a diagnostic similar to: > note: substitution failed for 'T = char const*' The message shows that `std::integral` was the offending concept and that `char const*` does not satisfy it. This is far clearer than the cascades of template‑instantiation errors that appeared before concepts. -When user‑defined concepts are involved, the diagnostic includes the failed sub‑requirement, making it possible to pinpoint exactly which operation is missing. For instance, if `Addable` were violated because `+` returned the wrong type, the compiler points to the `{ a + b } -> std::same_as` clause. +When user‑defined concepts are involved, the diagnostic includes the failed sub‑requirement. This makes it possible to pinpoint exactly which operation is missing. For instance, if `Addable` were violated because `+` returned the wrong type, the compiler points to the `{ a + b } -> std::same_as` clause. ## Try this Define a concept `Comparable` that requires both `a < b` and `a == b` to be valid. Then write a `min` function constrained by `Comparable`. Call the function with two `int` values and with two `std::string` values. diff --git a/book/src/ch19-nttp.md b/book/src/ch19-nttp.md index 8ed8819..23eaf6b 100644 --- a/book/src/ch19-nttp.md +++ b/book/src/ch19-nttp.md @@ -17,14 +17,14 @@ Before C++26, a floating‑point NTTP required a workaround: encode the value as ``` -Compile‑time hashing of string literals also relies on NTTPs. A hash function defined as `constexpr` can accept a `fixed_string` NTTP and produce a constant hash value. This value can be used as a case label in a `switch` statement, providing a zero‑overhead dispatch table for command strings. +Compile‑time hashing of string literals also relies on NTTPs. A hash function defined as `constexpr` can accept a `fixed_string` NTTP and produce a constant hash value. This value can be used as a case label in a `switch` statement. The result is a zero‑overhead dispatch table for command strings. An NTTP argument must come from one of a fixed set of categories: * Integral and enumeration constants (`int`, `char`, `enum`). * Pointers or references to objects with static storage duration, including function pointers. * Class‑type values that satisfy the *structural type* requirements. -* `auto` can deduce the type, allowing `template` to accept any of the above categories. +* `auto` can deduce the type. This allows `template` to accept any of the above categories. @@ -80,7 +80,7 @@ A UDL has a form for each literal category. Integer literals dispatch to `operat ## UDLs for parsed literals (constexpr) -A more advanced use of UDLs parses the literal characters at compile time, turning a string literal into a value without any runtime work. The function must be `consteval` (or `constexpr` in C++20) and can perform arbitrary compile‑time computation. +A more advanced use of UDLs parses the literal characters at compile time. It turns a string literal into a value without any runtime work. The function must be `consteval` (or `constexpr` in C++20) and can perform arbitrary compile‑time computation. ```cpp {{#include ../../examples/ch19/udl_parse.cpp}} diff --git a/book/src/ch21-legacy-tmp.md b/book/src/ch21-legacy-tmp.md index a14c206..c494787 100644 --- a/book/src/ch21-legacy-tmp.md +++ b/book/src/ch21-legacy-tmp.md @@ -54,7 +54,7 @@ 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, allowing the overload set to fall back to a generic implementation. +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. @@ -126,7 +126,7 @@ Both implementations produce identical runtime output for the test cases. The ta ## CRTP (Curiously Recurring Template Pattern) -CRTP is a form of static polymorphism where a base class template takes the derived class as a parameter. The base can call functions that the derived implements, using `static_cast`. This pattern appears in many older libraries (e.g., Boost.Fusion, Eigen) and remains useful for compile-time interfaces. +CRTP is a form of static polymorphism where a base class template takes the derived class as a parameter. The base can call functions that the derived implements. It uses `static_cast` to do so. This pattern appears in many older libraries (e.g., Boost.Fusion, Eigen) and remains useful for compile-time interfaces. ```cpp {{#include ../../examples/ch21/ch21_crtp_shape.cpp}} @@ -144,7 +144,7 @@ double area(const S& s) { return s.area_impl(): } The concept-based version provides the same static guarantee (`area_impl` exists) without the boilerplate of a CRTP base class. -The CRTP also lets a base provide default implementations that call into the derived type, which is how many mixin-style helpers share code without virtual dispatch. Because the call is resolved at compile time, it is inlined, giving the performance of hand-written code with the reuse of a shared base. +The CRTP also lets a base provide default implementations that call into the derived type, which is how many mixin-style helpers share code without virtual dispatch. Because the call is resolved at compile time, it is inlined. This gives the performance of hand-written code with the reuse of a shared base. --- @@ -204,6 +204,6 @@ When converting existing code, adopt a staged approach: 4. Run the test suite to confirm behaviour remains identical. 5. Refactor additional legacy patterns (type-trait checks, tag dispatch, CRTP) following the same mechanical mapping. -By iterating through the table above, you systematically reduce technical debt, modernise the codebase, and equip future contributors with clearer abstractions. The chapter’s examples demonstrate each step, ensuring you have concrete, compile-tested references for every transformation. +By iterating through the table above, you systematically reduce technical debt, modernise the codebase, and equip future contributors with clearer abstractions. The chapter’s examples demonstrate each step. They give you concrete, compile-tested references for every transformation. *The examples in this chapter are compiled and tested with the `book_example` macro. The `EXPECT` strings in the CMake registration verify that each program prints the expected identifiers.* diff --git a/book/src/ch24-modules.md b/book/src/ch24-modules.md index 8c59bfd..add6cdb 100644 --- a/book/src/ch24-modules.md +++ b/book/src/ch24-modules.md @@ -2,9 +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, making 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. 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. -Modules replace this textual inclusion with a compiled interface. A module’s interface is built once, producing 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. +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. ## import std @@ -86,13 +86,13 @@ A CMake target that represents a module interface is created with the source fil When the interface source changes, CMake rebuilds only the BMI and any dependents that import the module. The implementation unit is compiled as a static library that provides the definitions for the exported symbols. The static library is linked into any executable or library that imports the module. Because the interface and implementation are separate, developers can modify the implementation without triggering a rebuild of the interface, further reducing incremental build time. -CMake also propagates include directories from the module target to its dependents, so that headers used inside the module are found without additional `target_include_directories` calls. This behavior mirrors the way the standard library module is handled by the compiler. +CMake also propagates include directories from the module target to its dependents, so that headers used inside the module are found without additional `target_include_directories` calls. This behavior mirrors the way the compiler handles the standard library module. The book marks these examples as gaps, but the same CMake patterns work unchanged on a compiler that implements modules. The author encourages readers to try the conversion on a supporting toolchain and compare build metrics. -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, making 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. +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 diff --git a/book/src/ch26-concurrency-atomics.md b/book/src/ch26-concurrency-atomics.md index 640708d..a028b19 100644 --- a/book/src/ch26-concurrency-atomics.md +++ b/book/src/ch26-concurrency-atomics.md @@ -1,7 +1,9 @@ # 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 same rule applies to a mutex: the lock operation acts as an acquire, the unlock operation acts as a release, therefore a mutex also establishes a happens‑before edge. 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 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 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. ### 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. diff --git a/book/src/ch27-coroutines.md b/book/src/ch27-coroutines.md index 9bfd880..6a99668 100644 --- a/book/src/ch27-coroutines.md +++ b/book/src/ch27-coroutines.md @@ -2,7 +2,7 @@ ## 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 simply 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 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. 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. @@ -64,7 +64,7 @@ Another practical scenario is streaming data from a file or network socket. A co ## `co_await` a value -The `co_await` operator can be applied to an ordinary value when an awaiter is provided that simply returns the value after suspension. The awaiter is what makes `co_await` compile, so the operator always pairs with a suspension mechanism. The snippet below shows a generator that awaits a helper coroutine `compute` before yielding the result. The helper returns `int` after a dummy delay. The awaiting generator resumes once the delay completes and yields the computed integer. +The `co_await` operator can be applied to an ordinary value when an awaiter is provided that returns the value after suspension. The awaiter is what makes `co_await` compile, so the operator always pairs with a suspension mechanism. The snippet below shows a generator that awaits a helper coroutine `compute` before yielding the result. The helper returns `int` after a dummy delay. The awaiting generator resumes once the delay completes and yields the computed integer. ```cpp std::generator delayed() {