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/andbench/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:
__has_include(<header>)asks whether a header exists.- A feature-test macro such as
__cpp_lib_flat_mapasks whether the library claims a feature. - A
requiresexpression 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:
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::generatoris missing, so Chapter 3 builds a lazy in-order range by hand — a custom iterator and astd::ranges-compatible view — and offers a coroutine-based generator as a bonus. That is better pedagogy thanco_yieldmagic, and it teaches the iterator machinery properly.views::chunk,views::slide, andviews::cartesian_productare missing, so Chapter 2 implements two of them as exercises. You learn how views work by filling the holes.std::move_only_functionis missing; visitors will use templates orstd::function, and the difference is discussed when it matters.std::stacktraceis 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, andstd::submdspanappear 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¶
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)
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 80
DerivePointerAlignment: false
PointerAlignment: Left
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
#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
#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
#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
#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
#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
#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;
}
#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;
}
#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:
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:
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_commandreturnstd::expected<Command, std::string>instead of aCommandwith a special "invalid" value? - Why must a
requiresexpression be guarded by a feature-test macro before it names a missing feature? - Why is
fold_leftreported asPASShere, and what was wrong with the first probe? - What does the DOT renderer guarantee, and why does that make it testable?
- Why is the
volatilesink 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::expectedmakes errors part of the return type;std::spanlends a view without owning;std::printandstd::formatreplace iostream state.- Feature detection has two gates: a macro or header gate, then a
requiresgate. A missing name in arequiresexpression is a hard error. - A failed probe can be your bug.
fold_leftwas 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, orstd::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.