Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The compiler is a committee

The thesis

This book treats the C++ toolchain as a single committee: clang++ front‑end, clang‑tidy, path‑sensitive static analyzer, address and undefined‑behavior sanitizers, and clangd. The clang++ front‑end, the clang‑tidy lint suite, the path‑sensitive static analyzer, the address and undefined‑behavior sanitizers, and the clangd language server together form the compiler committee. A diagnostic that originates from any of these components is a violation of the language law, not a mere suggestion. The Core Guidelines describe rules that the committee intends to make machine enforceable, and the committee enforces them.

clang++        front-end compiler
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

The canonical way to obtain the exact toolchain used by the book is to invoke the Nix flake that lives at the repository root. Running nix develop spawns a development shell that contains the pinned versions:

  • LLVM 22.1.8 provides clang++, clang‑tidy, clang‑analyzer and clangd.
  • CMake 4.3.4 drives the builds.
  • mdBook 0.5.4 renders the final HTML.

When Nix is unavailable the book falls back to system packages. On macOS the user can install the same LLVM version with brew install llvm. On Linux the user can follow the instructions at apt.llvm.org to obtain a recent clang. The script scripts/verify.sh builds every example, runs the tests, and builds the book. The script runs CMake to configure the build, builds all book_example targets, executes each test with CTest, and finally calls mdbook build to ensure the markdown renders without missing includes. The development shell also sets CC=clang and CXX=clang++. CMake locates clang‑tidy on its own with find_program. The same environment provides clangd for IDE integration.

Hello, world, honestly

The book writes #include <print> because the header <print> is the modern way to reach std::println. The directive #include is the build model the book uses, because the classic preprocessor workflow is what real multi‑file projects run today. The preprocessor expands #include before compilation.

The preprocessor gets full treatment in chapter 23, and modules in chapter 24.

std::println replaced std::cout and printf as the default output mechanism in C++23. It checks format strings at compile time, writes directly to stdout, and returns void. The older facilities still work, but the book uses the modern one from the first example so the reader never has to unlearn a habit.

#include <print>

int main() {
    std::println("hello, world");
}

Compiling the file by hand looks like this:

clang++ -std=c++26 -Wall -Wextra -Wpedantic -c ch01_hello.cpp
clang++ -std=c++26 -Wall -Wextra -Wpedantic ch01_hello.o -o ch01_hello

The short command line shows the language version flag, the three warning groups that the book treats as law, and the absence of any additional options. The resulting binary prints hello, world on standard output. The header <print> became part of the standard library in C++23 and is fully supported by the libc++ shipped with the pinned toolchain.

Warnings are laws

The next example illustrates a classic lifetime violation. The file returns a pointer to a local variable, a pattern that the compiler can diagnose.

// DEMO: deliberately wrong. This file exists to produce a diagnostic.
// The function returns a pointer to a dead local. The lifetime-safety
// analysis and the plain -Wreturn-stack-address warning both catch it.
//
//   examples/ch01/ch01_dangling.cpp:9:13: warning: address of stack memory
//   associated with local variable 'x' returned [-Wreturn-stack-address]
//       9 |     return &x;
//         |             ^

int* bad() {
    int x = 42;
    return &x;
}

int main() {
    int* p = bad();
    return *p;
}

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]
    9 |     return &x;
      |             ^

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 stores into it, and clang enables it by default.

Sanitizers as Compile Options

Dynamic checking is another member of the committee. The address sanitizer (ASan) and the undefined‑behavior sanitizer (UBSan) detect errors at runtime. The following example purposely reads past the end of a std::vector to trigger an ASan report.

// DEMO: deliberately wrong. This file exists to produce a sanitizer report.
// Run under AddressSanitizer, the read past the end of the vector storage
// reports heap-buffer-overflow:
//
//   ==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000fc
//   READ of size 4 at 0x6020000000fc thread T0
//       #0 ... std::__1::__format::__create_format_arg ... format_arg_store.h:191
//       ...
//       #8 ... in main ch01_overflow.cpp:13

#include <print>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};
    volatile std::size_t past_the_end = v.size(); // volatile: defeat constant folding
    std::println("{}", v.data()[past_the_end]);
}

The report pasted in the file appears again exactly:

==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000fc
READ of size 4 at 0x6020000000fc thread T0
    #0 ... std::__1::__format::__create_format_arg ... format_arg_store.h:191
    ...
    #8 ... in main ch01_overflow.cpp:13

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

The clang static analyzer runs a path‑sensitive analysis that discovers bugs that are plain warnings miss. The command scan-build clang++ … invokes the analyzer and prints a concise HTML report. Integrated development environments that use clangd can invoke the analyzer on the fly. An example that the analyzer catches but the usual warnings miss is a use‑after‑free of a heap object passed through a function pointer. The analyzer reports the error with a trace that shows the allocation, the free, and the later dereference. The analyzer works by symbolically executing the control‑flow graph of each function and exploring the paths that reach each statement. It can track heap allocations across function boundaries and can flag memory leaks even when the program frees memory in a different function. The analysis is conservative. It reports false positives on some code patterns, and a reported error is still worth the reader’s time.

The Rulebook

The committee’s rulebook lives in the repository root as .clang‑tidy. The file appears verbatim below.

# The compiler committee's rulebook. Every book_example target builds with
# these checks; ch01 quotes this file verbatim. clang-tidy runs as part of
# compilation (see cmake/BookExample.cmake), so every retained check must stay
# silent on the examples.
#
# Stylistic checks are pruned: they either argue with the book's teaching
# style or fire on idiomatic example code. The retained correctness checks
# (bugprone, cppcoreguidelines, clang-analyzer, and the rest) stay on so a
# real defect in an example is still caught.
#
# Disabled checks are deliberate:
# - *avoid-magic-numbers: teaching examples are full of small literal values
#   whose names would add noise, not safety.
# - modernize-use-trailing-return-type: this book uses leading return types.
# - bugprone-exception-escape: every example main() may throw from std::println;
#   this check would flag all of them for no safety gain.
# - The stylistic checks below are pruned for the same reason: they fight the
#   book's style rather than find bugs.
Checks: >
  bugprone-*,
  cppcoreguidelines-*,
  modernize-*,
  performance-*,
  readability-*,
  clang-analyzer-*,
  -readability-identifier-length,
  -readability-braces-around-statements,
  -readability-named-parameter,
  -readability-implicit-bool-conversion,
  -readability-math-missing-parentheses,
  -readability-simplify-subscript-expr,
  -readability-isolate-declaration,
  -readability-convert-member-functions-to-static,
  -readability-make-member-function-const,
  -readability-else-after-return,
  -readability-container-data-pointer,
  -modernize-use-designated-initializers,
  -modernize-use-nodiscard,
  -modernize-use-std-numbers,
  -modernize-use-ranges,
  -modernize-avoid-bind,
  -modernize-type-traits,
  -modernize-use-constraints,
  -modernize-avoid-c-arrays,
  -performance-avoid-endl,
  -performance-unnecessary-value-param,
  -performance-inefficient-vector-operation,
  -bugprone-easily-swappable-parameters,
  -bugprone-crtp-constructor-accessibility,
  -bugprone-unsafe-functions,
  -cppcoreguidelines-avoid-non-const-global-variables,
  -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
  -cppcoreguidelines-pro-bounds-pointer-arithmetic,
  -cppcoreguidelines-pro-bounds-constant-array-index,
  -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
  -cppcoreguidelines-avoid-c-arrays,
  -cppcoreguidelines-owning-memory,
  -cppcoreguidelines-rvalue-reference-param-not-moved,
  -clang-analyzer-unix.Stream,
  -cppcoreguidelines-avoid-magic-numbers,
  -readability-magic-numbers,
  -modernize-use-trailing-return-type,
  -bugprone-exception-escape
WarningsAsErrors: '*'
HeaderFilterRegex: '.*'
FormatStyle: file

The configuration enables a broad set of checks grouped under the headings bugprone, cppcoreguidelines, modernize, performance, readability, and clang-analyzer. The bugprone group catches patterns that frequently cause bugs, for example misuse of std::move. The cppcoreguidelines group enforces the Core Guidelines, such as requiring initialization of all members. The modernize group suggests modern C++ idioms, for example replacing raw arrays with std::array. The performance group flags inefficient constructions such as unnecessary copies. The readability group encourages clear code, for example by preferring range‑based loops over index arithmetic. The clang-analyzer group runs the static analyzer during compilation. Three checks are deliberately disabled:

  • avoid-magic-numbers: teaching examples contain many small literal values and naming each one adds noise.
  • 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 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. 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:

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 build only when the build passes the option BOOK_ENABLE_GAPS=ON.

Try This

Trigger a UBSan diagnostic

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.

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.