- name
- loom-cpp
- description
- C++ language expertise for idiomatic, production-quality code.
- triggers
- ["cpp","c++","cmake","ctest","googletest","gtest","catch2","vcpkg","conan","clang-tidy","clang-format","raii","unique_ptr","shared_ptr","move semantics","std::span","string_view","concepts","constexpr","undefined behavior","undefined behaviour","sanitizer","asan","ubsan"]
# C++ Language Expertise
## Overview
Idiomatic, production-quality C++20/23 for an engineer who already knows the language. The expensive C++ mistakes compile cleanly: dangling views and references, undefined behaviour the optimizer exploits, resources nobody owns, and CMake targets that leak flags into each other. This skill is decision rules and traps. Check a compiler's support table (cppreference "compiler support") before relying on a C++23 library feature.
## Tooling
CMake describes the build, Ninja or Make runs it, CTest runs the tests. Keep the repository's generator, presets and package manager.
| Task | Command |
| --- | --- |
| Configure (out of source) | `cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` |
| Configure from a preset | `cmake --preset debug` (`CMakePresets.json`, checked in) |
| Build | `cmake --build build -j` |
| Test | `ctest --test-dir build --output-on-failure -j 8` (`--test-dir` needs CMake 3.20+) |
| List registered tests | `ctest --test-dir build -N` |
| Format | `clang-format -i <files>`; CI: `clang-format --dry-run --Werror <files>` |
| Lint | `run-clang-tidy -p build` (reads `compile_commands.json` and `.clang-tidy`) |
| Add a dependency | `vcpkg add port fmt` (manifest mode, `vcpkg.json`); Conan 2: the recipe plus `conan install . --build=missing` |
- vcpkg (toolchain `-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake`) and Conan 2 (`CMakeToolchain` generator) both hand CMake `find_package(... CONFIG REQUIRED)` targets. `FetchContent` downloads sources at configure time and needs network access then, which sandboxed sessions may lack; fetch test frameworks through the package manager or the system.
- clang-tidy: enable `bugprone-*`, `modernize-*`, `performance-*`, a pruned `cppcoreguidelines-*`, `misc-const-correctness`; set `WarningsAsErrors: '*'` in `.clang-tidy` so findings fail the gate. It must see the build's flags, which `compile_commands.json` provides.
- A pip-installed `cmake` shim earlier on `PATH` can shadow the real CMake suite (its `ctest` fails with `ModuleNotFoundError`). `command -v ctest` shows which binary runs.
- Multi-config generators (Visual Studio, Ninja Multi-Config) ignore `CMAKE_BUILD_TYPE`: pass `--config Debug` to `cmake --build` and `-C Debug` to `ctest`.
Target-based CMake: every setting belongs to a target, scoped `PRIVATE` (this target's build), `PUBLIC` (this target and its consumers) or `INTERFACE` (consumers only).
```cmake
cmake_minimum_required(VERSION 3.25)
project(spool LANGUAGES CXX)
add_library(spool src/spool_writer.cpp src/journal.cpp)
target_include_directories(spool PUBLIC include)
target_compile_features(spool PUBLIC cxx_std_20)
target_compile_options(spool PRIVATE
"$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall;-Wextra;-Wpedantic;-Wconversion;-Wshadow;-Werror>"
"$<$<CXX_COMPILER_ID:MSVC>:/W4;/WX;/permissive->")
include(CTest) # enable_testing() plus the BUILD_TESTING option
if(BUILD_TESTING)
find_package(GTest CONFIG REQUIRED)
add_executable(spool_tests tests/spool_writer_test.cpp)
target_link_libraries(spool_tests PRIVATE spool GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(spool_tests) # one CTest test per TEST(), named Suite.Name
endif()
```
- A generator expression holding several flags is quoted and `;`-separated. Unquoted spaces split it into broken arguments.
- `-Werror` goes on your own targets. A global `CMAKE_CXX_FLAGS` edit also applies it to third-party code built in the tree and breaks on the next compiler upgrade.
The gate:
```bash
clang-format --dry-run --Werror $(git ls-files '*.cpp' '*.hpp') \
&& cmake --build build \
&& run-clang-tidy -p build -quiet \
&& ctest --test-dir build --output-on-failure
```
## Modern C++ (20/23)
- **Concepts** replace SFINAE: constraints read as requirements and produce short errors.
```cpp
template <typename T>
concept Journal = requires(T j, std::span<const std::byte> bytes) {
{ j.append(bytes) } -> std::same_as<std::size_t>;
j.flush();
};
void drain(Journal auto& journal, std::span<const Record> records);
```
- `auto operator<=>(const T&) const = default;` generates all six comparisons memberwise; a defaulted `operator==` alone gives `==` and `!=`.
- `std::format` (C++20) and `std::print`/`std::println` (C++23) check format strings at compile time; they replace `printf` and `iostream` chains.
- `std::expected<T, E>` (C++23) carries recoverable errors in the return value; `std::optional`'s `and_then`/`transform`/`or_else` (C++23) chain without nested `if`s.
- Ranges and views are lazy and composable: `records | std::views::filter(is_live) | std::views::transform(&Record::id)`. Views refer to their source (see Lifetimes). `filter_view` caches `begin()`, so it cannot be iterated through a `const&`, and modifying an element so it stops satisfying the predicate during iteration is undefined.
- Designated initializers `Options{.root = dir, .max_bytes = 1 << 20}` (declaration order only); `consteval` functions and `constexpr` containers move checks to compile time; `std::bit_cast`, `std::to_underlying` (C++23), `std::unreachable` (C++23).
- Deducing `this` (C++23 explicit object parameter) removes const/non-const overload pairs and CRTP boilerplate.
- Modules build with CMake 3.28+ and Ninja, and `import std;` is still experimental in CMake; headers remain the portable choice for libraries.
- Guard optional library features with feature-test macros: `#if __cpp_lib_expected >= 202202L`.
## Ownership and RAII
Every resource (memory, file, socket, lock, OS handle) has exactly one owning object whose destructor releases it.
| Need | Use |
| --- | --- |
| Sole owner | a value member, or `std::unique_ptr<T>` from `std::make_unique` |
| Shared ownership, lifetime unknown until runtime | `std::shared_ptr<T>` from `std::make_shared` |
| Observe a `shared_ptr` without extending it; break cycles | `std::weak_ptr<T>` |
| Non-owning access | `T&`, `const T&`, `T*` (nullable), `std::span<T>`, `std::string_view` |
| C handle | `std::unique_ptr<Handle, Deleter>` |
```cpp
struct FileCloser {
void operator()(std::FILE* f) const noexcept { std::fclose(f); }
};
using File = std::unique_ptr<std::FILE, FileCloser>;
File open_journal(const std::filesystem::path& p) {
File f{std::fopen(p.string().c_str(), "ab")};
if (!f) throw std::system_error(errno, std::generic_category(), p.string());
return f;
}
class SpoolWriter { // rule of zero: every member manages itself
public:
explicit SpoolWriter(std::filesystem::path root) : root_(std::move(root)) {}
void add_sink(std::unique_ptr<Sink> sink) { sinks_.push_back(std::move(sink)); } // ownership moves in
private:
std::filesystem::path root_;
std::vector<std::unique_ptr<Sink>> sinks_;
};
```
- **Rule of zero:** compose from members that manage themselves and declare none of the five special members. A class that manages a resource directly declares all five (destructor, copy and move constructors, copy and move assignment) or `= delete`s the ones it cannot support.
- Parameters: read-only `const T&` (by value for cheap types, `std::string_view`, `std::span`); sink parameters by value, then `std::move` into place; in/out `T&`; ownership transfer `std::unique_ptr<T>` by value. A `shared_ptr` by value on every call costs two atomic operations and says nothing about ownership; pass `const T&` unless the callee keeps a copy.
- Mark move constructors and move assignment `noexcept`. `std::vector` reallocation copies elements whose move can throw (`std::move_if_noexcept`), so growth quietly becomes deep copies.
- A moved-from object is valid but unspecified: assign to it or destroy it, never read it.
- `std::move` on a `const` object selects the copy constructor without a diagnostic. `return std::move(local);` blocks copy elision (`-Wpessimizing-move`); return the local by name.
- `make_shared` puts the object and control block in one allocation, so a live `weak_ptr` keeps that memory allocated until the last weak reference is gone. `shared_from_this()` in a constructor throws `std::bad_weak_ptr`: no `shared_ptr` owns the object yet.
- Destructors are implicitly `noexcept`; an exception escaping one calls `std::terminate`.
## Lifetimes: `string_view`, `span` and Dangling References
`std::string_view` and `std::span` are a pointer and a length. They never own and never extend the lifetime of what they point at. All of these compile:
```cpp
std::string_view name = std::string("spool-") + id; // BAD: the temporary string dies at the ;
std::string_view make_label() {
std::string s = build();
return s; // BAD: view into a destroyed local
}
struct Entry { std::string_view key; };
Entry e{config.get("key")}; // BAD if get() returns std::string by value
std::vector<int> v = load();
std::span<int> head = std::span(v).first(3);
v.push_back(4); // may reallocate: head now dangles
for (auto& r : fetch_batch().records()) { } // BAD before C++23: the batch dies before the loop body
```
- Members and anything that outlives the call hold owning types (`std::string`, `std::vector`). Views are for parameters and locals whose source visibly outlives them.
- `string_view::data()` is not null-terminated. Passing it to a C API that expects a C string reads past the end; build a `std::string` first.
- Invalidation: a `vector` or `string` insertion that reallocates invalidates every iterator, reference and span into it; `unordered_map` rehashing invalidates iterators while references stay valid; erase during iteration uses the returned iterator or `std::erase_if` (C++20).
- A lambda capturing by reference (`[&]`) that outlives its scope (stored callback, thread, coroutine) dangles; capture by value or move owned state in (`[state = std::move(state)]`).
- A coroutine parameter taken by reference dangles once the caller's argument is destroyed while the coroutine is suspended. Coroutines take parameters by value.
- C++23 (P2718) extends every temporary in a range-`for` initializer to the end of the loop; older standards and compilers still dangle, so bind the owner to a named variable first.
- Tooling: GCC 13+ `-Wdangling-reference`; Clang `-Wdangling`, `-Wdangling-gsl` and `[[clang::lifetimebound]]` on accessor parameters; AddressSanitizer catches the rest at runtime (`ASAN_OPTIONS=detect_stack_use_after_return=1`).
## Undefined Behaviour and Sanitizers
Undefined behaviour (UB) is a rule violation the compiler assumes never happens. Optimizers delete checks that could only fail through UB (`if (x + 1 < x)` for signed `x` folds to `false`), so UB surfaces far from its cause and often only in release builds.
Common sources:
- signed integer overflow; shifting by a negative count or by the type's width or more; `INT_MIN / -1`
- out-of-bounds access (`operator[]` does not check), use after free, double free, dereferencing null or an end iterator
- reading an uninitialized variable; a non-`void` function falling off its end
- data races: two threads, at least one writing, no synchronization
- type punning through a pointer cast (strict aliasing); use `std::memcpy` or `std::bit_cast`
- modifying a `const` object or a string literal; misaligned access through a cast pointer
- a loop with no observable side effects that never terminates
Sanitizers instrument the build and report UB at runtime. Run the test suite under them in CI:
| Sanitizer | Flags | Finds | Notes |
| --- | --- | --- | --- |
| AddressSanitizer | `-fsanitize=address -fno-omit-frame-pointer` | out-of-bounds, use after free, leaks (LeakSanitizer on Linux) | about 2x slower; combine with UBSan |
| UndefinedBehaviorSanitizer | `-fsanitize=undefined -fno-sanitize-recover=all` | overflow, bad shifts, null, misalignment, invalid `bool`/enum loads | without `-fno-sanitize-recover` it prints and continues, and the test still passes |
| ThreadSanitizer | `-fsanitize=thread` | data races, lock-order inversions | separate build; cannot combine with ASan |
| MemorySanitizer | `-fsanitize=memory` (Clang only) | uninitialized reads | every linked library, the standard library included, must be instrumented |
```cmake
option(SPOOL_SANITIZE "Build with AddressSanitizer and UBSan" OFF)
if(SPOOL_SANITIZE) # before any add_library/add_executable: only later targets pick it up
add_compile_options(-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer)
add_link_options(-fsanitize=address,undefined)
endif()
```
- `-D_GLIBCXX_ASSERTIONS` (libstdc++) or `-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST` (libc++ 18+) adds bounds checks to `operator[]`, `front()` and similar at small cost, cheap enough for many production builds. GCC 14's `-fhardened` bundles `_GLIBCXX_ASSERTIONS` with `_FORTIFY_SOURCE=3`, the stack protector and PIE.
- Fuzz parsers and decoders with libFuzzer (`-fsanitize=fuzzer,address`).
- Optimization level changes which UB shows. Run sanitizer builds at `-O1` or higher as well as at `-O0`.
## Concurrency
- `std::jthread` (C++20) joins in its destructor and carries a `std::stop_token`. Destroying a joinable `std::thread` calls `std::terminate`.
- `std::scoped_lock` locks several mutexes with deadlock-free ordering; `std::unique_lock` when a `condition_variable` needs one. Wait with a predicate, which handles spurious wakeups: `cv.wait(lock, [&] { return !queue.empty(); });`.
- `std::atomic<T>` defaults to `memory_order_seq_cst`. A weaker order (`acquire`, `release`, `relaxed`) needs a written ordering argument, checked under ThreadSanitizer.
Auf GitHub ansehen