Skip to content

Chapter 1: The Workbench

Chapter goal

Build the workbench every later chapter plugs into: a library, a CLI that reports its own build environment and feature matrix, a DOT writer, a test harness, and a benchmark harness.

You will not touch a tree or a graph today. You will build the instrument that measures them.

Files CMakeLists.txt, .clang-format, .clang-tidy, include/atlas/version.hpp, include/atlas/features.hpp, include/atlas/dot.hpp, src/features.cpp, src/dot.cpp, apps/atlas/main.cpp, tests/test_main.cpp, bench/bench_main.cpp
New ideas The five-question lens; feature detection with macros and requires; std::expected; std::print and std::format; std::span; DOT; a test harness; a benchmark harness
Time 90–120 minutes

1. Why a workbench?

A course about trees and graphs could start with struct Node { Node* left; Node* right; }; and a diagram. Most do. This one starts with a workbench because the hard part of the subject is not writing a node. The hard part is knowing which node, why it stays correct, what it costs, and how you know.

Every chapter in this book answers the same five questions about every structure:

Question What a chapter must answer
Contract What operations exist, what do they mean, what can fail?
Invariant What one sentence makes every operation correct, and why does it survive?
Representation How does the structure sit in memory: ownership, layout, indices, cache lines?
Cost Time, space, cache, worst/amortized/expected — measured, not asserted.
Evidence What tests, benchmarks, and pictures prove the first four?

That is the spine of the book. It is also the reason we begin with a workbench: the fifth question needs somewhere to live. Tests and benchmarks are not an afterthought you add at the end of a project. They are part of the structure, and they need a home from the first day.

Think of a carpenter. The first project is not a chair; it is a bench, a set of jigs, and a sharpening station. A wobbly bench makes every later cut crooked, and a dull chisel is discovered only after the joinery is ruined. Our bench is atlas: one library, one CLI, one test harness, one benchmark harness, one DOT writer. Every later chapter adds a drawer to it.

Where this is going

Seven parts, thirty-six chapters. Part I builds the foundations: analysis, traversal, representation, and the search-tree contract. Part II keeps search trees balanced. Part III turns trees into priority engines, compressors, string indexes, storage engines, range indexes, and spatial indexes. Parts IV and V do graphs: traversal, connectivity, spanning, shortest paths, strongly connected components, and flow. Part VI covers the advanced canon: tree queries, dynamic graphs, persistence, and succinct structures. Part VII is craft: testing, performance, visualization, hard graphs, and a capstone.

Along the way, seven optional studios turn the library into an expression language, a game world, an exchange, a search engine, a mission planner, a versioned filesystem, and a visualizer. Chapter 1 gives all of them a place to stand.

2. The shape of the repository

Here is the layout you are about to type:

.
├── CMakeLists.txt
├── include/atlas/       # the library's public contract
│   ├── version.hpp
│   ├── features.hpp
│   └── dot.hpp
├── src/                 # the library's implementation
│   ├── features.cpp
│   └── dot.cpp
├── apps/atlas/          # the command-line client
│   └── main.cpp
├── tests/               # the proof
│   └── test_main.cpp
├── bench/               # the cost evidence
│   └── bench_main.cpp
├── studios/             # later: the set-piece projects
└── tutorial/            # the book you are reading

The split is deliberate:

  • include/atlas/ holds the contract. A header is what the rest of the program is allowed to know.
  • src/ holds the implementation. It can change without changing the contract.
  • apps/atlas/ is a client. It links the library; the library does not know it exists.
  • tests/ and bench/ are also clients. They answer the fifth question.

The library target is called atlas_core. The CLI is atlas, the tests are atlas_tests, and the benchmark is atlas_bench. Four targets, one library. In later chapters, the studios will link atlas_core too, which is how a chapter-sized idea becomes a project-sized one without forking the code.

You manage the C++ toolchain in this course. The chapter shows CMakeLists.txt so you can see how the targets fit together, but it is not a CMake tutorial. If you already have a build setup you prefer, keep it; the code is what matters.

3. The five questions, applied to the workbench

Before we write a line, apply the lens to the thing we are building:

Question The workbench's answer
Contract Four commands: doctor, features, dot, version. Exit code 0 on success, 2 on a usage error. Output is deterministic.
Invariant The feature matrix is computed at compile time from the same translation unit the CLI uses. There is no runtime discovery and no second source of truth.
Representation A static library, a constexpr array of Feature records, std::string_view names, and a std::span to read the array.
Cost Startup is a few milliseconds. The matrix costs nothing at run time because the compiler already answered every question.
Evidence atlas_tests, the doctor output itself, the DOT text, the benchmark CSV, and the chapter checkpoint.

This is the habit the book is trying to install. The lens is not only for AVL trees and flow networks; it works on a 400-line workbench too.

4. The state card

Start with the smallest file, the version header. It is the only place where the name and version of the project are written down.

#pragma once

#include <string_view>

namespace atlas {

inline constexpr std::string_view kName = "atlas";
inline constexpr std::string_view kVersion = "0.1.0";
inline constexpr std::string_view kTagline = "a workbench for trees and graphs";

} // namespace atlas

Three inline constexpr variables. inline lets the header be included in several translation units without a linker error; constexpr makes them compile-time constants; std::string_view is a non-owning view over a string literal, so no allocation happens. When Chapter 12 needs a version string in a compressed file header, it will already be here.

Next, the feature record and the functions that expose it:

#pragma once

#include <span>
#include <string_view>

namespace atlas::features {

enum class Group { cxx23_adapters, cxx23_facilities, cxx26_preview };
enum class Status { available, missing };

struct Feature {
    std::string_view name;
    Group group;
    Status status;
    std::string_view note;
};

[[nodiscard]] std::span<const Feature> all() noexcept;
[[nodiscard]] std::string_view group_name(Group group) noexcept;
[[nodiscard]] std::string_view status_name(Status status) noexcept;

} // namespace atlas::features

Feature is a plain record: a name, a group, a status, and a note. The two enum class types make the groups and statuses impossible to confuse with an integer. [[nodiscard]] says that ignoring the return value is a bug. The functions are noexcept because they cannot fail.

The interesting part is the return type of all(): std::span<const Feature>. A span is a pointer and a length — a view over contiguous data. It does not own the features; the array does. The caller gets a cheap, bounds-aware way to iterate. This is a pattern you will see in every later chapter: the storage owns, the interface lends.

Now the CLI's command parser. This is where std::expected enters:

enum class Command { doctor, features, dot, version };

std::expected<Command, std::string> parse_command(std::string_view text) {
    if (text == "doctor") {
        return Command::doctor;
    }
    if (text == "features") {
        return Command::features;
    }
    if (text == "dot") {
        return Command::dot;
    }
    if (text == "version") {
        return Command::version;
    }
    return std::unexpected(std::format("unknown command '{}'", text));
}

std::expected<T, E> is C++23's vocabulary type for "a value or an error". It is not an exception and not a sentinel: the error is part of the return type, so the caller cannot forget it. std::unexpected builds the error branch. The error type is std::string because the message needs to own its characters; text is a std::string_view that may point into argv, and we do not want the error to dangle.

In main, the result is checked before use:

int main(int argc, char** argv) {
    const std::span<char*> args{argv, static_cast<std::size_t>(argc)};
    if (args.size() < 2U) {
        std::print("{}", kUsage);
        return 2;
    }

    const auto command = parse_command(std::string_view{args[1]});
    if (!command) {
        std::println(stderr, "atlas: {}", command.error());
        std::print(stderr, "{}", kUsage);
        return 2;
    }

    switch (*command) {
    case Command::doctor:
        doctor();
        break;
    case Command::features:
        print_features();
        break;
    case Command::dot:
        dot_demo();
        break;
    case Command::version:
        std::println("{} {}", atlas::kName, atlas::kVersion);
        break;
    }
    return 0;
}

Four details are worth slowing down for.

std::span<char*> args{argv, static_cast<std::size_t>(argc)} wraps the C array in a view with an explicit length. The cast is there because argc is an int and the span's size is a std::size_t; -Wsign-conversion would complain otherwise, and it would be right to.

std::string_view{args[1]} does not copy the argument. It points at the characters the shell already allocated. We only read them; parse_command takes the view by value.

if (!command) is the expected idiom: operator bool is true when there is a value. command.error() is only valid in the error branch, and *command is only valid in the value branch. The type makes the two branches explicit.

std::println(stderr, ...) prints to standard error. The first argument may be a FILE*; the rest is a format string and its arguments. std::print and std::println are C++23's type-safe replacement for the iostream state machine. No std::endl, no manipulators, no << chains.

The doctor command pulls the state card together:

void doctor() {
    std::println("{} {} — {}", atlas::kName, atlas::kVersion, atlas::kTagline);
    std::println("build      {}, {}", build_type(), standard_name());
    std::println("compiler   {} {}", compiler_name(), compiler_version());
    std::println("platform   {}", platform_name());
    std::println("");
    std::println("The build itself proves the required features:");
    std::println("  std::print, std::format, std::expected, std::span,");
    std::println("  std::string_view, std::ranges, std::chrono");
    print_features();
}

build_type() reads NDEBUG, which CMake defines in a Release build. standard_name() formats __cplusplus:

std::string standard_name() {
    return std::format("C++{} ({})", __cplusplus / 100 % 100, __cplusplus);
}

On this toolchain, __cplusplus is 202302, so the output is C++23 (202302). The division and remainder are a small trick: 202302 / 100 is 2023, and 2023 % 100 is 23. It is not a complete standard-version parser, but it is honest for the values a C++23 compiler produces.

The compiler and platform helpers are preprocessor switches:

std::string_view compiler_name() {
#if defined(__clang__)
    return "Clang";
#elif defined(__GNUC__)
    return "GCC";
#elif defined(_MSC_VER)
    return "MSVC";
#else
    return "unknown";
#endif
}

Clang defines __GNUC__ too, which is why the __clang__ branch comes first. The platform helper follows the same shape. These are the kinds of facts a state card should report, because "it does not compile" is a very different problem on Apple clang, Homebrew clang, GCC, and MSVC.

Run it:

$ ./build/atlas doctor
atlas 0.1.0 — a workbench for trees and graphs
build      Release, C++23 (202302)
compiler   Clang 23.1.4
platform   macOS arm64

The build itself proves the required features:
  std::print, std::format, std::expected, std::span,
  std::string_view, std::ranges, std::chrono

C++23 adapters
  std::flat_map            PASS    ordered baseline for Ch 5
  std::mdspan              PASS    matrices and DP tables
  ranges::to               PASS    materialize views
  views::enumerate         PASS    indexed loops
  views::zip               PASS    parallel iteration
  views::join_with         PASS    string assembly
  views::chunk             MISSING if missing, build in Ch 2
  views::slide             MISSING if missing, build in Ch 2
  ranges::fold_left        PASS    range fold
  ranges::starts_with      PASS    range prefix test
  views::cartesian_product MISSING if missing, build in Ch 2

C++23 facilities
  std::generator           MISSING if missing, build in Ch 3
  std::move_only_function  MISSING std::function fallback
  std::stacktrace          MISSING not used by the course

C++26 preview
  std::hive                MISSING C++26 preview
  std::inplace_vector      MISSING C++26 preview
  std::submdspan           MISSING C++26 preview
  std::execution::just     MISSING C++26 preview
  contracts                MISSING C++26 preview
  reflection               MISSING C++26 preview

The top half is the machine you are on. The bottom half is the machine the book is written for. They are not the same, and the difference is the next section.

5. What your compiler actually has

The book says "C++23 or newer" because that is the promise the standard makes. Your compiler makes a narrower promise: it implements some of C++23, most of C++20, and none of C++26 yet. If the book pretends otherwise, the first chapter that uses std::generator becomes a chapter about compiler bugs.

So the workbench probes the toolchain and prints the truth. There are three techniques, and they are easy to confuse:

  1. __has_include(<header>) asks whether a header exists.
  2. A feature-test macro such as __cpp_lib_flat_map asks whether the library claims a feature.
  3. A requires expression asks whether a specific type or call actually compiles.

The first two are cheap and safe. The third is the gold standard, but it has a sharp edge: naming something that does not exist inside a requires expression is a hard error, not a false result. A requires expression checks expressions; it cannot rescue a name that was never declared.

That is why every probe in src/features.cpp is guarded twice. Here is the pattern, with the flat-map probe and two of the missing adapters:

#if __has_include(<flat_map>) && defined(__cpp_lib_flat_map)
constexpr bool kFlatMap = requires { typename std::flat_map<int, int>; };
#else
constexpr bool kFlatMap = false;
#endif

#if defined(__cpp_lib_ranges_chunk)
constexpr bool kChunk =
    requires(std::vector<int> values) { values | std::views::chunk(2); };
#else
constexpr bool kChunk = false;
#endif

#if defined(__cpp_lib_ranges_fold)
constexpr bool kFoldLeft = requires(std::vector<int> values) {
    std::ranges::fold_left(values, 0, std::plus{});
};
#else
constexpr bool kFoldLeft = false;
#endif

The first #if is the gate. If the header or the macro is missing, the requires expression is not compiled at all, and the answer is simply false. If the gate passes, the requires expression runs and confirms that the API the course plans to call is really there. The two gates answer two different questions: does the library claim it? and does the call work?

There is a story behind fold_left that is worth telling, because it is the kind of mistake that will happen to you.

When I first probed std::ranges::fold_left, the compiler said no member named 'fold_left' in namespace 'std::ranges'. The feature-test macro was defined, so I assumed the library was lying. It was not. fold_left is a range algorithm, and range algorithms live in <algorithm>, not in <ranges>. The probe's #include list was wrong, not the macro. Once <algorithm> was included, the probe passed.

The lesson is not "trust the macro". It is: a failed probe can be your bug. Check the header, the namespace, and the exact overload before you conclude that the library is incomplete. The matrix is evidence, not a verdict.

The hard-error trap

This does not compile on a library that lacks views::chunk, even though it looks like a probe:

constexpr bool kChunk =
    requires(std::vector<int> values) { values | std::views::chunk(2); };

The name std::views::chunk does not exist, so the compiler stops before it can answer false. Gate the expression with the feature-test macro first. The same rule applies to std::generator, std::move_only_function, and every C++26 name in the table.

The matrix is data, not decoration. The CLI reads the same constexpr array that the tests read, and the tests check its shape. When the toolchain changes, the matrix changes, and the book can point at the difference instead of pretending it does not exist.

What the matrix says about this book:

  • std::generator is missing, so Chapter 3 builds a lazy in-order range by hand — a custom iterator and a std::ranges-compatible view — and offers a coroutine-based generator as a bonus. That is better pedagogy than co_yield magic, and it teaches the iterator machinery properly.
  • views::chunk, views::slide, and views::cartesian_product are missing, so Chapter 2 implements two of them as exercises. You learn how views work by filling the holes.
  • std::move_only_function is missing; visitors will use templates or std::function, and the difference is discussed when it matters.
  • std::stacktrace is missing, and the course does not depend on it.
  • Every C++26 feature in the table is missing. Contracts, reflection, std::execution, std::hive, std::inplace_vector, and std::submdspan appear as "tomorrow's C++" asides and in an appendix. They never become a dependency.

This is what "C++23 or newer" means in practice: the course teaches the language you actually have, and it teaches you to check.

6. The first picture: DOT

Trees and graphs are hard to hold in your head. The course uses three tiers of visualization, and the first one is text.

DOT is the graph description language used by Graphviz. A graph is a set of nodes and edges in a small, readable syntax:

digraph "atlas" {
    "root" [label="atlas", color="black"];
    "trees" [label="trees", color="darkgreen"];
    "graphs" [label="graphs", color="navy"];
    "root" -> "trees" [label="left"];
    "root" -> "graphs" [label="right"];
}

DOT earns its place for three reasons. It is text, so it diffs and commits cleanly. It is deterministic, so a test can compare it exactly. And it is layout-independent: Graphviz decides where the nodes go, so we can focus on the structure rather than on coordinates. Graphviz is optional; it is not installed on this machine by default. You can install it with brew install graphviz, or paste the output into any online DOT viewer.

The types are deliberately small:

struct Node {
    std::string id;
    std::string label;
    std::string color;
};

struct Edge {
    std::string from;
    std::string to;
    std::string label;
};

struct Graph {
    std::string name;
    bool directed = true;
    std::vector<Node> nodes;
    std::vector<Edge> edges;
};

Graph owns its strings and its vectors. This is the first ownership decision in the book, and it is the simple one: a graph you are about to print is not on a hot path, so clarity beats a string arena. Later structures will make the opposite trade-off for good reasons; the lens will tell us when.

The renderer has one job that is easy to get wrong: escaping. A label containing a quote or a backslash will break the DOT file, and the resulting error from Graphviz will point at the wrong line. Escape the characters that matter:

std::string escape(std::string_view text) {
    std::string out;
    out.reserve(text.size());
    for (const char c : text) {
        switch (c) {
        case '"':
            out += "\\\"";
            break;
        case '\\':
            out += "\\\\";
            break;
        case '\n':
            out += "\\n";
            break;
        default:
            out += c;
            break;
        }
    }
    return out;
}

out.reserve(text.size()) is a small optimization with a real lesson: the escaped string is never shorter than the input, so one allocation of the input size covers the common case. The switch handles the three characters that would otherwise change the meaning of the quoted string. A newline becomes the two characters \ and n, which DOT interprets as a line break.

The renderer builds the file a line at a time:

std::string to_dot(const Graph& graph) {
    const std::string_view kind = graph.directed ? "digraph" : "graph";
    std::string out = std::format("{} {} {{\n", kind, quoted(graph.name));
    for (const Node& node : graph.nodes) {
        out += std::format("    {} [label={}", quoted(node.id),
                           quoted(node.label));
        if (!node.color.empty()) {
            out += std::format(", color={}", quoted(node.color));
        }
        out += "];\n";
    }
    for (const Edge& edge : graph.edges) {
        const std::string_view arrow = graph.directed ? "->" : "--";
        out += std::format("    {} {} {}", quoted(edge.from), arrow,
                           quoted(edge.to));
        if (!edge.label.empty()) {
            out += std::format(" [label={}]", quoted(edge.label));
        }
        out += ";\n";
    }
    out += "}\n";
    return out;
}

The function is a pure transformation: same graph in, same text out. That is exactly what makes it testable. There is no global state, no clock, no random ordering. If the test compares the output against a golden string, a failure means the graph or the renderer changed — not that the machine was in a different mood.

atlas dot builds a three-node graph and prints it:

$ ./build/atlas dot
digraph "atlas" {
    "root" [label="atlas", color="black"];
    "trees" [label="trees", color="darkgreen"];
    "graphs" [label="graphs", color="navy"];
    "root" -> "trees" [label="left"];
    "root" -> "graphs" [label="right"];
}

Save it as atlas.dot, run dot -Tsvg atlas.dot -o atlas.svg, and open the SVG. You now have the first picture in the book, and it took fewer than sixty lines. The later tiers will produce better pictures — terminal renderings in Chapter 3, our own SVG layout in Chapter 34 — but this one will remain the interchange format that every structure can emit.

7. Proving it: the test harness

A test framework is a dependency, and dependencies have a cost: installation, configuration, version drift, and a syntax you have to learn before you can test anything. For the first chapter, thirty lines are enough.

struct TestState {
    int failures = 0;
};

void check(TestState& state, bool condition, std::string_view expression,
           int line) {
    if (condition) {
        return;
    }
    std::println(stderr, "FAIL line {}: {}", line, expression);
    ++state.failures;
}

#define CHECK(expr) check(state, (expr), #expr, __LINE__)

TestState holds the failure count, so there is no mutable global. check prints the source line and the expression that failed; #expr is the stringification operator, which turns the macro argument into text. __LINE__ is the line number at the macro call site, not inside the function. The macro is ugly, but it is a test harness; a little ugliness in service of a good error message is a fair trade.

The tests check four things:

CHECK(!atlas::kName.empty());
CHECK(atlas::kVersion == "0.1.0");
CHECK(atlas::features::all().size() > 10);
CHECK(atlas::features::all().front().name == "std::flat_map");

CHECK(atlas::dot::escape("plain") == "plain");
CHECK(atlas::dot::escape("a\"b") == "a\\\"b");
CHECK(atlas::dot::escape("a\\b") == "a\\\\b");
CHECK(atlas::dot::escape("a\nb") == "a\\nb");

The first four are shape checks: the version exists, the table is not empty, and the table begins with the feature the course will lean on first. The next four are behavior checks: the escaper handles the characters that would break a DOT file. The distinction matters. Shape checks catch a broken build; behavior checks catch a broken idea.

The DOT check builds a tiny graph and asserts that the text contains the pieces that matter:

atlas::dot::Graph graph;
graph.name = "test";
graph.nodes = {{"a", "A", ""}, {"b", "B", ""}};
graph.edges = {{"a", "b", ""}};
const std::string dot_text = atlas::dot::to_dot(graph);
CHECK(dot_text.contains("digraph \"test\""));
CHECK(dot_text.contains("\"a\" -> \"b\""));

std::string::contains is a C++23 addition; until then you had to spell find(...) != npos. The check is intentionally loose: it does not pin the whole file, because the renderer may legitimately grow formatting options later. It pins the contract — a directed graph says digraph, and an edge uses ->.

ctest runs the test target:

$ ctest --test-dir build --output-on-failure
Test project /Users/dlcmh/dev/treegraph-tutorial/build
    Start 1: atlas_tests
1/1 Test #1: atlas_tests ......................   Passed    0.03 sec

100% tests passed out of 1

The add_test line in CMakeLists.txt is what makes that work. It is one line, and it is the difference between "I ran the tests once" and "the tests run every time".

8. Measuring it: the benchmark harness

The last piece of the workbench is the stopwatch. A benchmark is not a proof of correctness, but it is evidence about cost, and the lens asks for evidence.

using Clock = std::chrono::steady_clock;

template <class Fn> double measure_ms(Fn&& fn) {
    const auto start = Clock::now();
    std::forward<Fn>(fn)();
    const auto stop = Clock::now();
    return std::chrono::duration<double, std::milli>(stop - start).count();
}

std::chrono::steady_clock is monotonic: it cannot jump backward when the system clock is adjusted, which makes it the right clock for measuring elapsed time. The duration is converted to double milliseconds, so the output is readable and the arithmetic does not silently truncate.

The first benchmark compares traversal of a std::vector<int> with traversal of a std::list<int>:

void bench_vector(const std::vector<int>& values, volatile long long& sink) {
    long long sum = 0;
    for (const int value : values) {
        sum += value;
    }
    sink = sum;
}

void bench_list(const std::list<int>& values, volatile long long& sink) {
    long long sum = 0;
    for (const int value : values) {
        sum += value;
    }
    sink = sum;
}

The volatile sink is not decoration. Without it, the optimizer is allowed to notice that the sum is never used and delete the loop; the benchmark would then measure nothing very quickly. Writing the sum to a volatile object forces the computation to happen.

The containers are built once, outside the timer. The timer measures only the traversal:

std::vector<int> vector_values(kCount);
std::iota(vector_values.begin(), vector_values.end(), 0);
const std::list<int> list_values(vector_values.begin(), vector_values.end());

volatile long long sink = 0;
const double vector_ms =
    measure_ms([&] { bench_vector(vector_values, sink); });
const double list_ms = measure_ms([&] { bench_list(list_values, sink); });

On this machine, one million integers take about 0.4 ms in a vector and about 2.9 ms in a list:

$ ./build/atlas_bench --csv
benchmark,ms
vector_traverse,0.393
list_traverse,2.869
sink 499999500000

The sum is the same either way; the data is the same either way. The difference is layout. The vector stores its integers contiguously, so the CPU's prefetcher can guess what comes next. The list stores each integer in a separately allocated node with two pointers, so every step is a pointer chase into a new cache line. The list is not "slower because it is a linked list"; it is slower because it asks the memory system a harder question.

That is the first measured lesson of the course, and it is deliberately a small one. Chapter 2 turns this harness into a method: counters, growth curves, and the difference between a benchmark and an experiment.

9. The complete file

CMakeLists.txt
cmake_minimum_required(VERSION 3.28)

# Force Homebrew LLVM before project() so it takes effect.
if(APPLE)
    if(EXISTS "/opt/homebrew/opt/llvm/bin/clang++")
        set(CMAKE_CXX_COMPILER "/opt/homebrew/opt/llvm/bin/clang++")
        set(CMAKE_C_COMPILER "/opt/homebrew/opt/llvm/bin/clang")
    elseif(EXISTS "/usr/local/opt/llvm/bin/clang++")
        set(CMAKE_CXX_COMPILER "/usr/local/opt/llvm/bin/clang++")
        set(CMAKE_C_COMPILER "/usr/local/opt/llvm/bin/clang")
    endif()
endif()

project(atlas LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_compile_options(
    -Wall
    -Wextra
    -Wpedantic
    -Wconversion
    -Wsign-conversion
)

add_library(atlas_core
    src/dot.cpp
    src/features.cpp
)
target_include_directories(atlas_core PUBLIC include)

add_executable(atlas apps/atlas/main.cpp)
target_link_libraries(atlas PRIVATE atlas_core)

add_executable(atlas_tests tests/test_main.cpp)
target_link_libraries(atlas_tests PRIVATE atlas_core)

add_executable(atlas_bench bench/bench_main.cpp)
target_link_libraries(atlas_bench PRIVATE atlas_core)

enable_testing()
add_test(NAME atlas_tests COMMAND atlas_tests)
.clang-format
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 80
DerivePointerAlignment: false
PointerAlignment: Left
.clang-tidy
Checks: >
  -*,
  cppcoreguidelines-*,
  modernize-*,
  performance-*,
  readability-*,
  -modernize-use-trailing-return-type,
  -cppcoreguidelines-avoid-magic-numbers,
  -readability-magic-numbers,
  -readability-identifier-length,
  -modernize-use-designated-initializers,
  -cppcoreguidelines-pro-type-vararg,
  -performance-enum-size,
  -readability-use-concise-preprocessor-directives,
  -readability-trailing-comma,
  -readability-redundant-typename,
  -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
  -cppcoreguidelines-pro-bounds-pointer-arithmetic,
  -modernize-use-ranges

WarningsAsErrors: >
  cppcoreguidelines-owning-memory,
  modernize-make-unique,
  modernize-make-shared,
  modernize-use-using
include/atlas/version.hpp
#pragma once

#include <string_view>

namespace atlas {

inline constexpr std::string_view kName = "atlas";
inline constexpr std::string_view kVersion = "0.1.0";
inline constexpr std::string_view kTagline = "a workbench for trees and graphs";

} // namespace atlas
include/atlas/features.hpp
#pragma once

#include <span>
#include <string_view>

namespace atlas::features {

enum class Group { cxx23_adapters, cxx23_facilities, cxx26_preview };
enum class Status { available, missing };

struct Feature {
    std::string_view name;
    Group group;
    Status status;
    std::string_view note;
};

[[nodiscard]] std::span<const Feature> all() noexcept;
[[nodiscard]] std::string_view group_name(Group group) noexcept;
[[nodiscard]] std::string_view status_name(Status status) noexcept;

} // namespace atlas::features
include/atlas/dot.hpp
#pragma once

#include <string>
#include <string_view>
#include <vector>

namespace atlas::dot {

struct Node {
    std::string id;
    std::string label;
    std::string color;
};

struct Edge {
    std::string from;
    std::string to;
    std::string label;
};

struct Graph {
    std::string name;
    bool directed = true;
    std::vector<Node> nodes;
    std::vector<Edge> edges;
};

// Escape the characters that would otherwise break a DOT quoted string.
[[nodiscard]] std::string escape(std::string_view text);

// Render a graph as Graphviz DOT. The result is deterministic: same graph,
// same text, which is what makes it testable.
[[nodiscard]] std::string to_dot(const Graph& graph);

} // namespace atlas::dot
src/features.cpp
#include "atlas/features.hpp"

#include <version>

#include <algorithm>
#include <array>
#include <functional>
#include <ranges>
#include <string>
#include <utility>
#include <vector>

#if __has_include(<execution>)
#include <execution>
#endif
#if __has_include(<flat_map>)
#include <flat_map>
#endif
#if __has_include(<generator>)
#include <generator>
#endif
#if __has_include(<hive>)
#include <hive>
#endif
#if __has_include(<inplace_vector>)
#include <inplace_vector>
#endif
#if __has_include(<mdspan>)
#include <mdspan>
#endif
#if __has_include(<stacktrace>)
#include <stacktrace>
#endif

namespace atlas::features {
namespace {

// A feature-test macro is the first gate: if it is missing, the name may not
// exist at all, and naming it inside a requires-expression is a hard error.
// The requires-expression is the second gate: it checks that the API we plan
// to call actually works, not merely that a macro says it should.

#if __has_include(<flat_map>) && defined(__cpp_lib_flat_map)
constexpr bool kFlatMap = requires { typename std::flat_map<int, int>; };
#else
constexpr bool kFlatMap = false;
#endif

#if __has_include(<mdspan>) && defined(__cpp_lib_mdspan)
constexpr bool kMdspan =
    requires { typename std::mdspan<int, std::extents<int, 2, 2>>; };
#else
constexpr bool kMdspan = false;
#endif

#if defined(__cpp_lib_ranges_to_container)
constexpr bool kRangesTo = requires(std::vector<int> values) {
    std::move(values) | std::ranges::to<std::vector>();
};
#else
constexpr bool kRangesTo = false;
#endif

#if defined(__cpp_lib_ranges_enumerate)
constexpr bool kEnumerate =
    requires(std::vector<int> values) { values | std::views::enumerate; };
#else
constexpr bool kEnumerate = false;
#endif

#if defined(__cpp_lib_ranges_zip)
constexpr bool kZip = requires(std::vector<int> left, std::vector<int> right) {
    std::views::zip(left, right);
};
#else
constexpr bool kZip = false;
#endif

#if defined(__cpp_lib_ranges_join_with)
constexpr bool kJoinWith = requires(std::vector<std::vector<int>> values) {
    values | std::views::join_with(std::vector<int>{0});
};
#else
constexpr bool kJoinWith = false;
#endif

#if defined(__cpp_lib_ranges_chunk)
constexpr bool kChunk =
    requires(std::vector<int> values) { values | std::views::chunk(2); };
#else
constexpr bool kChunk = false;
#endif

#if defined(__cpp_lib_ranges_slide)
constexpr bool kSlide =
    requires(std::vector<int> values) { values | std::views::slide(2); };
#else
constexpr bool kSlide = false;
#endif

#if defined(__cpp_lib_ranges_fold)
constexpr bool kFoldLeft = requires(std::vector<int> values) {
    std::ranges::fold_left(values, 0, std::plus{});
};
#else
constexpr bool kFoldLeft = false;
#endif

#if defined(__cpp_lib_ranges_starts_ends_with)
constexpr bool kStartsWith = requires(std::string text, std::string prefix) {
    std::ranges::starts_with(text, prefix);
};
#else
constexpr bool kStartsWith = false;
#endif

#if defined(__cpp_lib_ranges_cartesian_product)
constexpr bool kCartesian =
    requires(std::vector<int> left, std::vector<int> right) {
        std::views::cartesian_product(left, right);
    };
#else
constexpr bool kCartesian = false;
#endif

#if __has_include(<generator>) && defined(__cpp_lib_generator)
constexpr bool kGenerator = requires { typename std::generator<int>; };
#else
constexpr bool kGenerator = false;
#endif

#if defined(__cpp_lib_move_only_function)
constexpr bool kMoveOnlyFunction =
    requires { typename std::move_only_function<int()>; };
#else
constexpr bool kMoveOnlyFunction = false;
#endif

#if __has_include(<stacktrace>) && defined(__cpp_lib_stacktrace)
constexpr bool kStacktrace = requires { std::stacktrace::current(); };
#else
constexpr bool kStacktrace = false;
#endif

#if __has_include(<hive>) && defined(__cpp_lib_hive)
constexpr bool kHive = requires { typename std::hive<int>; };
#else
constexpr bool kHive = false;
#endif

#if __has_include(<inplace_vector>) && defined(__cpp_lib_inplace_vector)
constexpr bool kInplaceVector =
    requires { typename std::inplace_vector<int, 4>; };
#else
constexpr bool kInplaceVector = false;
#endif

#if __has_include(<mdspan>) && defined(__cpp_lib_submdspan)
constexpr bool kSubmdspan =
    requires(std::mdspan<int, std::extents<int, 2, 2>> matrix) {
        std::submdspan(matrix, 0, std::full_extent);
    };
#else
constexpr bool kSubmdspan = false;
#endif

#if __has_include(<execution>) && defined(__cpp_lib_senders)
constexpr bool kExecution = requires { std::execution::just(1); };
#else
constexpr bool kExecution = false;
#endif

#if defined(__cpp_contracts)
constexpr bool kContracts = true;
#else
constexpr bool kContracts = false;
#endif

#if defined(__cpp_reflection)
constexpr bool kReflection = true;
#else
constexpr bool kReflection = false;
#endif

constexpr std::array<Feature, 20> kFeatures{{
    {"std::flat_map", Group::cxx23_adapters,
     kFlatMap ? Status::available : Status::missing,
     "ordered baseline for Ch 5"},
    {"std::mdspan", Group::cxx23_adapters,
     kMdspan ? Status::available : Status::missing, "matrices and DP tables"},
    {"ranges::to", Group::cxx23_adapters,
     kRangesTo ? Status::available : Status::missing, "materialize views"},
    {"views::enumerate", Group::cxx23_adapters,
     kEnumerate ? Status::available : Status::missing, "indexed loops"},
    {"views::zip", Group::cxx23_adapters,
     kZip ? Status::available : Status::missing, "parallel iteration"},
    {"views::join_with", Group::cxx23_adapters,
     kJoinWith ? Status::available : Status::missing, "string assembly"},
    {"views::chunk", Group::cxx23_adapters,
     kChunk ? Status::available : Status::missing, "if missing, build in Ch 2"},
    {"views::slide", Group::cxx23_adapters,
     kSlide ? Status::available : Status::missing, "if missing, build in Ch 2"},
    {"ranges::fold_left", Group::cxx23_adapters,
     kFoldLeft ? Status::available : Status::missing, "range fold"},
    {"ranges::starts_with", Group::cxx23_adapters,
     kStartsWith ? Status::available : Status::missing, "range prefix test"},
    {"views::cartesian_product", Group::cxx23_adapters,
     kCartesian ? Status::available : Status::missing,
     "if missing, build in Ch 2"},
    {"std::generator", Group::cxx23_facilities,
     kGenerator ? Status::available : Status::missing,
     "if missing, build in Ch 3"},
    {"std::move_only_function", Group::cxx23_facilities,
     kMoveOnlyFunction ? Status::available : Status::missing,
     "std::function fallback"},
    {"std::stacktrace", Group::cxx23_facilities,
     kStacktrace ? Status::available : Status::missing,
     "not used by the course"},
    {"std::hive", Group::cxx26_preview,
     kHive ? Status::available : Status::missing, "C++26 preview"},
    {"std::inplace_vector", Group::cxx26_preview,
     kInplaceVector ? Status::available : Status::missing, "C++26 preview"},
    {"std::submdspan", Group::cxx26_preview,
     kSubmdspan ? Status::available : Status::missing, "C++26 preview"},
    {"std::execution::just", Group::cxx26_preview,
     kExecution ? Status::available : Status::missing, "C++26 preview"},
    {"contracts", Group::cxx26_preview,
     kContracts ? Status::available : Status::missing, "C++26 preview"},
    {"reflection", Group::cxx26_preview,
     kReflection ? Status::available : Status::missing, "C++26 preview"},
}};

} // namespace

std::span<const Feature> all() noexcept { return kFeatures; }

std::string_view group_name(Group group) noexcept {
    switch (group) {
    case Group::cxx23_adapters:
        return "C++23 adapters";
    case Group::cxx23_facilities:
        return "C++23 facilities";
    case Group::cxx26_preview:
        return "C++26 preview";
    }
    std::unreachable();
}

std::string_view status_name(Status status) noexcept {
    switch (status) {
    case Status::available:
        return "PASS";
    case Status::missing:
        return "MISSING";
    }
    std::unreachable();
}

} // namespace atlas::features
src/dot.cpp
#include "atlas/dot.hpp"

#include <format>
#include <string>
#include <string_view>

namespace atlas::dot {
namespace {

std::string quoted(std::string_view text) {
    return std::format("\"{}\"", escape(text));
}

} // namespace

std::string escape(std::string_view text) {
    std::string out;
    out.reserve(text.size());
    for (const char c : text) {
        switch (c) {
        case '"':
            out += "\\\"";
            break;
        case '\\':
            out += "\\\\";
            break;
        case '\n':
            out += "\\n";
            break;
        default:
            out += c;
            break;
        }
    }
    return out;
}

std::string to_dot(const Graph& graph) {
    const std::string_view kind = graph.directed ? "digraph" : "graph";
    std::string out = std::format("{} {} {{\n", kind, quoted(graph.name));
    for (const Node& node : graph.nodes) {
        out += std::format("    {} [label={}", quoted(node.id),
                           quoted(node.label));
        if (!node.color.empty()) {
            out += std::format(", color={}", quoted(node.color));
        }
        out += "];\n";
    }
    for (const Edge& edge : graph.edges) {
        const std::string_view arrow = graph.directed ? "->" : "--";
        out += std::format("    {} {} {}", quoted(edge.from), arrow,
                           quoted(edge.to));
        if (!edge.label.empty()) {
            out += std::format(" [label={}]", quoted(edge.label));
        }
        out += ";\n";
    }
    out += "}\n";
    return out;
}

} // namespace atlas::dot
apps/atlas/main.cpp
#include "atlas/dot.hpp"
#include "atlas/features.hpp"
#include "atlas/version.hpp"

#include <cstddef>
#include <cstdio>
#include <expected>
#include <format>
#include <print>
#include <span>
#include <string>
#include <string_view>
#include <utility>

namespace {

constexpr std::string_view kUsage =
    "usage: atlas <command>\n"
    "\n"
    "commands:\n"
    "  doctor    print the build environment and the feature matrix\n"
    "  features  print only the feature matrix\n"
    "  dot       print a small DOT graph (pipe it to a Graphviz viewer)\n"
    "  version   print the atlas version\n";

enum class Command { doctor, features, dot, version };

std::expected<Command, std::string> parse_command(std::string_view text) {
    if (text == "doctor") {
        return Command::doctor;
    }
    if (text == "features") {
        return Command::features;
    }
    if (text == "dot") {
        return Command::dot;
    }
    if (text == "version") {
        return Command::version;
    }
    return std::unexpected(std::format("unknown command '{}'", text));
}

std::string_view compiler_name() {
#if defined(__clang__)
    return "Clang";
#elif defined(__GNUC__)
    return "GCC";
#elif defined(_MSC_VER)
    return "MSVC";
#else
    return "unknown";
#endif
}

std::string compiler_version() {
#if defined(__clang__)
    std::string version{__clang_version__};
    while (!version.empty() && version.back() == ' ') {
        version.pop_back();
    }
    return version;
#elif defined(__GNUC__)
    return std::string{__VERSION__};
#else
    return "unknown";
#endif
}

std::string_view platform_name() {
#if defined(__APPLE__) && defined(__aarch64__)
    return "macOS arm64";
#elif defined(__APPLE__)
    return "macOS x86_64";
#elif defined(__linux__) && defined(__aarch64__)
    return "Linux arm64";
#elif defined(__linux__)
    return "Linux x86_64";
#elif defined(_WIN32)
    return "Windows";
#else
    return "unknown";
#endif
}

std::string_view build_type() {
#ifdef NDEBUG
    return "Release";
#else
    return "Debug";
#endif
}

std::string standard_name() {
    return std::format("C++{} ({})", __cplusplus / 100 % 100, __cplusplus);
}

void print_features() {
    for (const auto group : {atlas::features::Group::cxx23_adapters,
                             atlas::features::Group::cxx23_facilities,
                             atlas::features::Group::cxx26_preview}) {
        std::println("\n{}", atlas::features::group_name(group));
        for (const auto& feature : atlas::features::all()) {
            if (feature.group != group) {
                continue;
            }
            std::println("  {:<24} {:<7} {}", feature.name,
                         atlas::features::status_name(feature.status),
                         feature.note);
        }
    }
}

void doctor() {
    std::println("{} {} — {}", atlas::kName, atlas::kVersion, atlas::kTagline);
    std::println("build      {}, {}", build_type(), standard_name());
    std::println("compiler   {} {}", compiler_name(), compiler_version());
    std::println("platform   {}", platform_name());
    std::println("");
    std::println("The build itself proves the required features:");
    std::println("  std::print, std::format, std::expected, std::span,");
    std::println("  std::string_view, std::ranges, std::chrono");
    print_features();
}

void dot_demo() {
    atlas::dot::Graph graph;
    graph.name = "atlas";
    graph.nodes = {
        {"root", "atlas", "black"},
        {"trees", "trees", "darkgreen"},
        {"graphs", "graphs", "navy"},
    };
    graph.edges = {
        {"root", "trees", "left"},
        {"root", "graphs", "right"},
    };
    std::print("{}", atlas::dot::to_dot(graph));
}

} // namespace

int main(int argc, char** argv) {
    const std::span<char*> args{argv, static_cast<std::size_t>(argc)};
    if (args.size() < 2U) {
        std::print("{}", kUsage);
        return 2;
    }

    const auto command = parse_command(std::string_view{args[1]});
    if (!command) {
        std::println(stderr, "atlas: {}", command.error());
        std::print(stderr, "{}", kUsage);
        return 2;
    }

    switch (*command) {
    case Command::doctor:
        doctor();
        break;
    case Command::features:
        print_features();
        break;
    case Command::dot:
        dot_demo();
        break;
    case Command::version:
        std::println("{} {}", atlas::kName, atlas::kVersion);
        break;
    }
    return 0;
}
tests/test_main.cpp
#include "atlas/dot.hpp"
#include "atlas/features.hpp"
#include "atlas/version.hpp"

#include <print>
#include <string>
#include <string_view>

namespace {

struct TestState {
    int failures = 0;
};

void check(TestState& state, bool condition, std::string_view expression,
           int line) {
    if (condition) {
        return;
    }
    std::println(stderr, "FAIL line {}: {}", line, expression);
    ++state.failures;
}

#define CHECK(expr) check(state, (expr), #expr, __LINE__)

} // namespace

int main() {
    TestState state;
    CHECK(!atlas::kName.empty());
    CHECK(atlas::kVersion == "0.1.0");
    CHECK(atlas::features::all().size() > 10);
    CHECK(atlas::features::all().front().name == "std::flat_map");

    CHECK(atlas::dot::escape("plain") == "plain");
    CHECK(atlas::dot::escape("a\"b") == "a\\\"b");
    CHECK(atlas::dot::escape("a\\b") == "a\\\\b");
    CHECK(atlas::dot::escape("a\nb") == "a\\nb");

    atlas::dot::Graph graph;
    graph.name = "test";
    graph.nodes = {{"a", "A", ""}, {"b", "B", ""}};
    graph.edges = {{"a", "b", ""}};
    const std::string dot_text = atlas::dot::to_dot(graph);
    CHECK(dot_text.contains("digraph \"test\""));
    CHECK(dot_text.contains("\"a\" -> \"b\""));

    if (state.failures != 0) {
        std::println(stderr, "{} test(s) failed", state.failures);
        return 1;
    }
    std::println("all tests passed");
    return 0;
}
bench/bench_main.cpp
#include <chrono>
#include <cstddef>
#include <list>
#include <numeric>
#include <print>
#include <string_view>
#include <utility>
#include <vector>

namespace {

using Clock = std::chrono::steady_clock;

template <class Fn> double measure_ms(Fn&& fn) {
    const auto start = Clock::now();
    std::forward<Fn>(fn)();
    const auto stop = Clock::now();
    return std::chrono::duration<double, std::milli>(stop - start).count();
}

void bench_vector(const std::vector<int>& values, volatile long long& sink) {
    long long sum = 0;
    for (const int value : values) {
        sum += value;
    }
    sink = sum;
}

void bench_list(const std::list<int>& values, volatile long long& sink) {
    long long sum = 0;
    for (const int value : values) {
        sum += value;
    }
    sink = sum;
}

} // namespace

int main(int argc, char** argv) {
    const bool csv = argc > 1 && std::string_view{argv[1]} == "--csv";
    constexpr std::size_t kCount = 1'000'000;

    std::vector<int> vector_values(kCount);
    std::iota(vector_values.begin(), vector_values.end(), 0);
    const std::list<int> list_values(vector_values.begin(),
                                     vector_values.end());

    volatile long long sink = 0;
    const double vector_ms =
        measure_ms([&] { bench_vector(vector_values, sink); });
    const double list_ms = measure_ms([&] { bench_list(list_values, sink); });
    const long long final_sink = sink;

    if (csv) {
        std::println("benchmark,ms");
        std::println("vector_traverse,{:.3f}", vector_ms);
        std::println("list_traverse,{:.3f}", list_ms);
    } else {
        std::println("{:<16} {:>10}", "benchmark", "ms");
        std::println("{:<16} {:>10.3f}", "vector_traverse", vector_ms);
        std::println("{:<16} {:>10.3f}", "list_traverse", list_ms);
    }
    std::println("sink {}", final_sink);
    return 0;
}

10. Build and run

From the repository root:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_CXX_FLAGS="-Werror"
cmake --build build
ctest --test-dir build --output-on-failure
./build/atlas doctor
./build/atlas dot
./build/atlas_bench --csv

The -Werror flag is not a punishment. It turns warnings into a conversation: -Wconversion and -Wsign-conversion will tell you every place where a value narrows or a signed value crosses into unsigned territory. On a data-structure course, those are often real bugs, not noise. Fix the conversion, or make it explicit with a cast and a comment; do not turn the warning off.

The build produces three executables and one static library in build/: atlas, atlas_tests, atlas_bench, and libatlas_core.a. The library is the part that later chapters will grow.

11. Checkpoint

Run the chapter's verification script:

cd references/verify/ch01
./run.sh

It builds Release with -Werror, runs the tests, checks the doctor matrix, checks the DOT output and the benchmark CSV, builds and runs the ASan/UBSan configuration, and runs clang-format and clang-tidy. The last line should be:

CH01 OK

You can also check the chapter's complete listings against the verified source tree:

python3 references/scripts/check_listing.py \
    tutorial/part-1/01-workbench.md references/verify/ch01

Every file should print ok.

Before moving on, make sure you can answer these without looking:

  • What are the five questions, and which one does the test harness answer?
  • Why does parse_command return std::expected<Command, std::string> instead of a Command with a special "invalid" value?
  • Why must a requires expression be guarded by a feature-test macro before it names a missing feature?
  • Why is fold_left reported as PASS here, and what was wrong with the first probe?
  • What does the DOT renderer guarantee, and why does that make it testable?
  • Why is the volatile sink necessary in the benchmark?

12. When it does not work

Symptom Likely cause Check
fatal error: 'print' file not found The build used Apple clang instead of Homebrew LLVM clang++ --version; the CMake block that forces /opt/homebrew/opt/llvm/bin/clang++
no member named 'chunk' inside a requires expression The probe was not gated by __cpp_lib_ranges_chunk Wrap the probe in #if defined(...); see §5
no member named 'fold_left' <algorithm> was not included, or the macro gate is missing Add <algorithm>; confirm __cpp_lib_ranges_fold
-Wsign-conversion on argc std::span wants a std::size_t size static_cast<std::size_t>(argc)
DOT viewer shows one node or a syntax error A label contains an unescaped quote or backslash Run dot -Tsvg; check escape and the test cases
ctest finds no tests enable_testing() or add_test is missing The last three lines of CMakeLists.txt
Benchmark numbers jump around One short run on a busy machine Run it several times; Chapter 2 builds a proper method
clang-tidy reports warnings A real issue, or a check that does not fit this code Read the warning; fix it or record the suppression in .clang-tidy
atlas: unknown command 'foo' The parser worked The message and exit code 2 are the contract

13. Exercises

Comprehension. Explain, in your own words, why __cpp_lib_ranges_chunk has to gate the requires expression. What exactly does the compiler do when it sees std::views::chunk and the name does not exist? Then explain the fold_left story: why did the first probe fail even though the feature-test macro was defined?

Modification. Add atlas features --json. It should print the feature matrix as a JSON array of objects with name, group, status, and note. Keep the existing text output unchanged. Hints: you already have the records; JSON needs escaping for quotes and backslashes; do not add a JSON library for this.

Stretch. Add atlas dot --undirected and atlas dot --label <text>. The undirected graph must use -- instead of ->; the label must be escaped. Add a test for both. Then make atlas doctor print the compiler's target triple (hint: __VERSION__ is not enough; look for the Clang macro that reports the target).

14. Recap

  • The workbench is a library (atlas_core) plus four clients: the CLI, the tests, the benchmark, and the DOT writer.
  • The five-question lens — contract, invariant, representation, cost, evidence — is the book's method, and it works on the workbench itself.
  • std::expected makes errors part of the return type; std::span lends a view without owning; std::print and std::format replace iostream state.
  • Feature detection has two gates: a macro or header gate, then a requires gate. A missing name in a requires expression is a hard error.
  • A failed probe can be your bug. fold_left was in <algorithm>, not <ranges>.
  • This toolchain has most of C++23, but not std::generator, views::chunk, views::slide, views::cartesian_product, std::move_only_function, or std::stacktrace; the course adapts rather than pretending.
  • DOT is the first visualization tier: text, deterministic, testable, and layout-free.
  • Tests and benchmarks are part of the structure, not an afterthought.

15. What's next

Chapter 2, Counting Steps, turns the benchmark harness into an instrument. You will count operations instead of guessing, fit growth curves, learn why an amortized cost is not an average, and implement the C++23 range algorithms this library is missing. Then Chapter 3 builds the first tree.