Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/chapter-structure.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
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
176 changes: 164 additions & 12 deletions 01_low_level/13_ub/README.md
Original file line number Diff line number Diff line change
@@ -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++.
Expand All @@ -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
Expand All @@ -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 |
|------------------------|---------------------------------|
Expand All @@ -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) {
Expand All @@ -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`
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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 makes the C-style `ckd_add`, `ckd_sub`, and `ckd_mul` function templates available through `<stdckdint.h>` 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.

---

## 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<double*>(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<int> 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<bool> 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 an unchecked promise

The C++23 function `std::unreachable()` from `<utility>` 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 <utility> // C++23 std::unreachable

// Only for a genuinely proven impossible state:
// if (invariant_holds) ...; else std::unreachable();
```

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

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.
Expand Down Expand Up @@ -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.

Expand Down
75 changes: 75 additions & 0 deletions scripts/check_chapter_structure.py
Original file line number Diff line number Diff line change
@@ -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|<placeholder>", 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())
Loading