From e26fa056e93dca28bb6ed4fbd689ceeeff1e8c53 Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 00:57:25 +0700 Subject: [PATCH 1/6] docs: expand undefined-behavior chapter and correct guarded-null optimization misconception --- 01_low_level/13_ub/README.md | 176 ++++++++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 12 deletions(-) diff --git a/01_low_level/13_ub/README.md b/01_low_level/13_ub/README.md index dd1300d..10ca4e0 100644 --- a/01_low_level/13_ub/README.md +++ b/01_low_level/13_ub/README.md @@ -1,4 +1,4 @@ -# 13. Undefined Behavior in C and C++ +# 13. Undefined Behavior and Its Evolution from C++98 to C++26 This chapter is dedicated to Undefined Behavior (UB) - one of the most misunderstood, dangerous, and performance‑critical aspects of C and C++. @@ -14,7 +14,7 @@ That includes: --- -## Why UB exists +## 1. Why UB exists UB is not a compiler bug. It exists to: - allow aggressive optimizations @@ -24,7 +24,7 @@ UB is not a compiler bug. It exists to: --- -## UB vs. Unspecified vs. Implementation‑Defined +## 2. UB vs. Unspecified vs. Implementation‑Defined | Category | Meaning | |------------------------|---------------------------------| @@ -34,9 +34,9 @@ UB is not a compiler bug. It exists to: --- -## How compilers exploit UB +## 3. How compilers exploit UB -If the compiler proves UB *could* happen, it may assume it never happens. +For a path with undefined behavior, the optimizer may reason under the assumption that valid executions do not take that path. A speculative possibility of UB elsewhere is not enough to invalidate a correctly guarded access. ```cpp int f(int* p) { @@ -45,11 +45,11 @@ int f(int* p) { } ``` -If `p` is ever null → UB → compiler may remove the check entirely. +The null check makes this example **defined** when `p` is null; the dereference is reached only when the pointer is non-null. Non-null alone is not sufficient for safe dereference: the pointer must still designate a live, accessible object. --- -## How to fight UB +## 4. How to fight UB - Compile with sanitizers: `-fsanitize=undefined,address` - Enable warnings: `-Wall -Wextra -Wpedantic` @@ -58,18 +58,18 @@ If `p` is ever null → UB → compiler may remove the check entirely. --- -## Files +## 5. Files * `ub_examples.cpp` - isolated UB examples with detailed comments --- -## Rule +## 6. Rule > If your program has UB, the compiler owes you nothing. -## UB cases explained (based on the original educational snippet) +## 7. UB cases explained (based on the original educational snippet) Below is a short explanation for each UB pattern from your original file. Rule of thumb: the moment UB occurs, the whole program becomes unconstrained - even code "after" the UB may be optimized away. @@ -159,7 +159,7 @@ Fix: return on all paths; enable `-Wreturn-type`. --- -## Practical way to study UB +## 8. Practical way to study UB Run one case at a time with sanitizers: ```bash @@ -171,6 +171,158 @@ Sanitizers do not catch *all* UB (e.g., some strict-aliasing miscompilations), b --- +## 9. Why optimizer reasoning about undefined behavior is more subtle than a crash + +Undefined behavior means that a particular execution has no requirements imposed by the C++ abstract-machine rules. It does not mean that a compiler can remove every guard merely because a dangerous operation appears somewhere in the function. The key distinction is whether the bad operation is reachable along the actual execution path and whether the compiler can rely on a violated precondition. + +### A prior check of a pointer can be entirely valid + +A null check *before* dereferencing a pointer is a perfectly legitimate defensive pattern. It is wrong to claim that a compiler may arbitrarily remove such a check because the program would have undefined behavior *if the pointer were null*. + +```cpp +int value_or_zero(const int* p) { + if (p == nullptr) return 0; + return *p; // only reached for non-null p +} +``` + +A null pointer is not dereferenced on the null path. Optimization may simplify code under additional documented assumptions, but ordinary C++ semantics require the branch to protect that access. By contrast, a check *after an unconditional dereference* can be optimized away as redundant on defined executions. + +### Signed overflow lets optimization reject impossible paths + +For ordinary signed integer addition, overflow is undefined behavior. A compiler may optimize on the premise that a defined program does not overflow, invalidating intuitive attempts to detect overflow by examining a wrapped result. + +```cpp +bool greater_after_increment(int x) { + return x + 1 > x; // true for every defined evaluation +} +``` + +To test overflow, use precondition checks before adding, a wider type where available, or a checked-arithmetic facility. C++26's `std::ckd_add`, `std::ckd_sub`, and `std::ckd_mul` provide standardized checked integer operations where supported. Do not silently assume that a compiler option such as `-fwrapv` is a cross-platform language rule. + +--- + +## 10. Object lifetimes and aliasing create UB without crossing a buffer boundary + +Many low-level errors produce no out-of-bounds address at all. They occur because an object does not exist at the accessed location, because the glvalue type is not allowed to access the object, or because the program incorrectly assumes a memory representation proves a live object lifetime. + +### `reinterpret_cast` does not make the target object exist + +Casting an address to `T*` is only a pointer conversion; it neither allocates suitably aligned storage nor starts an arbitrary object's lifetime. This matters when parsing packets or implementing custom allocators. + +```cpp +alignas(double) unsigned char bytes[sizeof(double)]{}; +auto* p = reinterpret_cast(bytes); +// Dereferencing p is not a general object-creation mechanism. +``` + +Use actual construction, `std::bit_cast` for equal-size trivially copyable representation conversion, or applicable C++20 implicit-lifetime operations and C++23 `std::start_lifetime_as` with their stated preconditions. + +### The pointer can keep the same address after the object dies + +Destroying an object ends its lifetime even if the backing allocation remains valid. A stale pointer does not revive the old object; container erasure, vector reallocation, and placement-new storage reuse require careful reassessment of pointer validity. + +```cpp +std::vector values{1, 2}; +int* p = values.data(); +values.push_back(3); // may reallocate: p may now dangle +``` + +Reserve policies, owning containers, and stable identifiers can prevent some invalidation bugs; an address equality check alone cannot prove object identity after reuse. + +--- + +## 11. Concurrency adds undefined behavior that sanitizers only approximate + +A C++ data race arises when conflicting evaluations access the same memory location in different threads without a happens-before relationship and at least one is not atomic, subject to the memory-model rules. The program then has undefined behavior, even if the machine appears to perform indivisible aligned writes. + +### Volatile is not a portable thread-synchronization mechanism + +`volatile` affects certain evaluations of volatile-qualified objects; it does not provide atomic read-modify-write operations, release/acquire synchronization, or a happens-before edge. + +```cpp +std::atomic stop{false}; +// Thread A: stop.store(true, std::memory_order_release); +// Thread B: while (!stop.load(std::memory_order_acquire)) { } +``` + +The exact memory order may be relaxed for a standalone stop flag depending on the larger protocol. The fundamental requirement is to model the shared state with atomics, mutexes, or another valid synchronization mechanism. + +### Atomic pointer publication is not automatic memory reclamation + +An atomic pointer can publish an object to another thread, but a reader can still race with deletion after loading the pointer. Hazard pointers, epoch reclamation, read-copy-update designs, and locks solve different parts of the lifetime protocol. C++26 standard hazard-pointer and RCU library facilities increase the vocabulary, but do not retroactively make raw atomic pointers safe. + +### ThreadSanitizer findings and limitations + +ThreadSanitizer instruments executed accesses and can expose many unprotected data races. It is not a proof of race freedom and may require a specialized runtime, supported platform, and avoidance of unsupported synchronization mechanisms. Pair it with stress tests, invariants, and a clear ownership model. + +--- + +## 12. Compile-time evaluation and unreachable contracts change the location of failures + +C++20 `consteval` and later constant-evaluation changes make some invalid operations diagnosable at compile time when they are actually evaluated during constant evaluation. This is different from runtime sanitization; not every potential runtime path is evaluated by the compiler. + +### `std::unreachable` in C++23 is a dangerous assertion about control flow + +The standard `std::unreachable()` tells the compiler that reaching this point has undefined behavior. Its purpose is to give optimizers a strong assumption, not to implement a safe fallback for unknown enum states or corrupt input. + +```cpp +#include // for other modern utilities +// #include is NOT the home of std::unreachable; +// include for std::to_underlying, notwithstanding. +// C++23: #include is not sufficient; use only for its own APIs. +``` + +Do not use `std::unreachable` to handle untrusted input or an incomplete switch. Instead throw, return an error, or terminate deliberately according to the API. In C++23 its header is ``? No: use the documented ``/implementation reference for target library rather than guessing from legacy builtins. + +### Contracts introduced by C++26 are not permission to ignore preconditions + +C++26 contracts add language mechanisms for expressing preconditions and postconditions, with behavior depending on contract semantics and the selected evaluation mode. The feature does not abolish undefined behavior inside unchecked operations, and a contract annotation cannot make a dangling pointer valid. + +Keep contract checks at trust boundaries, but specify how release builds and consumers should behave when input validation fails. Do not confuse an optimizer assumption with a recoverable validation failure. + +--- + +## 13. C++26 uninitialized values distinguish two classes of problematic evaluation + +Historically, explanations of uninitialized automatic objects focused on indeterminate values. C++26 introduces additional terminology for *erroneous values*, changing how some uninitialized evaluations are classified without authorizing their ordinary use. + +### Reading an unwritten ordinary `int` is still not a valid data source + +```cpp +int x; // no initialization +// int y = x; // do not do this +x = 10; +int y = x; // defined +``` + +The distinction between undefined and erroneous behavior matters in standard wording and tool reporting. For ordinary application programming, the practical rule is unchanged: initialize values or prove that every execution writes them before use. Byte-oriented types have narrowly specified uninitialized-friendly operations, but those exceptions are not permission to use arbitrary uninitialized data. + +### Runtime checks only cover the paths they observe + +AddressSanitizer catches many heap/stack/global bounds and lifetime defects. UndefinedBehaviorSanitizer instruments selected undefined operations, and MemorySanitizer detects uses of uninitialized data on supported toolchains. A single run with no report cannot establish that all source paths are defined. + +When studying intentionally invalid snippets, isolate one scenario per executable and invoke it only in a controlled sanitizing test environment. Do not run all UB demonstrations in one binary and expect deterministic results after the first invalid operation. + +--- + +## 14. Portability, numerical assumptions, and diagnostic strategy + +Some behavior is implementation-defined or unspecified rather than undefined. Confusing those categories leads to false bug reports and weak tests. Standard C++ programs may depend on documented implementation choices, but must not accidentally rely on a choice being universal. + +### Floating-point arithmetic is not interchangeable with signed integer overflow + +IEEE-style floating overflow often produces infinity when the implementation supports it; signed integer overflow is undefined by core language rules. Floating-point precision, NaNs, trap settings, and fast-math compiler modes are separate concerns and should be tested according to the actual numerical contract. + +### Compiler flags can mask bugs while preserving non-portability + +`-fno-strict-aliasing` alters some optimization assumptions but does not start object lifetimes or fix alignment. `-fwrapv` on supported compilers supplies a particular signed-overflow interpretation, but the ISO C++ rule remains different. Building with `-O0` can change symptoms while leaving the invalid source unchanged. + +For senior-level debugging, compile the smallest reproducer under at least two optimization levels and two compilers where practical. Treat sanitizer results, generated assembly, and the abstract-machine rules as complementary evidence—not as interchangeable authorities. + +--- + + ## Rules worth keeping in working memory The controlling distinction for undefined behavior is between the portable language contract and the behavior of a particular compilation environment. C++ gives programmers the ability to build close to system boundaries, but also requires those boundaries to be specified rather than guessed. @@ -308,7 +460,7 @@ Sequencing rules were strengthened for several expressions. Further constexpr/lifetime clarifications refine diagnostics. -### C++26: Forthcoming support +### C++26: Checked arithmetic, contracts, and erroneous values Safety facilities evolve, but UB does not universally become a runtime exception. From 28422658d9b5b971ecf40c0b4485ffcd09fdaf04 Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 00:58:26 +0700 Subject: [PATCH 2/6] test: add reusable Python chapter structure checker --- scripts/check_chapter_structure.py | 75 ++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 scripts/check_chapter_structure.py diff --git a/scripts/check_chapter_structure.py b/scripts/check_chapter_structure.py new file mode 100644 index 0000000..bb89f76 --- /dev/null +++ b/scripts/check_chapter_structure.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Validate Book 01 technical-literature chapter contracts (stdlib only).""" +from __future__ import annotations + +import argparse +from pathlib import Path +import re +import sys + +ENDING = ( + "Rules worth keeping in working memory", + "Diagnostics, useful compiler settings and extensions", + "Standards timeline", + "Migration note for Java / Python / C# developers", + "Further reading", +) +YEARS = ("C++98/03", "C++11", "C++14", "C++17", "C++20", "C++23", "C++26") +STUBS = re.compile(r"\b(?:TODO|TBD|FIXME|PLACEHOLDER|fill this in)\b|", re.I) +HEADING = re.compile(r"^(#{1,6})\s+(.+?)\s*$", re.M) +FENCE = re.compile(r"^\s*(`{3,}|~{3,})", re.M) +CHAPTER = re.compile(r"^(\d{2})_") + +def validate(path: Path) -> list[str]: + body = path.read_text(encoding="utf-8") + errors: list[str] = [] + chapter = CHAPTER.match(path.parent.name) + if not chapter: + return [f"{path}: not a numbered chapter directory"] + headings = [(len(m.group(1)), m.group(2)) for m in HEADING.finditer(body)] + if not headings or headings[0][0] != 1 or not headings[0][1].startswith(chapter.group(1)): + errors.append("H1 must start with chapter number") + h2 = [h for level, h in headings if level == 2] + if h2[-len(ENDING):] != list(ENDING): + errors.append("final five H2 sections must match the chapter template in order") + for h in ENDING: + if h2.count(h) != 1: + errors.append(f"expected exactly one H2: {h}") + numeric = [int(m.group(1)) for h in h2 if (m := re.match(r"^(\d+)\.\s", h))] + if not numeric or numeric != list(range(1, len(numeric) + 1)): + errors.append(f"numbered H2 sections not consecutive: {numeric}") + found_stubs = STUBS.findall(body) + if found_stubs: + errors.append(f"contains stub markers: {found_stubs[:3]}") + fences = FENCE.findall(body) + if len(fences) % 2: + errors.append("unbalanced fenced code blocks") + timeline_start = body.find("\n## Standards timeline") + migration_start = body.find("\n## Migration note") + if timeline_start >= 0 and migration_start > timeline_start: + timeline = body[timeline_start:migration_start] + missing = [std for std in YEARS if not re.search(r"^### " + re.escape(std) + r"\b", timeline, re.M)] + if missing: + errors.append(f"standards timeline missing: {', '.join(missing)}") + if len(body) < 7000: + errors.append("chapter is unexpectedly short for advanced technical literature") + for item in errors: + print(f"{path}: {item}", file=sys.stderr) + if not errors: + print(f"PASS {path} ({len(numeric)} numbered sections)") + return errors + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("paths", nargs="*", type=Path, + help="README.md paths; defaults to chapters 01–13") + args = ap.parse_args() + paths = args.paths or sorted(Path("01_low_level").glob("[0-9][0-9]_*/README.md")) + if not paths: + print("No chapter READMEs found", file=sys.stderr) + return 2 + failures = sum(bool(validate(p)) for p in paths) + return 1 if failures else 0 + +if __name__ == "__main__": + raise SystemExit(main()) From 438602633bc024d70c3f537df1b626603b9d2076 Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 00:58:28 +0700 Subject: [PATCH 3/6] ci: check modified chapter markdown --- .github/workflows/chapter-structure.yml | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/chapter-structure.yml diff --git a/.github/workflows/chapter-structure.yml b/.github/workflows/chapter-structure.yml new file mode 100644 index 0000000..f0b0042 --- /dev/null +++ b/.github/workflows/chapter-structure.yml @@ -0,0 +1,37 @@ +name: Chapter Markdown Structure +on: + pull_request: + paths: + - '01_low_level/**/README.md' + - 'scripts/check_chapter_structure.py' + - '.github/workflows/chapter-structure.yml' + push: + branches: [master] + paths: + - '01_low_level/**/README.md' + - 'scripts/check_chapter_structure.py' + - '.github/workflows/chapter-structure.yml' + workflow_dispatch: +jobs: + chapter-contract: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Validate Book 01 chapter structure + shell: bash + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + mapfile -t files < <(git diff --name-only --diff-filter=ACMR "${{ github.event.pull_request.base.sha }}" HEAD -- '01_low_level/*/README.md') + if (( ${#files[@]} )); then + python scripts/check_chapter_structure.py "${files[@]}" + else + echo "No changed chapter READMEs to validate" + fi + else + python scripts/check_chapter_structure.py + fi From c5536d23198bd39e1b9f468e11d2d8dd62922adb Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 00:59:10 +0700 Subject: [PATCH 4/6] docs: correct advanced example and source references --- 01_low_level/13_ub/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/01_low_level/13_ub/README.md b/01_low_level/13_ub/README.md index 10ca4e0..6338bba 100644 --- a/01_low_level/13_ub/README.md +++ b/01_low_level/13_ub/README.md @@ -262,18 +262,18 @@ ThreadSanitizer instruments executed accesses and can expose many unprotected da C++20 `consteval` and later constant-evaluation changes make some invalid operations diagnosable at compile time when they are actually evaluated during constant evaluation. This is different from runtime sanitization; not every potential runtime path is evaluated by the compiler. -### `std::unreachable` in C++23 is a dangerous assertion about control flow +### `std::unreachable` in C++23 is an unchecked promise -The standard `std::unreachable()` tells the compiler that reaching this point has undefined behavior. Its purpose is to give optimizers a strong assumption, not to implement a safe fallback for unknown enum states or corrupt input. +The C++23 function `std::unreachable()` from `` informs the optimizer that execution cannot reach that point in a well-defined program. Reaching it causes undefined behavior: it is not a recoverable runtime assertion. ```cpp -#include // for other modern utilities -// #include is NOT the home of std::unreachable; -// include for std::to_underlying, notwithstanding. -// C++23: #include is not sufficient; use only for its own APIs. +#include // C++23 std::unreachable + +// Only for a genuinely proven impossible state: +// if (invariant_holds) ...; else std::unreachable(); ``` -Do not use `std::unreachable` to handle untrusted input or an incomplete switch. Instead throw, return an error, or terminate deliberately according to the API. In C++23 its header is ``? No: use the documented ``/implementation reference for target library rather than guessing from legacy builtins. +Do not use it for untrusted input or as the fallback of a switch whose enum can hold unnamed values. Validate or report errors instead. An invalid optimizer assumption can be more damaging than a deliberate failure. ### Contracts introduced by C++26 are not permission to ignore preconditions From 56fc0071527c3c32c79a0d69cd0b0fac03ab75ab Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 01:00:21 +0700 Subject: [PATCH 5/6] docs: refine standard guarantees and subsection hierarchy --- 01_low_level/13_ub/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/01_low_level/13_ub/README.md b/01_low_level/13_ub/README.md index 6338bba..ef76114 100644 --- a/01_low_level/13_ub/README.md +++ b/01_low_level/13_ub/README.md @@ -198,7 +198,7 @@ bool greater_after_increment(int x) { } ``` -To test overflow, use precondition checks before adding, a wider type where available, or a checked-arithmetic facility. C++26's `std::ckd_add`, `std::ckd_sub`, and `std::ckd_mul` provide standardized checked integer operations where supported. Do not silently assume that a compiler option such as `-fwrapv` is a cross-platform language rule. +To test overflow, use precondition checks before adding, a wider type where available, or a checked-arithmetic facility. C++26 makes the C-style `ckd_add`, `ckd_sub`, and `ckd_mul` function templates available through `` where supported. Their names are supplied in the global namespace; whether corresponding `std::` declarations exist is unspecified. They report whether the mathematically exact result was representable. Do not silently assume that a compiler option such as `-fwrapv` is a cross-platform language rule. --- From cdf66c93e1c4f6ba30e9de7ab641a1fefc7882aa Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 01:03:57 +0700 Subject: [PATCH 6/6] ci: validate only changed chapters on push and PR; keep manual full-book audit --- .github/workflows/chapter-structure.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/chapter-structure.yml b/.github/workflows/chapter-structure.yml index f0b0042..f2afa8e 100644 --- a/.github/workflows/chapter-structure.yml +++ b/.github/workflows/chapter-structure.yml @@ -26,12 +26,16 @@ jobs: shell: bash run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then - mapfile -t files < <(git diff --name-only --diff-filter=ACMR "${{ github.event.pull_request.base.sha }}" HEAD -- '01_low_level/*/README.md') - if (( ${#files[@]} )); then - python scripts/check_chapter_structure.py "${files[@]}" - else - echo "No changed chapter READMEs to validate" - fi + base="${{ github.event.pull_request.base.sha }}" + elif [[ "${{ github.event_name }}" == "push" ]]; then + base="${{ github.event.before }}" else python scripts/check_chapter_structure.py + exit + fi + mapfile -t files < <(git diff --name-only --diff-filter=ACMR "$base" HEAD -- '01_low_level/*/README.md') + if (( ${#files[@]} )); then + python scripts/check_chapter_structure.py "${files[@]}" + else + echo "No changed chapter READMEs to validate" fi