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
114 changes: 99 additions & 15 deletions 01_low_level/09_functions/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 09. Functions and their Evolution in Modern C++
# 09. Functions from C++98 to C++26: Types, Calls, ABI, and Callable Objects

> Functions are the fundamental abstraction boundary in C++.
> They combine language rules, ABI conventions, optimizer behavior, and C legacy.
Expand Down Expand Up @@ -499,7 +499,7 @@ Use `va_list` correctly or avoid entirely.

---

## 7. Attributes (`[[...]]`)
## 6. Attributes (`[[...]]`)

Attributes annotate functions for tools and optimizers.

Expand Down Expand Up @@ -781,7 +781,7 @@ If you want, I can add a follow-up file showing:

---

## 8. Static local variables
## 7. Static local variables

```cpp
void f() {
Expand All @@ -797,7 +797,7 @@ Facts:

---

## 9. Parameter passing and decay
## 8. Parameter passing and decay

### Arrays decay to pointers

Expand All @@ -812,7 +812,7 @@ void f(int[1024]); // all identical

---

## 10. Variadic interfaces
## 9. Variadic interfaces

### Three approaches

Expand All @@ -830,7 +830,7 @@ void f(std::initializer_list<int>);

---

## 11. Default arguments
## 10. Default arguments

* Only for trailing parameters
* Bound at call site
Expand All @@ -842,7 +842,7 @@ void log(int level = get_default());

---

## 13. Inline functions
## 11. Inline functions

### Meaning of `inline`

Expand All @@ -866,7 +866,7 @@ void log(int level = get_default());
* If you put the inline function's definition into a `*.cpp` file, and if it is called from another `*.cpp` file, you'll get an "unresolved external" error from the linker
---

## 14. Overload resolution (deep dive)
## 12. Overload resolution (deep dive)

Resolution order:

Expand All @@ -884,7 +884,7 @@ Resolution order:

---

## 15. Initializer lists `{}`
## 13. Initializer lists `{}`

### Deduction rules

Expand All @@ -905,7 +905,7 @@ f({}); // error: type unknown

---

## 16. C++ Callables & Lambdas
## 14. C++ Callables & Lambdas

This is a detailed reference on "callables" in C++ and the evolution of lambdas:
- C++98 function objects ("functors")
Expand Down Expand Up @@ -1284,7 +1284,7 @@ auto add10 = [](int x){ return add_free(10, x); };
```

`std::bind` pitfalls:
- placeholder indexing is hard to read
- argument indexing is hard to read
- value vs reference capture surprises (use `std::ref`)
- error messages can be noisy

Expand Down Expand Up @@ -1472,7 +1472,7 @@ If you want, I can add:

---

## 18. Macros vs functions
## 15. Macros vs functions

### 1. What the preprocessor is (and is not)

Expand Down Expand Up @@ -1844,7 +1844,7 @@ Small compile-time toggles can be appropriate, especially in embedded, or when y

---

## 19. At-Exit Functions
## 16. At-Exit Functions

* The quick_exit() function is like exit() except that it does not invoke any destructors
* Register functions to be invoked by quick_exit() using at_quick_exit()
Expand All @@ -1854,6 +1854,90 @@ Small compile-time toggles can be appropriate, especially in embedded, or when y
* std::quick_exit: doesn't execute static destructors, but does flush critical IO and call handler if exists
---

## Advanced function rules that matter from C++20 to C++26

### C++20 immediate functions distinguish evaluation time from return type

A `consteval` function must be invoked in an immediate-function context or as part of an allowed immediate invocation; its result must meet constant-evaluation rules. `constexpr` does not impose this requirement on every call. Neither keyword automatically makes expensive computations free: compile-time work consumes compiler resources and can shift cost from runtime into builds.

```cpp
consteval unsigned mask(unsigned bit) { return 1u << bit; }
constexpr unsigned selected = mask(5);
static_assert(selected == 32);
```

For untrusted or runtime indices, validate the shift precondition and use a normal runtime function. C++23 relaxed several constexpr function restrictions, and C++26 extends constant-evaluation capabilities further, but language-version support must be tested on the actual compiler.

### C++23 explicit object parameters replace many cv/ref overloads

The explicit object parameter, often called *deducing this*, gives a member function an explicit `this`-object parameter. The mechanism can reduce repetitive overloads for lvalue/rvalue and const/non-const access, and its type deduction interacts with forwarding in ways that ordinary implicit object parameter declarations do not.

```cpp
// C++23, when __cpp_explicit_this_parameter >= 202110L:
struct Counter {
int value = 0;
template<class Self>
decltype(auto) get(this Self&& self) {
return std::forward<Self>(self).value;
}
};
```

In actual builds include `<utility>` and guard the declaration itself; a C++20 compiler cannot parse the syntax even if it is inside an uninstantiated template. Explicit-object member functions differ from implicit-object members in pointer-to-member types, so check callbacks and ABI assumptions before modernizing widely exported APIs.

### C++23 `std::invoke_r` separates invocation from return conversion

`std::invoke` (C++17) unifies function objects, functions, and member pointers. C++23 `std::invoke_r<R>` additionally requests a conversion of the result to `R`, or discards it for `void`.

```cpp
#if defined(__cpp_lib_invoke_r) && __cpp_lib_invoke_r >= 202106L
auto f = [] { return 42; };
long result = std::invoke_r<long>(f);
#endif
```

This is preferable to informal `std::function` wrapping when no type erasure or ownership of callable state is needed. It does not turn arbitrary non-invocable expressions into valid invocations.

### C++23 `std::move_only_function` represents non-copyable callables

`std::function` historically required a copy-constructible stored target. C++23 adds `std::move_only_function`, which can hold a callable with move-only captures and can encode cv/ref and `noexcept` qualifiers in the wrapper's signature.

```cpp
#if defined(__cpp_lib_move_only_function) && __cpp_lib_move_only_function >= 202110L
std::move_only_function<int()> task =
[p = std::make_unique<int>(7)] { return *p; };
assert(task() == 7);
#endif
```

The wrapper is owning and type-erased; an empty invocation has preconditions. For non-owning callbacks, C++26 `std::function_ref` provides a different lifetime contract.

### C++26 `std::function_ref` avoids ownership but exposes dangling hazards

A `std::function_ref<R(Args...)>` is a lightweight reference to a callable, useful for synchronous algorithms that invoke it but do not retain it. Unlike `std::function` and `std::move_only_function`, it does not own the target or extend its lifetime.

```cpp
// C++26 with <functional> and __cpp_lib_function_ref support:
void for_each_item(std::span<const int> data,
std::function_ref<void(int)> callback);
```

Do not store such a reference for asynchronous use unless the callable's owner is explicitly guaranteed to survive. The benefit comes from communicating borrowing semantics rather than from promising zero overhead on every ABI.

### Coroutines are language transformations, not ordinary resumable function calls

C++20 coroutines use `co_await`, `co_yield`, or `co_return` to transform a function into a state machine whose frame and lifetime follow coroutine rules. The return type must supply an appropriate promise type. The compiler-generated frame can outlive the initial call, which changes what captured references and parameters are safe.

A coroutine that retains a reference to a stack local across suspension can dangle even when the same code would be safe in a synchronous function. Carefully distinguish parameters copied into the coroutine frame from objects merely referenced by it. Consider cancellation, exception propagation, and destruction of suspended frames as part of the function's contract.

### C++26 pack indexing refines generic function parameter packs

C++26 pack indexing (`Ts...[I]` in type contexts and `pack...[I]` in expression contexts) makes it possible to select a specific type or argument from a pack without recursive helper machinery. It does not alter the evaluation, forwarding, or lifetime rules of the selected argument.

Use a feature test such as `__cpp_pack_indexing` before introducing these expressions. Template design should still prefer a clear concept/constraint and describe what happens when the selected pack index is out of bounds.

---

## Rules worth keeping in working memory

The central distinction in functions and callable interfaces is between what the C++ type system guarantees, what the implementation commonly does, and what an API merely assumes. Carrying that distinction from declarations through operations avoids most of the surprises discussed above.
Expand Down Expand Up @@ -2005,9 +2089,9 @@ Concepts, `consteval`, coroutines, and richer constexpr rules extend callable co

Explicit object parameters and `if consteval` improve member and immediate-function APIs.

### C++26: Emerging improvements
### C++26: Borrowed callables and pack indexing

New callable and constexpr facilities are implementation-dependent until shipped.
C++26 standardizes `std::function_ref` as a non-owning callable view and pack indexing for directly selecting elements of parameter packs. Neither change removes callable lifetime or forwarding obligations; library support is feature-dependent.

When documenting a facility, state its required language/library version rather than inferring support from the compiler's branding.

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