Skip to content

Appendix F — The Rosetta Cookbook

Appendix F — The Rosetta Cookbook

Parts I–VI teach the language. This appendix serves a different moment: mid-task, the C# name already in your head — or the Java name, middle column — and fifteen seconds to spend. Find the thing you are reaching for; the recipe gives the C++ spelling, says why it looks that way, and names the trap that costs an afternoon. The why paragraphs cross-reference the chapter that owns each concept rather than re-teaching it — this page is for looking up, not for reading.

Every listing compiles, runs, and holds under the canonical flags — all but one, which is C++23; that one, the two that need libcrypto, the one that needs libcurl and the one that needs sqlite3 build behind probes, and say so where they appear. Each recipe carries a Rust tab beside its C++ one, and that tab has two shapes: code, where Rust's standard library answers the question, and a single line naming the crate that does, where it has no answer at all — no JSON, no calendar, no cryptography, no HTTP, no SQLite, no shared memory, no memory mapping. Fifteen of the fifty-four read that way, and the line says which crate and why there is nothing to show; the crate under exercises/cookbook/rust/ takes no dependency, which is what makes the tested half tested. The recipes live as code in exercises/cookbook/, and scripts/build_all.sh asserts what each one claims on every push. Recipe numbers are stable — recipes append and are never renumbered — so a note that says "Recipe 7" stays right.

Reaching for... ...or, in Java The recipe
File.ReadAllText Files.readString Recipe 1 — Read a whole file into a string
string.Split String.split Recipe 2 — Split a string
string.Join String.join Recipe 3 — Join strings
StringBuilder StringBuilder (same name) Recipe 4 — Build a string in a loop
string.Format / $"..." String.format Recipe 5 — Format values into a string
Stopwatch System.nanoTime Recipe 6 — Time a call
using / IDisposable try-with-resources Recipe 7 — Wrap a C handle so it frees itself
TryGetValue Map.get / getOrDefault Recipe 8 — Look up a key without inserting it
File.WriteAllText Files.writeString Recipe 9 — Write a string to a file
Path.Combine Path.of / resolve Recipe 10 — Build a path from pieces
File.Exists / Directory.Exists Files.exists / isRegularFile Recipe 11 — Check that a file or directory exists
Directory.GetFiles Files.list Recipe 12 — List the files in a directory
Task.Run / await ExecutorService + Future.get Recipe 13 — Run work on another thread and wait for it
event / EventHandler addXxxListener Recipe 14 — Expose an event
Console.WriteLine / Console.Error System.out / System.err Recipe 15 — Print a diagnostic you will actually see
System.Timers.Timer / Task.Delay ScheduledExecutorService Recipe 16 — Run something every interval
Encoding.UTF8.GetString / GetBytes getBytes(UTF_8) / new String(bytes, UTF_8) Recipe 17 — Convert between UTF-8 and UTF-16
list.IndexOf / Contains / str.Contains indexOf / contains Recipe 18 — Find an element, an index, or a substring
int? / ?? / ?. Optional<T> / orElse / map Recipe 19 — Carry a value that may be absent
pattern-matching switch on a type sealed interfaces + switch Recipe 20 — Switch on the kind of a message
class ParseException : Exception class ParseException extends Exception Recipe 21 — Throw and catch your own exception type
int.TryParse with a reason / a Result<T> from a library Optional / Either from a library Recipe 22 — Return a value or an error
string.IsNullOrEmpty / s ?? "" s == null \|\| s.isEmpty() Recipe 23 — Test for an empty string, and for no string at all
[Conditional("DEBUG")] / #if DEBUG assert (with -ea) Recipe 24 — Compile a diagnostic out of Release
JsonSerializer.Serialize Jackson writeValueAsString Recipe 25 — Serialize a record to JSON
JsonSerializer.Deserialize<T> Jackson readValue Recipe 26 — Read a JSON config with defaults
new List<T>(capacity) / new T[n] new ArrayList<>(n) / new int[n] Recipe 27 — Pre-size a collection
Stopwatch in a finally / timing a delegate try/finally + System.nanoTime Recipe 28 — Time a block on every exit, and a call for its result
DateTime.UtcNow.ToString("o") Instant.now() / ISO_INSTANT Recipe 29 — Stamp a log line with the time
TimeSpan.FromSeconds(2) handed to a native call Duration.ofSeconds(2) / toMillis() Recipe 30 — Pass a timeout to a C API
IConfiguration read at startup / IsEnabledAsync System.getenv at startup Recipe 31 — Read a feature flag once
[Flags] enum + HasFlag EnumSet.of / contains Recipe 32 — Combine flags as an enum class
a field of class type + IDisposable a field + AutoCloseable Recipe 33 — Hold an owned object as a field
new BigThing() — always on the heap new BigThing() — always on the heap Recipe 34 — An object too big for the stack
EnumerateObject / TryGetProperty / EnumerateArray Jackson JsonNodefields() / has / elements() Recipe 35 — Walk a JSON document you do not own
SHA256.HashData / Convert.ToHexString MessageDigest.getInstance("SHA-256") Recipe 36 — Hash bytes
AesGcm.Encrypt / Decrypt Cipher.getInstance("AES/GCM/NoPadding") Recipe 37 — Seal bytes for a reader in C#
File.Replace / write-then-move by hand Files.move(..., ATOMIC_MOVE) Recipe 38 — Save a file without losing the old one
Directory.CreateDirectory / File.Copy / File.Move / Directory.Delete(recursive) Files.createDirectories / copy / move / walkFileTree Recipe 39 — Create, copy, move and delete, and a whole tree
FileSystemWatcher WatchService Recipe 40 — Notice a file changed
HttpClient.GetStringAsync HttpClient.send Recipe 41 — Call an HTTP endpoint
SqliteConnection / SqliteCommand.ExecuteReader DriverManager.getConnection / PreparedStatement Recipe 42 — Open a local database and run a query
MemoryMappedFile.CreateOrOpen / CreateViewAccessor FileChannel.map (files only) Recipe 43 — Share a buffer with another process
Regex.IsMatch / Match(...).Groups[1] / Regex.Replace Pattern.compile / Matcher.group(1) / replaceAll Recipe 44 — Match a pattern
Trim / Equals(OrdinalIgnoreCase) / StartsWith / EndsWith strip / equalsIgnoreCase / startsWith / endsWith Recipe 45 — Trim, compare ignoring case, prefix and suffix
PostAsJsonAsync / ReadFromJsonAsync<T> BodyPublishers.ofString(mapper.writeValueAsString(r)) / Jackson readValue Recipe 46 — Post a JSON body and read a JSON reply
Rfc2898DeriveBytes.Pbkdf2 / HKDF.DeriveKey SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") / KDF.getInstance("HKDF-SHA256") (JDK 25) Recipe 47 — Derive a key
HMACSHA256.HashData / CryptographicOperations.FixedTimeEquals Mac.getInstance("HmacSHA256") / MessageDigest.isEqual Recipe 48 — Sign and verify bytes
MemoryMappedFile.CreateFromFile / File.ReadAllBytes on a large file FileChannel.map(READ_ONLY) Recipe 49 — Read a large file without copying it
internal / a private static helper package-private Recipe 50 — Keep a helper out of every other file
[DoesNotReturn] / [Obsolete] / an analyzer attribute @Deprecated / @CheckReturnValue Recipe 51 — Tell the compiler what a function promises
Math.Round / Math.Floor / Math.Ceiling / (int)x Math.round / Math.floor / Math.ceil / (int) Recipe 52 — Round a number, and turn it into an integer
JsonConverter<T> + [JsonConverter(typeof(...))] a Jackson JsonSerializer<T> in a SimpleModule Recipe 53 — Serialize a type you do not own
Ed25519.SignData / VerifyData (.NET 10), or ECDsa / RSA.SignData Signature.getInstance("Ed25519") Recipe 54 — Sign so that the verifier cannot forge
LINQ Streams the collections index predates this page: the LINQ table of Chapter 11

The clocks, by name. The five things System gave you for time, and where each lands — the map the timing recipes teach one row at a time:

In C# In C++ Which recipe
Stopwatch two steady_clock::time_points and a duration 6, 28
TimeSpan std::chrono::duration — the unit is in the type, count() strips it 30
DateTime.UtcNow system_clock::time_point, the one clock with a calendar 29
DateTime.Now / DateTimeOffset / time zones C++20's zoned_time, where the standard library ships it; before it, localtime_r/localtime_s and an offset you read yourself none — 29's trap only warns
Task.Delay / Thread.Sleep std::this_thread::sleep_for(duration) 16

Recipe 1 — Read a whole file into a string

In C#: var text = File.ReadAllText(path);

The recipe:

std::string read_all_text(const std::filesystem::path& path) {
    std::ifstream in(path, std::ios::binary);
    if (!in) {
        throw std::runtime_error("cannot open: " + path.string());
    }
    std::ostringstream buffer;
    buffer << in.rdbuf();    // one streamed read; no line loop to get wrong
    return buffer.str();
}
pub fn read_all_text(path: &Path) -> std::io::Result<String> {
    std::fs::read_to_string(path)    // one call; a missing file is an Err, not an empty string
}

Why it looks like this. There is no File static class: the stream object is the open file, and Chapter 1 already taught you what that buys — the ifstream closes itself on every path out of the function, the throw included. rdbuf() hands the whole file to the string stream in one operation, the closest thing iostreams have to a one-liner. And std::ios::binary reads the bytes as they are; without it, Windows translates \r\n on the way through and the "same" file compares differently per platform. The parameter is a std::filesystem::path rather than a std::string, because on Windows a path is not made of char (Recipe 10) and the stream constructors have taken a path since C++17 — a string argument still converts. Needs <filesystem>, <fstream>, <sstream>, <stdexcept>. In Rust the same call is std::fs::read_to_string, and the trap below cannot happen: a missing file is an Err, not an empty string, and the ? at the call site is the check you would otherwise forget.

Trap: a stream that failed to open does not throw — every read on it quietly produces nothing, so without the if (!in) check a missing file becomes an empty string and no error. That check is the part File.ReadAllText did for you.

Recipe 2 — Split a string

In C#: var parts = text.Split(',');

The recipe:

std::vector<std::string> split(const std::string& text, char sep) {
    std::vector<std::string> parts;
    std::istringstream stream(text);
    std::string field;
    while (std::getline(stream, field, sep)) {
        parts.push_back(field);
    }
    return parts;
}
pub fn split(text: &str, sep: char) -> Vec<String> {
    text.split(sep).map(str::to_owned).collect()    // an iterator of &str; collect owns
}

Why it looks like this. std::string ships no Split, and this loop is the idiom the ecosystem converged on: getline's third argument makes any character the "line" ending, so the same function that reads lines from a file reads fields from a string stream. It keeps interior empty fields ("a,,b" gives three), which is what field-shaped data needs. The other spelling you will meet, stream >> word, splits on runs of any whitespace and drops empties — right for words, wrong for columns; choose the one you mean. Needs <sstream>, <vector>.

Trap: "a,b," splits into two fields here where C# gives three — getline never reports a field after the final separator, so a trailing delimiter is invisible; if the column count matters, validate it.

Recipe 3 — Join strings

In C#: var line = string.Join(", ", parts);

The recipe:

std::string join(const std::vector<std::string>& parts, const std::string& sep) {
    std::string result;
    for (const auto& part : parts) {
        if (!result.empty()) {
            result += sep;    // between elements only - never leading
        }
        result += part;
    }
    return result;
}
pub fn join(parts: &[String], sep: &str) -> String {
    parts.join(sep)    // between elements only - never leading
}

Why it looks like this. The guard clause is the whole trick: append the separator only once something is already there, and the fencepost problem never starts. Ten lines for what C# does in one feels like a step down — until you notice they are the same ten lines every time. Write it once per codebase; most codebases already have, so grep before adding yours.

Trap: result += sep + part; builds and destroys a temporary string every pass — the two += lines append in place and say the same thing (Recipe 4 is the why).

Recipe 4 — Build a string in a loop

In C#: var sb = new StringBuilder(); sb.Append(...);

The recipe:

std::string build_report(const std::vector<int>& values) {
    std::string out;
    // one allocation up front - the StringBuilder(capacity) constructor
    out.reserve(values.size() * 12);
    for (int value : values) {
        out += "value=";
        out += std::to_string(value);
        out += '\n';
    }
    return out;
}
pub fn build_report(values: &[i32]) -> String {
    use std::fmt::Write;
    // one allocation up front - the StringBuilder(capacity) constructor
    let mut out = String::with_capacity(values.len() * 12);
    for value in values {
        writeln!(out, "value={value}").unwrap();    // writing to a String cannot fail
    }
    out
}

Why it looks like this. std::string is the string builder. StringBuilder exists because C# strings are immutable, so += there re-creates the whole string every pass; here the string is your own mutable buffer (Chapter 2's value semantics), += appends in place with amortized growth, and reserve plays the capacity constructor. This is the rare reflex to unlearn outright: the class C# taught you to avoid in a loop is the right default in C++.

Trap: the C# tax comes back if you write out = out + piece — the assignment form re-creates the string every pass in any language; the appender is += (or .append()).

Recipe 5 — Format values into a string

In C#: var s = $"{count} samples, ratio {ratio:F2}";

The recipe:

std::string describe(int count, double ratio) {
    std::ostringstream out;
    out << count << " samples, ratio "
        << std::fixed << std::setprecision(2) << ratio;
    return out.str();
}

std::string describe_c(int count, double ratio) {
    char buffer[64];
    std::snprintf(buffer, sizeof buffer, "%d samples, ratio %.2f", count, ratio);
    return buffer;
}
pub fn describe(count: i32, ratio: f64) -> String {
    format!("{count} samples, ratio {ratio:.2}")    // one form; type-checked at compile time
}

Why it looks like this. The honest answer: C++17 has no interpolation. std::format, the true analogue, arrives in C++20 — and toolchains around SDK work are exactly the ones that lag. Until yours has it, these are the two dialects: the stream when you want the compiler checking types (std::fixed << std::setprecision(2) is :F2 spelled as stream state), and snprintf when printf specifiers are already the local language — around C SDKs they are, because that is how the C world logs, asserts, and documents (Chapter 16's shapes speak it natively). Needs <sstream> and <iomanip>, or <cstdio>.

Trap: std::snprintf never overruns, but it truncates silently — the return value is the length it wanted to write, and comparing that against the buffer size is the only way to notice the cut.

Recipe 6 — Time a call

In C#: var sw = Stopwatch.StartNew(); ... sw.ElapsedMilliseconds

The recipe:

void report_batch_time() {
    const auto start = std::chrono::steady_clock::now();
    run_the_batch();    // the code being timed
    const auto elapsed = std::chrono::steady_clock::now() - start;
    const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
    std::cout << ms.count() << " ms\n";
}
pub fn report_batch_time(run_the_batch: impl FnOnce()) -> Duration {
    let start = Instant::now();    // monotonic, like steady_clock - never the wall clock
    run_the_batch();               // the code being timed
    let elapsed = start.elapsed();
    println!("{} ms", elapsed.as_millis());
    elapsed
}

Why it looks like this. A stopwatch is two time points and a subtraction; steady_clock is the monotonic clock, which is what Stopwatch was underneath all along. The subtraction gives a typed duration rather than a bare number, and duration_cast makes the unit visible at the call site — where ElapsedMilliseconds buried it in a property name, here you choose it, and <chrono> will not let you mix units by accident. Needs <chrono>, <iostream>.

Trap: system_clock is the wall clock — NTP or the user can move it mid-measurement, backwards included. Intervals come from steady_clock; system_clock is for timestamps only.

Recipe 7 — Wrap a C handle so it frees itself

In C#: using var file = File.OpenWrite(path);

The recipe:

using FileHandle = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;

FileHandle open_file(const char* path, const char* mode) {
    return FileHandle(std::fopen(path, mode), &std::fclose);
}
pub struct FileHandle(*mut c_void);    // the raw C handle, owned

impl Drop for FileHandle {
    fn drop(&mut self) {
        unsafe { fclose(self.0) };    // the destructor: runs on every path out of scope
    }
}

pub fn open_file(path: &str, mode: &str) -> Option<FileHandle> {
    let (path, mode) = (CString::new(path).ok()?, CString::new(mode).ok()?);
    let raw = unsafe { fopen(path.as_ptr(), mode.as_ptr()) };
    if raw.is_null() { None } else { Some(FileHandle(raw)) }    // a failed open is None; no Drop runs
}

Why it looks like this. The most load-bearing three lines of the transition: the deleter is part of the pointer's type, so destruction calls fclose exactly once on every exit path — using, without needing a block. FILE* here stands for every handle a C API ever hands you: substitute the SDK's create/destroy pair and the recipe is unchanged (Chapter 16's Bestiary is a catalogue of exactly such pairs). unique_ptr never calls the deleter on null, so a failed fopen needs no special-casing — test the handle, like the C API taught you. When there is more to manage than one close — a callback registration, a paired init/deinit with state — graduate to the wrapper class of Chapter 18. Needs <memory>, <cstdio>. In Rust the wrapper is a struct with Drop — the destructor by another name — around the raw handle, and the C functions are declared with extern "C"; the unsafe blocks mark exactly the two lines that trust C, which is the boundary Chapter 39 draws by hand.

Trap: a plain std::unique_ptr<std::FILE> compiles happily and then calls delete on a pointer C code allocated — undefined behavior every time. The deleter must match the allocator, which is the whole reason it is part of the type.

Recipe 8 — Look up a key without inserting it

In C#: if (settings.TryGetValue("timeout", out var value))

The recipe:

void apply_timeout_setting(const std::map<std::string, int>& settings) {
    const auto it = settings.find("timeout");
    if (it != settings.end()) {
        apply_timeout(it->second);    // found - the iterator is the out-parameter
    }
}
pub fn apply_timeout_setting(settings: &BTreeMap<String, i32>, apply_timeout: impl FnOnce(i32)) {
    if let Some(&timeout) = settings.get("timeout") {    // get never inserts; entry() is the insert
        apply_timeout(timeout);
    }
}

Why it looks like this. find is TryGetValue with the iterator playing the out-parameter: one lookup, no exception, no insertion. Its two siblings do different jobs — at() is the throwing indexer, and operator[] is insert-or-return, a writer's tool. Chapter 11 owns the container story; this is its most-used line, pulled out to where you will look for it. Needs <map> — or <unordered_map>; the recipe is identical. In Rust HashMap::get and BTreeMap::get never insert, and the inserting lookup has its own name, entry — the distinction C++ hides behind [].

Trap: reading a missing key with settings["timeout"] default-constructs a value and inserts it — the read mutates the map. That is also why [] does not compile on a const map: the compiler is telling you it writes.

Recipe 9 — Write a string to a file

In C#: File.WriteAllText(path, text);

The recipe:

void write_all_text(const std::filesystem::path& path, const std::string& text) {
    std::ofstream out(path, std::ios::binary);
    if (!out) {
        throw std::runtime_error("cannot create: " + path.string());
    }
    out << text;
    if (!out.flush()) {
        throw std::runtime_error("write failed: " + path.string());
    }
}
pub fn write_all_text(path: &Path, text: &str) -> std::io::Result<()> {
    std::fs::write(path, text)    // create or truncate, write, close - every failure is the Err
}

Why it looks like this. The mirror of Recipe 1, with one asymmetry that matters: on the way out, errors arrive late. The operating system buffers writes, so a full disk or a yanked drive often surfaces only when the buffer flushes — and the destructor's close, which also flushes, cannot report it, because destructors do not throw. The explicit flush() before scope end is therefore the one place the failure can become an exception instead of silence. std::ios::binary for the same reason as Recipe 1: bytes as written, no platform newline translation — and the parameter is a path for Recipe 1's reason. Needs <filesystem>, <fstream>, <stdexcept>.

Trap: skip the flush-and-check and a full disk is silent data loss — the write "succeeds", the destructor swallows the error, and the file is short. C# threw; here the check is yours.

Recipe 10 — Build a path from pieces

In C#: var full = Path.Combine(dir, "logs", "app.txt");

The recipe:

std::filesystem::path log_path(const std::filesystem::path& dir) {
    return dir / "logs" / "app.txt";    // '/' inserts the platform's separator
}
pub fn log_path(dir: &Path) -> PathBuf {
    dir.join("logs").join("app.txt")    // join inserts the platform's separator
}

Why it looks like this. std::filesystem::path (C++17) overloads division, so the code reads like the path it builds, and the separator is the platform's problem again — the thing you lost leaving Path.Combine behind. It is a real type, not a string convention: .filename(), .extension() and .parent_path() replace the Path.Get* family. And one C# rule ports exactly: an absolute right-hand side replaces everything to its left, just as it does in Path.Combine — that reflex survives the move. One thing Path.Combine never made you ask is what a path is made of: path::value_type is wchar_t on Windows and char everywhere else, and a std::string handed to the constructor is read in the platform's native narrow encoding — on Windows the process's code page, which is UTF-8 only if the process opted in — so a UTF-8 name from a JSON file or Recipe 17 arrives on disk as Chapter 9's mojibake. std::filesystem::u8path(s) says the string is UTF-8 (C++17; C++20 deprecates it for path(u8"...") with char8_t), and p.u8string() is the way back — a std::string in C++17, a std::u8string in C++20; the buildlab-msvc job asserts the round trip, because Windows is the one platform where the two constructors differ. Needs <filesystem>.

Trap: p += "logs" compiles and glues — += is string concatenation with no separator, so one character separates dir/logs from dirlogs; the separator-aware append is /= (or /).

Recipe 11 — Check that a file or directory exists

In C#: if (File.Exists(path)) / if (Directory.Exists(path))

The recipe:

namespace fs = std::filesystem;

bool config_present(const fs::path& p) {
    return fs::is_regular_file(p);    // File.Exists: it exists AND is a file
}

bool logs_dir_present(const fs::path& p) {
    return fs::is_directory(p);       // Directory.Exists: exists AND is a directory
}
pub fn config_present(p: &Path) -> bool {
    p.is_file()    // File.Exists: it exists AND is a file
}

pub fn logs_dir_present(p: &Path) -> bool {
    p.is_dir()     // Directory.Exists: exists AND is a directory
}

Why it looks like this. The split is the same split C# makes: is_regular_file is File.Exists (it exists and is a file), is_directory is Directory.Exists, and the bare fs::exists — either kind — maps to .NET 7's late-arriving Path.Exists; before that it had no C# name. Every std::filesystem function ships as Chapter 8's pair — a throwing overload and an error_code overload — so the error dialect is your choice per call site; the alias line is the convention everyone writes. Needs <filesystem>.

Trap: check-then-open is a race — the file can vanish between the two, so gate nothing on this that the open will not re-verify itself; Recipe 1's if (!in) is the check that counts, this one is for reporting.

Recipe 12 — List the files in a directory

In C#: foreach (var f in Directory.GetFiles(dir))

The recipe:

std::vector<std::filesystem::path> list_files(const std::filesystem::path& dir) {
    std::vector<std::filesystem::path> files;
    for (const auto& entry : std::filesystem::directory_iterator(dir)) {
        if (entry.is_regular_file()) {
            files.push_back(entry.path());
        }
    }
    return files;
}
pub fn list_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    for entry in std::fs::read_dir(dir)? {    // the directory itself may be unreadable
        let entry = entry?;                   // and so may any one entry
        if entry.file_type()?.is_file() {
            files.push(entry.path());
        }
    }
    Ok(files)
}

Why it looks like this. The iterator is the enumeration: range-for over a directory_iterator visits each entry once, the entry answers is_regular_file() from what the traversal already knows, and recursive_directory_iterator is SearchOption.AllDirectories. There is no pattern argument — filter on .extension() yourself, which costs a line and spares you a glob dialect. Needs <filesystem>, <vector>.

Trap: the order is unspecified — the same loop lists alphabetically on your machine and arbitrarily on the CI box, and C# never promised an order either, it just tended to deliver one; std::sort the result if order matters.

Recipe 13 — Run work on another thread and wait for it

In C#: var task = Task.Run(CountDefects); ... var n = await task;

The recipe:

int overlap_work() {
    std::future<int> task = std::async(std::launch::async, count_defects);
    const int other = do_other_work();    // runs while count_defects runs
    return other + task.get();            // the await: blocks until the result arrives
}
pub fn overlap_work(count_defects: fn() -> i32, do_other_work: fn() -> i32) -> i32 {
    let task = std::thread::spawn(count_defects);    // starts now, on its own thread
    let other = do_other_work();                     // runs while count_defects runs
    other + task.join().expect("count_defects panicked")    // the await: blocks until the result arrives
}

Why it looks like this. std::async is Task.Run without the runtime: on gcc/clang usually a fresh OS thread, no pool unless you build one; MSVC runs it on the Windows thread pool, recycling threads — so never rely on fresh-thread guarantees like thread_local starting clean — Chapter 29's model, in one line. .get() is await spelled as a block: this thread stops until the result arrives; nothing suspends, nothing resumes elsewhere. The std::launch::async policy is not decoration — the default may defer the work to run lazily inside .get(), on this thread, which is the opposite of what Task.Run means. One behavior ports exactly: a throw inside the work is captured and rethrown at .get(), the same unwrapping await did for you. Needs <future>. In Rust the future is a JoinHandle, and join() returns a Result whose Err carries the panic — the exception-surfaces-at-get() behaviour, made visible in the type.

Trap: the future returned by std::async blocks in its destructor until the work finishes — dropping it to fire-and-forget turns "run this in the background" into "stop here until it is done", silently serializing the program.

Recipe 14 — Expose an event

In C#: public event EventHandler<int> SampleReady;SampleReady?.Invoke(this, s);

The recipe:

class SampleSource {
public:
    using Handler = std::function<void(int)>;

    int subscribe(Handler handler) {
        handlers_.emplace_back(next_id_, std::move(handler));
        return next_id_++;    // the token is how -= works without delegate identity
    }

    void unsubscribe(int id) {
        handlers_.erase(std::remove_if(handlers_.begin(), handlers_.end(),
                            [id](const auto& entry) { return entry.first == id; }),
                        handlers_.end());
    }

    void raise(int sample) {    // the ?.Invoke: an empty list is a zero-pass loop
        for (const auto& entry : handlers_) {
            entry.second(sample);
        }
    }

private:
    std::vector<std::pair<int, Handler>> handlers_;
    int next_id_ = 0;
};
pub struct SampleSource {
    handlers: Vec<(u32, Box<dyn FnMut(i32)>)>,
    next_id: u32,
}

impl SampleSource {
    pub fn new() -> Self {
        Self { handlers: Vec::new(), next_id: 0 }
    }

    pub fn subscribe(&mut self, handler: impl FnMut(i32) + 'static) -> u32 {
        self.handlers.push((self.next_id, Box::new(handler)));
        self.next_id += 1;
        self.next_id - 1    // the token is how -= works without delegate identity
    }

    pub fn unsubscribe(&mut self, id: u32) {
        self.handlers.retain(|(token, _)| *token != id);
    }

    pub fn raise(&mut self, sample: i32) {    // the ?.Invoke: an empty list is a zero-pass loop
        for (_, handler) in &mut self.handlers {
            handler(sample);
        }
    }
}

impl Default for SampleSource {
    fn default() -> Self {
        Self::new()
    }
}

Why it looks like this. event is language sugar over a delegate field; here the field is explicit — a vector of callables — and std::function is the delegate (Chapter 10). The token replaces -=: C# unsubscribes by delegate identity, but std::function cannot be compared for equality, so subscribers hold the id that subscribe returned. The ?.Invoke null-check disappears — an empty vector loops zero times — and "only the declaring class may raise" is an access decision you make (put raise in private), not a language rule. Two C# habits to check at the door: a handler that unsubscribes during raise mutates the vector mid-loop — Chapter 21's invalidation, arriving through an event — and the whole consuming side of this pattern is Chapter 22's subject. Needs <functional>, <vector>, <algorithm>, <utility>. In Rust the handler list is Vec<(u32, Box<dyn FnMut(i32)>)>, retain is the unsubscribe, and the borrow checker asks the question C# never did: a handler that captures the source itself cannot be written without Rc<RefCell<…>>, which is the cycle made visible.

Trap: the C# leak runs the other way here — C#'s classic event bug is the publisher keeping dead subscribers alive; nothing here keeps anything alive, so a subscriber that dies without unsubscribe leaves a dangling capture, and the next raise is a use-after-free delivered by your own class.

Recipe 15 — Print a diagnostic you will actually see

In C#: Console.WriteLine(...) / Console.Error.WriteLine(...)

The recipe:

void report_progress(int done, int total) {
    std::cout << "processed " << done << " of " << total << '\n';    // buffered: fast
}

void report_failure(const std::string& what) {
    std::cerr << "error: " << what << '\n';    // unbuffered: survives a crash
}
pub fn report_progress(done: usize, total: usize) {
    println!("processed {done} of {total}");    // stdout: line-buffered, and a terminal shows it
}

pub fn report_failure(what: &str) {
    eprintln!("error: {what}");    // stderr: unbuffered, survives a crash, separable in a pipe
}

Why it looks like this. The mapping is direct — cout is Console.Out, cerr is Console.Error — but the split that matters is buffering. cout is line-buffered at a terminal on POSIX (a Windows console flushes per call — even sooner) and fully buffered into a file or CI log everywhere, and a process that dies takes the buffer with it — Chapter 28 watched four [ ok ] lines vanish exactly this way. cerr is unbuffered: slower per line, on screen before the next statement runs, which is precisely what you want from the message that explains the crash. When the codebase needs real logging — levels, sinks, rotation — the ecosystem default is spdlog; in plug-in work, first check whether the host SDK hands you a log callback, because writing into the host's log is worth more than owning your own. And for Chapter 29's bugs, prints are the wrong tool entirely — they change the timing (Chapter 31's point); reach for the sanitizer instead. Needs <iostream>.

Trap: std::endl is a flush, not a newline — in a hot loop it turns buffered output into a syscall per line; but drop flushing entirely and Chapter 28's fate awaits: the crash eats the buffer and the log ends four lines early. '\n' by default, flush on purpose.

Recipe 16 — Run something every interval

In C#: var t = new System.Timers.Timer(250); t.Elapsed += OnTick; t.Start();

The recipe:

class RepeatingTimer {
public:
    RepeatingTimer(std::chrono::milliseconds interval, std::function<void()> tick)
        : worker_([this, interval, tick = std::move(tick)] {
              while (!stop_) {
                  // Task.Delay, spelled honestly: a thread you own, blocked.
                  std::this_thread::sleep_for(interval);
                  if (!stop_) {
                      tick();
                  }
              }
          }) {}

    ~RepeatingTimer() {
        stop_ = true;
        worker_.join();    // Chapter 29's obligation - and this join IS the Stop()
    }

private:
    std::atomic<bool> stop_{false};    // declared before worker_: initialized first
    std::thread worker_;
};
pub struct RepeatingTimer {
    stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
    worker: Option<std::thread::JoinHandle<()>>,
}

impl RepeatingTimer {
    pub fn new(interval: Duration, mut tick: impl FnMut() + Send + 'static) -> Self {
        let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let seen = std::sync::Arc::clone(&stop);
        let worker = std::thread::spawn(move || {
            while !seen.load(std::sync::atomic::Ordering::Relaxed) {
                // Task.Delay, spelled honestly: a thread you own, blocked.
                std::thread::sleep(interval);
                if !seen.load(std::sync::atomic::Ordering::Relaxed) {
                    tick();
                }
            }
        });
        Self { stop, worker: Some(worker) }
    }
}

impl Drop for RepeatingTimer {
    fn drop(&mut self) {
        self.stop.store(true, std::sync::atomic::Ordering::Relaxed);
        if let Some(worker) = self.worker.take() {
            let _ = worker.join();    // Chapter 29's obligation - and this join IS the Stop()
        }
    }
}

Why it looks like this. The standard library has no timer, and the honest answer has two halves. In plug-in work, the host's tick or idle callback is the timer — starting your own thread inside someone else's event loop is a transplant error, so read the SDK's threading documentation before writing this class. When you do own the process, a timer is exactly this: a worker thread, a sleep loop (sleep_for is Task.Delay, blocking a real thread — Chapter 29's model), and an atomic stop flag the destructor sets before the join that Chapter 29 obliges. The member order is Finding 2 of Chapter 25 applied: stop_ is declared before worker_ so the thread never reads an uninitialized flag. And the captured this is why the type must not move — Chapter 18's re-register-on-move lesson; the user-declared destructor conveniently suppresses the moves. Teardown waits out at most one interval; a condition_variable turns that into an immediate wake when it matters. Needs <atomic>, <chrono>, <functional>, <thread>. In Rust the timer is the same two fields, an AtomicBool behind an Arc and a JoinHandle, and Drop does the join — which is why the handle is an Option: join consumes it, and a destructor only gets &mut self.

Trap: a timer whose tick touches an object must not outlive it — and C# let you forget Stop() because the GC kept the target alive; here the join in the destructor is the Stop, and skipping it (a detached thread) is a tick delivered into freed memory.

Recipe 17 — Convert between UTF-8 and UTF-16

In C#: Encoding.UTF8.GetBytes(s) / Encoding.UTF8.GetString(bytes) — or nothing at all, because string was UTF-16 and the runtime converted at every boundary without telling you.

The recipe:

// UTF-8 -> UTF-16. Invalid input becomes U+FFFD, the convention browsers
// follow; no exceptions, no locale, no deprecated machinery.
std::u16string utf8_to_utf16(std::string_view utf8) {
    std::u16string out;
    for (std::size_t i = 0; i < utf8.size(); ) {
        const auto b0 = static_cast<unsigned char>(utf8[i]);
        std::size_t n = b0 < 0x80          ? 1
                      : (b0 & 0xE0) == 0xC0 ? 2
                      : (b0 & 0xF0) == 0xE0 ? 3
                      : (b0 & 0xF8) == 0xF0 ? 4 : 0;
        char32_t cp = n == 1 ? b0
                    : n     ? b0 & (0x7Fu >> n)   // the lead byte's payload
                            : 0xFFFDu;            // stray or invalid lead
        std::size_t taken = 1;
        for (std::size_t k = 1; n && k < n && i + k < utf8.size(); ++k) {
            const auto bk = static_cast<unsigned char>(utf8[i + k]);
            if ((bk & 0xC0) != 0x80) { n = 0; break; }  // sequence cut short
            cp = (cp << 6) | (bk & 0x3Fu);
            ++taken;
        }
        if (n == 0 || taken != n || cp > 0x10FFFFu ||
            (cp >= 0xD800u && cp <= 0xDFFFu) ||           // surrogates
            (n == 2 && cp < 0x80u) || (n == 3 && cp < 0x800u) ||
            (n == 4 && cp < 0x10000u))                    // overlong forms
            cp = 0xFFFDu;
        i += taken;
        if (cp < 0x10000u) {
            out.push_back(static_cast<char16_t>(cp));
        } else {                                  // astral plane: a pair
            cp -= 0x10000u;
            out.push_back(static_cast<char16_t>(0xD800u + (cp >> 10)));
            out.push_back(static_cast<char16_t>(0xDC00u + (cp & 0x3FFu)));
        }
    }
    return out;
}

// UTF-16 -> UTF-8. Lone surrogates become U+FFFD; everything else is
// mechanical: split the code point across 1-4 bytes, high bits first.
std::string utf16_to_utf8(std::u16string_view utf16) {
    std::string out;
    for (std::size_t i = 0; i < utf16.size(); ++i) {
        char32_t cp = utf16[i];
        if (cp >= 0xD800u && cp <= 0xDBFFu && i + 1 < utf16.size() &&
            utf16[i + 1] >= 0xDC00u && utf16[i + 1] <= 0xDFFFu) {
            cp = 0x10000u + ((cp - 0xD800u) << 10) + (utf16[i + 1] - 0xDC00u);
            ++i;                                  // consumed the pair
        } else if (cp >= 0xD800u && cp <= 0xDFFFu) {
            cp = 0xFFFDu;                         // lone surrogate
        }
        if (cp < 0x80u) {
            out.push_back(static_cast<char>(cp));
        } else if (cp < 0x800u) {
            out.push_back(static_cast<char>(0xC0u | (cp >> 6)));
            out.push_back(static_cast<char>(0x80u | (cp & 0x3Fu)));
        } else if (cp < 0x10000u) {
            out.push_back(static_cast<char>(0xE0u | (cp >> 12)));
            out.push_back(static_cast<char>(0x80u | ((cp >> 6) & 0x3Fu)));
            out.push_back(static_cast<char>(0x80u | (cp & 0x3Fu)));
        } else {
            out.push_back(static_cast<char>(0xF0u | (cp >> 18)));
            out.push_back(static_cast<char>(0x80u | ((cp >> 12) & 0x3Fu)));
            out.push_back(static_cast<char>(0x80u | ((cp >> 6) & 0x3Fu)));
            out.push_back(static_cast<char>(0x80u | (cp & 0x3Fu)));
        }
    }
    return out;
}
// UTF-8 -> UTF-16. A &str is valid UTF-8 by construction, so the only
// conversion left is the encoding; invalid BYTES are handled one step
// earlier, by from_utf8_lossy, which writes U+FFFD the way browsers do.
pub fn utf8_to_utf16(utf8: &str) -> Vec<u16> {
    utf8.encode_utf16().collect()
}

pub fn utf8_bytes_to_utf16(bytes: &[u8]) -> Vec<u16> {
    String::from_utf8_lossy(bytes).encode_utf16().collect()
}

// UTF-16 -> UTF-8. Lone surrogates become U+FFFD; everything else is mechanical.
pub fn utf16_to_utf8(utf16: &[u16]) -> String {
    String::from_utf16_lossy(utf16)
}

Why it looks like this. The honest part first: the standard library has no good answer — <codecvt> was deprecated in C++17 with no replacement — so real codebases convert with the platform (MultiByteToWideChar / WideCharToMultiByte on Windows, where vendor "wide" strings and 16-bit wchar_t live), with ICU or their framework, or with the vendor SDK's own helpers. Chapter 9's rule is only that the conversion is named, wherever it lives. What hand-rolling the mechanism once buys you is the demystification: seventy lines cover every code point Unicode will ever assign, both directions are bit-work at documented offsets — Chapter 34's wire discipline applied to text — and damaged input becomes U+FFFD (the browser convention) instead of an exception, which is the policy question every converter must answer and most APIs bury. char16_t is the portable spelling of "16-bit unit"; on Windows it and wchar_t are the same bits. Needs <string>, <string_view>. In Rust the two directions are one call each, encode_utf16 and String::from_utf16_lossy, and the third half of the problem — bytes that are not valid UTF-8 — is String::from_utf8_lossy, because a &str cannot hold them in the first place.

Trap: none of the three size()s counts characters — "Grüße" is five characters, seven UTF-8 bytes and five UTF-16 units, while one 𝄞 is one, four and two. A length check that "worked for years" on ASCII is an encoding bug with a long fuse.

Recipe 18 — Find an element, an index, or a substring

In C#: list.IndexOf(x), list.Contains(x), text.Contains("word")

The recipe:

template <class Seq, class T>
std::optional<std::size_t> index_of(const Seq& values, const T& wanted) {
    const auto it = std::find(values.begin(), values.end(), wanted);
    if (it == values.end()) {
        return std::nullopt;             // an algorithm says "not found" as end()
    }
    return static_cast<std::size_t>(std::distance(values.begin(), it));
}

bool contains_word(std::string_view text, std::string_view word) {
    return text.find(word) != std::string_view::npos;    // a string says it as npos
}
pub fn index_of<T: PartialEq>(values: &[T], wanted: &T) -> Option<usize> {
    values.iter().position(|v| v == wanted)    // "not found" is None, not a sentinel
}

pub fn contains_word(text: &str, word: &str) -> bool {
    text.contains(word)    // a str says it as bool; find() would say Option<usize>
}

Why it looks like this. "Not found" has three spellings in C++: an algorithm says end(), a string says npos — the largest size_t there is — and a lookup you write yourself says optional or nullptr, which is why index_of hands back the index as Recipe 19's optional rather than as C#'s -1, and only after the check. std::find is IndexOf without the index, and the index is a std::distance away; C++20 gives the associative containers a member contains, C++23 gives strings one, and a vector never gets it. Chapter 11 owns the algorithm story. Needs <algorithm>, <iterator>, <optional>, <string_view>.

Trap: if (text.find(word)) compiles and tests the position — a match at offset 0 reads as false and npos as true; and std::find_if over a std::map compiles too, walking every node when the member m.find(key) was the lookup you meant.

Recipe 19 — Carry a value that may be absent

In C#: int? port = int.TryParse(text, out var p) ? p : null; port ?? 8080; text?.Length

The recipe:

std::optional<int> parse_port(std::string_view text) {
    int value = 0;
    const auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
    if (ec != std::errc{} || end != text.data() + text.size() || value < 0 || value > 65535) {
        return std::nullopt;              // not a port: absence, not an error (Chapter 8)
    }
    return value;                         // TryParse's out-parameter, as the return
}

int port_or_default(std::optional<int> port) {
    return port.value_or(8080);           // the ?? operator
}

std::optional<std::size_t> digits_in(const std::optional<std::string>& text) {
    if (!text) {
        return std::nullopt;              // ?. by hand: C++17 has no null-propagating call
    }
    return text->size();                  // -> is only legal once you have checked
}
pub fn parse_port(text: &str) -> Option<u16> {
    text.parse::<u16>().ok()    // not a port: absence, not an error - u16 already means 0..=65535
}

pub fn port_or_default(port: Option<u16>) -> u16 {
    port.unwrap_or(8080)    // the ?? operator
}

pub fn digits_in(text: Option<&str>) -> Option<usize> {
    text.map(str::len)    // the ?. operator: map runs only when there is something to map
}

Why it looks like this. std::optional<T> is T? with the value kept behind * and -> rather than in front of them, so the caller has to look before touching it, and std::from_chars is int.TryParse — no exception, no locale, an error code and an end pointer you check. The three shapes are the three C# operators: std::nullopt is null, value_or is ??, and ?. has no C++17 spelling — the if (!text) is that operator written out. Chapter 10 owns the type, and Chapter 8 decides when absence is the right answer at all. Needs <optional>, <charconv>, <string>, <string_view>.

Trap: *port or port-> on an empty optional is undefined behavior, not a null-reference exception — it reads garbage, the program carries on, and the sanitizers say nothing; port.value() is the spelling that throws.

Recipe 20 — Switch on the kind of a message

In C#: switch (e) { case Temperature t: ...; case Fault f: ...; case Heartbeat: ...; }

The recipe:

struct Temperature { int centi; };        // centi-degrees, as the wire carries them
struct Fault       { int code; };
struct Heartbeat   {};
using Event = std::variant<Temperature, Fault, Heartbeat>;

template <class... Fs> struct overloaded : Fs... { using Fs::operator()...; };
template <class... Fs> overloaded(Fs...) -> overloaded<Fs...>;

std::string describe(const Event& e) {
    return std::visit(overloaded{
        [](const Temperature& t) { return "temperature " + std::to_string(t.centi) + " centi-degrees"; },
        [](const Fault& f)       { return "fault " + std::to_string(f.code); },
        [](Heartbeat)            { return std::string("heartbeat"); },
    }, e);
}
pub enum Event {
    Temperature { centi: i32 },    // centi-degrees, as the wire carries them
    Fault { code: i32 },
    Heartbeat,
}

pub fn describe(e: &Event) -> String {
    match e {    // exhaustive: a fourth kind is a compile error here, not a fall-through
        Event::Temperature { centi } => format!("temperature {centi} centi-degrees"),
        Event::Fault { code } => format!("fault {code}"),
        Event::Heartbeat => "heartbeat".to_string(),
    }
}

Why it looks like this. C# pattern-matches on the runtime type of an object; C++17 has no runtime type for three unrelated structs, so the closed set is spelled as a std::variant and the switch as std::visit — a call that hands the live alternative to whichever lambda takes it. The two overloaded lines are the idiom that turns those lambdas into one callable with one operator() each; the standard library does not ship it, and every codebase on C++17 has a copy. Chapter 10 owns the type, and says when a variant beats the class hierarchy you would have written in C#. Needs <variant>, <string>. In Rust the closed set is an enum with data on its variants and the switch is match, which the compiler holds to exhaustiveness: add a fourth kind and every match without it stops compiling, where std::visit over an incomplete overloaded does the same one template error at a time.

Trap: leave one alternative out of the visitor and the build fails — which is the feature; the same omission in a switch on a kind field compiles and falls through, and that is how a vendor's new event type crashes a plug-in a year after it shipped.

Recipe 21 — Throw and catch your own exception type

In C#: class ParseException : Exception { public int Line { get; } }catch (ParseException e)

The recipe:

class ParseError : public std::runtime_error {
public:
    ParseError(int line, const std::string& what)
        : std::runtime_error("line " + std::to_string(line) + ": " + what),
          line_(line) {}
    int line() const noexcept { return line_; }    // the payload what() cannot carry
private:
    int line_;
};

int parse_channel_count(std::string_view text, int line) {
    int value = 0;
    const auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
    if (ec != std::errc{} || end != text.data() + text.size() || value <= 0) {
        throw ParseError(line, "channel count is not a number: '" + std::string(text) + "'");
    }
    return value;
}

int channels_or_default(std::string_view text, int line) {
    try {
        return parse_channel_count(text, line);
    } catch (const ParseError& e) {          // the derived type FIRST
        log_line(e.line(), e.what());
        return 2;
    } catch (const std::exception& e) {      // then the base: order is the rule
        log_line(line, e.what());
        return 2;
    }
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParseError {
    pub line: u32,    // the payload the message alone cannot carry
    pub what: String,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "line {}: {}", self.line, self.what)
    }
}

impl std::error::Error for ParseError {}    // so it can travel inside Box<dyn Error>

pub fn parse_channel_count(text: &str, line: u32) -> Result<i32, ParseError> {
    match text.parse::<i32>() {
        Ok(value) if value > 0 => Ok(value),
        _ => Err(ParseError { line, what: format!("channel count is not a number: '{text}'") }),
    }
}

pub fn channels_or_default(text: &str, line: u32) -> i32 {
    match parse_channel_count(text, line) {    // no catch: the failure is a value you match on
        Ok(value) => value,
        Err(e) => {
            eprintln!("{e}");    // Display is the what(); e.line is still there
            2
        }
    }
}

Why it looks like this. Derive from std::runtime_error (or std::logic_error for a caller bug) so every catch (const std::exception&) in the program — the plug-in entry point of Chapter 8 among them — already handles it, and build the message once in the constructor, because what() returns a const char* that cannot be assembled later. Anything what() cannot carry is a member with an accessor. Throw by value, catch by const& (Chapter 8): a catch by value slices the payload off. Needs <stdexcept>, <string>, <string_view>, <charconv>. In Rust there is no throw: the type is a struct with Display and std::error::Error, the function returns Result<i32, ParseError>, and the catch-order rule disappears because there is nothing to catch in order — the caller matches on the value.

Trap: catch clauses are tried in order, so a catch (const std::exception&) written above the catch (const ParseError&) makes the second handler dead code — both compilers warn by default (clang names it -Wexceptions), so a codebase that silences warnings ships it.

Recipe 22 — Return a value or an error

In C#: if (!int.TryParse(text, out var n)) … when the caller needs the reason — or a Result<T, TError> from a library

The recipe:

template <class T, class E>
class Result {
public:
    static Result ok(T value)   { return Result(std::in_place_index<0>, std::move(value)); }
    static Result fail(E error) { return Result(std::in_place_index<1>, std::move(error)); }

    bool has_value() const noexcept { return state_.index() == 0; }
    explicit operator bool() const noexcept { return has_value(); }

    const T& value() const { return std::get<0>(state_); }   // throws bad_variant_access on a failure
    const E& error() const { return std::get<1>(state_); }   // ...and on a success

private:
    template <std::size_t I, class X>                        // built in place: one move, not two
    Result(std::in_place_index_t<I> door, X&& x) : state_(door, std::forward<X>(x)) {}
    std::variant<T, E> state_;                               // index 0 is the value, 1 the error
};

struct ConfigError { int line; std::string what; };
struct Config      { int channels; };

// The translation at the module's edge: the parser throws, this function
// returns. Nothing above it ever sees a ParseError.
Result<Config, ConfigError> load_config(std::string_view text) {
    try {
        return Result<Config, ConfigError>::ok(Config{parse_channel_count(text, 1)});
    } catch (const ParseError& e) {          // the throw stops here: failure becomes a value
        return Result<Config, ConfigError>::fail(ConfigError{e.line(), e.what()});
    }
}
// The translation at the module's edge - and Result<T, E> is the standard
// library's type: nothing to write, only the two errors to map.
pub fn load_config(text: &str) -> Result<Config, ConfigError> {
    let channels = parse_channel_count(text, 1)
        .map_err(|e| ConfigError { line: e.line, what: e.to_string() })?;    // ? returns the Err
    Ok(Config { channels })
}

Why it looks like this. Three spellings of one idea, chosen by what the caller needs to know: std::optional<T> (Recipe 19) when absence needs no explanation, this Result<T, E> when it does and the toolchain is C++17, and std::expected<T, E> when it is C++23 — the same function with std::unexpected(...) on the failure side, plus and_then and transform for chaining, which Chapter 8's translation-layer section shows and exercises/cookbook/expected.cpp builds as the cookbook's one C++23 listing. The Result above is Chapter 10's std::variant behind two named doors, and the try inside load_config is that section's edge: the parser throws, the function returns. Needs <variant>, <utility>, <cstddef>, <string>, <string_view>. In Rust Result<T, E> is the standard library's, ? is the translation layer in one character, and this recipe's only work is the map_err from one error type to the other.

Trap: value() on the error side throws — bad_variant_access here, bad_expected_access<E> in C++23 — so a caller that skips the check has not written error-code style, it has written an exception with a worse name; test with if (r) first, and value() is for the one frame allowed to throw.

Recipe 23 — Test for an empty string, and for no string at all

In C#: if (string.IsNullOrEmpty(name))name ?? "unnamed"

The recipe:

std::string name_or_default(const char* from_c_api) {
    if (from_c_api == nullptr) {            // the one null there is: a C API's "no name"
        return "unnamed";
    }
    return from_c_api;                      // safe now - std::string(nullptr) is UB
}
/// The one null there is: a C API's "no name". A &str cannot be null, so
/// the check lives at the boundary where the raw pointer arrives.
///
/// # Safety
/// `from_c_api` must be null or point at a NUL-terminated string that
/// outlives the call.
pub unsafe fn name_or_default(from_c_api: *const std::os::raw::c_char) -> String {
    if from_c_api.is_null() {
        return "unnamed".to_string();
    }
    std::ffi::CStr::from_ptr(from_c_api).to_string_lossy().into_owned()    // safe now
}

Why it looks like this. IsNullOrEmpty exists because a C# string can be null and empty and callers rarely care which; C++ separates the two by type, and Chapter 9 owns the facts — a std::string cannot be null, so empty() is the whole test, "no string at all" is Recipe 19's optional<std::string>, and the one null in the picture is the const char* a C API returns, which is what this recipe guards. The copy into a std::string is deliberate: returning a view would inherit the C buffer's lifetime, Chapter 10's dangling view. Needs <string>. In Rust the null cannot reach a &str at all; the check happens where the raw pointer arrives, inside an unsafe function whose contract is written above it.

Trap: std::string name = Thing_GetName(h); with a null return is undefined behavior that reads like an assignment — libc++ dies inside the constructor and libstdc++ throws std::logic_error — and scripts/check_platform_claims.sh asserts both, because neither is a report you would expect from that line.

Recipe 24 — Compile a diagnostic out of Release

In C#: [Conditional("DEBUG")] static void CheckInvariant(...) — or #if DEBUG … #endif

The recipe:

void check_channel_count([[maybe_unused]] int channels) {          // used only in Debug
    assert(channels > 0 && "a session has at least one channel");   // gone under NDEBUG
#ifndef NDEBUG
    std::cerr << "[debug] channels=" << channels << '\n';           // and so is this block
#endif
}
pub fn check_channel_count(channels: i32) {
    debug_assert!(channels > 0, "a session has at least one channel");    // gone in --release
    if cfg!(debug_assertions) {
        eprintln!("[debug] channels={channels}");    // and so is this block: cfg! is a constant
    }
}

Why it looks like this. assert is Appendix E's assert / NDEBUG entry wearing [Conditional("DEBUG")]'s job, with two differences worth the paragraph. The && "message" is the idiom for a message, since the whole expression is what the failure prints, and an #ifndef NDEBUG block is #if DEBUG for anything larger than one expression — the sign inverted, because the define means release. And unlike [Conditional], which removes the call, the call and its argument evaluation survive here; only the body empties, which is why the parameter is [[maybe_unused]] (in Release nothing reads it, and -Wextra would say so) and why a macro — #ifdef NDEBUG / #define CHECK_CHANNELS(x) ((void)0) — is the spelling that also spares the argument. Needs <cassert>, <iostream>.

Trap: the expression inside assert vanishes with it — assert(bump() == 1) runs bump() in Debug and never in Release, and exercises/cookbook/logging.cpp is built both ways to prove it.

Recipe 25 — Serialize a record to JSON

In C#: var text = JsonSerializer.Serialize(readings, new JsonSerializerOptions { WriteIndented = true });

The recipe:

struct Reading {
    int sensor;
    double value;
    std::string unit;
};

// Two free functions the library finds by argument-dependent lookup - no
// attribute, no reflection: this IS the [JsonPropertyName] table, by hand.
void to_json(json& j, const Reading& r) {
    j = json{{"sensor", r.sensor}, {"value", r.value}, {"unit", r.unit}};
}

void from_json(const json& j, Reading& r) {
    j.at("sensor").get_to(r.sensor);
    j.at("value").get_to(r.value);
    j.at("unit").get_to(r.unit);
}

std::string serialize(const std::vector<Reading>& readings) {
    return json(readings).dump(2);            // 2 = indent; dump() alone is one line
}

Rust's standard library has no JSON. The ecosystem's answer is serde with serde_json#[derive(Serialize, Deserialize)] on the struct is the whole [JsonPropertyName] table — and it is a dependency, which is Chapter 27's decision, not this page's; the crate stays dependency-free.

Why it looks like this. The standard library has no JSON (Chapter 27), so this is the cookbook's one dependency — nlohmann/json, vendored under exercises/third_party/ exactly as that chapter's first strategy says, version recorded beside it. There is no reflection to walk your fields, so the mapping is two free functions the library finds by argument-dependent lookup (Appendix E's ADL entry) — write them once per type and every std::vector<Reading>, std::map<std::string, Reading> and nested struct converts for free; forget one and the error is Chapter 41's overload-resolution novel, naming neither from_json nor Reading. Keys come out sorted, because the document is a map — unlike JsonSerializer, which writes properties in declaration order, so never diff the two outputs as text. A std::optional<T> member serializes as null when empty — the library has written one since 3.12 — never as a missing key, and in the vendored 3.12.0 reads back only by hand (Recipe 35's contains-then-at, with is_null() for the value: the read-side overload is declared behind a guard that is never open, fixed upstream in #4742 and unreleased at the time of writing); there is no JsonIgnoreCondition.WhenWritingNull, so strip the null before dump, or accept that absent and null are one word on your wire and write that down (Chapter 34). Needs <nlohmann/json.hpp> (-isystem exercises/third_party on the compile line, which scripts/check.sh adds), <string>, <vector>, and using json = nlohmann::json;.

Trap: j["missing"] on a non-const document inserts a null for the key (Recipe 8's trap), so a read that meant to check has changed what you serialize next. const is not the fix: the write no longer compiles, but a read of a missing key on a const json is an assertion failure, undefined behavior under NDEBUG. at() is the read, on both.

Recipe 26 — Read a JSON config with defaults

In C#: var cfg = JsonSerializer.Deserialize<Config>(text)!;public int Timeout { get; set; } = 30; for the field that may be absent, required for the one that must not be

The recipe:

struct Config {
    int timeout = 30;
    std::string name;
};

Config load_config(std::string_view text) {
    const json j = json::parse(text);         // junk throws json::parse_error - the event pole
    Config c;
    c.timeout = j.value("timeout", c.timeout);      // TryGetValue with a default: absent is fine
    c.name = j.at("name").get<std::string>();        // at(): required - missing throws out_of_range
    return c;
}

Rust's standard library has no JSON either, so this is Recipe 25's crate read the other way: serde_json::from_str into a struct with #[serde(default)] on the fields that may be absent is the idiom. A dependency this crate does not take.

Why it looks like this. Three outcomes, three spellings, and they are Chapter 8's decision applied to a file. Text that is not JSON at all is the event pole and parse throws; a field that may be absent is not an error, and value(key, default) is TryGetValue with the default in the call; a field that must be there is at(), which throws out_of_range naming the key. The default covers absence only: a key present with the wrong type, null included, throws type_error — and so does value() on a document that parsed but is not an object, because a bare 5 is valid JSON. The document owns its strings — get<std::string>() copies out, which is the point. Needs <nlohmann/json.hpp> (-isystem exercises/third_party), <string>, <string_view>, and using json = nlohmann::json;.

Trap: a reference into the document — const auto& s = j.at("name").get_ref<const std::string&>(); — is Chapter 10's dangling view the moment j goes out of scope, a heap-use-after-free under ASan; copy the value out, or keep the document alive as long as anything points into it.

Recipe 27 — Pre-size a collection

In C#: var samples = new List<int>(capacity); — or new double[n], which is a different thing

The recipe:

std::vector<int> read_samples(std::size_t expected) {
    std::vector<int> samples;
    samples.reserve(expected);            // List<T>(capacity): room for expected, size still 0
    for (std::size_t i = 0; i < expected; ++i) {
        samples.push_back(next_sample());  // size grows; no reallocation until the room runs out
    }
    return samples;
}

std::vector<double> zeroed(std::size_t n) {
    return std::vector<double>(n);        // new double[n]: n elements, every one 0.0
}
pub fn read_samples(expected: usize, mut next_sample: impl FnMut() -> i32) -> Vec<i32> {
    let mut samples = Vec::with_capacity(expected);    // List<T>(capacity): room for expected, len still 0
    for _ in 0..expected {
        samples.push(next_sample());    // len grows; no reallocation until the room runs out
    }
    samples
}

pub fn zeroed(n: usize) -> Vec<f64> {
    vec![0.0; n]    // new double[n]: n elements, every one 0.0
}

Why it looks like this. A vector carries two numbers and C# showed you one: size() is Count, the elements that exist; capacity() is the room allocated for them. reserve sets the second and leaves the first alone — nothing is constructed, and Chapter 11's reallocation is paid once, up front, at the moment you know the count — while vector(n) and resize(n) set both, constructing n value-initialized elements, which is new T[n]'s contract and not List<T>(n)'s: resize(n) followed by n calls to push_back gives you 2n elements, the first n of them zero. Reserve once, before the loop — a reserve inside it is either a no-op or, at size() + 1, a reallocation on every pass, the amortized doubling switched off by hand. On Chapter 36's deadline path a reserve at setup keeps a push_back in the callback from allocating, for as long as the count stays inside it; and it does not pinChapter 33's pitfall stands. Needs <vector>.

Trap: reserve(n) then v[i] = x compiles and writes into room that holds no element — undefined behavior that AddressSanitizer names container-overflow under libc++ (Chapter 21's report shape) and, under libstdc++, only when -D_GLIBCXX_SANITIZE_VECTOR switches the annotations on.

Recipe 28 — Time a block on every exit, and a call for its result

In C#: var sw = Stopwatch.StartNew(); try { ... } finally { Log(sw.Elapsed); } — or a helper that times a delegate and returns its result

The recipe:

class ScopedTimer {
public:
    explicit ScopedTimer(std::chrono::nanoseconds& record)
        : record_(record), start_(std::chrono::steady_clock::now()) {}
    ~ScopedTimer() { record_ = std::chrono::steady_clock::now() - start_; }   // return, throw: every path
    ScopedTimer(const ScopedTimer&) = delete;
    ScopedTimer& operator=(const ScopedTimer&) = delete;

private:
    std::chrono::nanoseconds& record_;
    std::chrono::steady_clock::time_point start_;
};

template <class F, class... Args>
auto time_call(std::chrono::nanoseconds& record, F&& f, Args&&... args)
    -> std::invoke_result_t<F, Args...> {
    ScopedTimer timer(record);
    return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);   // each argument passed on as it arrived
}
pub struct ScopedTimer<'a> {
    record: &'a mut Duration,
    start: Instant,
}

impl<'a> ScopedTimer<'a> {
    pub fn new(record: &'a mut Duration) -> Self {
        Self { record, start: Instant::now() }
    }
}

impl Drop for ScopedTimer<'_> {
    fn drop(&mut self) {
        *self.record = self.start.elapsed();    // return, ?, panic: every path out runs this
    }
}

pub fn time_call<R>(record: &mut Duration, f: impl FnOnce() -> R) -> R {
    let _timer = ScopedTimer::new(record);
    f()    // the result passes straight through; the timer records on the way out
}

Why it looks like this. The finally is a destructor — Chapter 1's shape applied to a measurement, so the stop runs on the early return and on the throw without a try at the call site, and the copies are deleted because a copy would be a second stopwatch with the same start writing the same record, and the number would no longer be the block's — Chapter 1's reason for deleting the file handle's copies, in miniature. The wrapper is a template with && on a deduced type: F&& and Args&&... are Chapter 6's forwarding references, and std::forward hands each argument on as it arrived — an lvalue stays borrowed, an rvalue stays stealable — where std::move would have gutted the caller's variable and a plain pass would have copied. std::invoke_result_t names the return type without running the call (Chapter 10's decltype with the plumbing hidden), and std::invoke accepts a lambda, a function pointer or a member pointer alike. Both write into a record you own rather than printing, so a test can assert on it — and a number from either is a mean, which Chapter 36 says can only acquit a mean; count allocations when the question is the worst case. Needs <chrono>, <functional>, <type_traits>, <utility>.

Trap: a timed call whose result nobody reads is a call the optimizer may delete outright, so the timer brackets nothing and reports nanoseconds — use the result, and measure at -O2 without the sanitizers, because Chapter 36's factor of twenty is not uniform across code shapes.

Recipe 29 — Stamp a log line with the time

In C#: DateTime.UtcNow.ToString("o") — to the millisecond, where "o" prints seven fractional digits

The recipe:

std::string timestamp_utc() {
    const auto now = std::chrono::system_clock::now();          // the wall clock: the one with a calendar
    const auto since_epoch = now.time_since_epoch();
    const auto whole = std::chrono::floor<std::chrono::seconds>(since_epoch);   // what to_time_t would give, rounding settled
    const std::time_t seconds = whole.count();
    const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(since_epoch - whole);
    std::tm utc{};
#if defined(_WIN32)
    gmtime_s(&utc, &seconds);              // the thread-safe spellings: never std::gmtime
#else
    gmtime_r(&seconds, &utc);
#endif
    std::ostringstream out;
    out << std::put_time(&utc, "%Y-%m-%dT%H:%M:%S")
        << '.' << std::setw(3) << std::setfill('0') << millis.count() << 'Z';
    return out.str();
}

The standard library has the wall clock (SystemTime) but no calendar: formatting a SystemTime as a date is the time or chrono crate's job, a dependency this crate does not take. What std gives you honestly is SystemTime::now().duration_since(UNIX_EPOCH) — seconds and millis since the epoch, which a log line can carry as a number.

Why it looks like this. Recipe 6 said intervals come from steady_clock; a timestamp is the other clock's job, because system_clock is the only one whose now() means a calendar date. C++17 can turn it into text only by stepping down into C: the whole seconds since the epoch become a time_t (floor rather than to_time_t, which the standard lets round or truncate as it pleases), the <ctime> breakdown gives a std::tm, and std::put_time formats it with strftime's specifiers — so the milliseconds, which time_t cannot hold, come from the same duration the seconds were cut from. The _r / _s split is not pedantry: on POSIX std::gmtime returns a pointer into storage shared by every thread, which is Chapter 29's data race hiding in a formatting function, and on Windows it is the spelling MSVC deprecates. C++20 collapses the whole recipe into std::format("{:%FT%TZ}", std::chrono::floor<std::chrono::milliseconds>(now)); until your toolchain is there, this is the spelling. Needs <chrono>, <ctime>, <iomanip>, <sstream>, <string>.

Trap: the Z is a character you wrote, not something the clock knows — swap gmtime_r for localtime_r to get "readable" times and every line now claims UTC while carrying local time, wrong by the offset in every log you correlate with another machine's.

Recipe 30 — Pass a timeout to a C API

In C#: device.Wait(TimeSpan.FromSeconds(2)) — with the unit inside the type

The recipe:

int Device_Wait(std::uint32_t timeout_ms);   // the vendor's declaration: a bare integer, the unit in the name

int wait_for_sample(std::chrono::milliseconds timeout) {
    return Device_Wait(static_cast<std::uint32_t>(timeout.count()));   // the unit left the type HERE, and only here
}
// The vendor's declaration: a bare integer, the unit in the name. Passed in
// here so the recipe can be tested without the vendor; in a plug-in it is
// the extern "C" function itself.
pub type DeviceWait = unsafe extern "C" fn(timeout_ms: u32) -> std::os::raw::c_int;

pub fn wait_for_sample(timeout: Duration, device_wait: DeviceWait) -> std::os::raw::c_int {
    let ms = u32::try_from(timeout.as_millis()).unwrap_or(u32::MAX);    // the unit leaves the type HERE, and only here
    unsafe { device_wait(ms) }
}

Why it looks like this. A C API has no TimeSpan: a timeout arrives as a bare integer with the unit in the parameter name — the shape of every C-facing SDK in Chapter 16's Bestiary — and the wrapper is where the typed duration stops: your side speaks std::chrono::milliseconds, and count() — the only call that turns a duration back into a number — sits on the line next to the _ms parameter and nowhere else. The conversions run in one direction for free: wait_for_sample(2s) and wait_for_sample(std::chrono::minutes(1)) compile, because seconds to milliseconds loses nothing, while a function taking seconds handed 250ms does not compile until you write the duration_cast that admits the truncation. The static_cast is also where the range leaves: a milliseconds count is 64 bits wide and the vendor's uint32_t wraps at forty-nine days, silently, so a wrapper whose callers can pass anything long clamps before the cast. The literals need using namespace std::chrono_literals; in the scope that uses them. Needs <chrono>, <cstdint>. In Rust the same conversion is as_millis() at the call, u32::try_from rather than a cast, and the vendor's declaration is unsafe extern "C" fn — the unit still leaves the type in exactly one place.

Trap: .count() has no idea what unit it is counting — seconds(2).count() handed to a _ms parameter compiles and waits two milliseconds — so the parameter type of your wrapper, not the caller's discipline, is what puts the thousand in.

Recipe 31 — Read a feature flag once

In C#: configuration.GetValue<bool>("FastPath") — or await featureManager.IsEnabledAsync("FastPath") — wherever the code needs it

The recipe:

struct Features {
    bool audit = false;                  // the defaults ARE the off state
    bool fast_path = false;
    int  batch_size = 64;

    // One source among several - Recipe 26's JSON file, the host's
    // preferences API, a command line. Whatever the source, it is read HERE,
    // once, and never again.
    static Features from_environment() {
        Features f;
        if (const char* v = std::getenv("MYPLUGIN_AUDIT"))      f.audit = std::string_view(v) == "1";
        if (const char* v = std::getenv("MYPLUGIN_FAST_PATH"))  f.fast_path = std::string_view(v) == "1";
        if (const char* v = std::getenv("MYPLUGIN_BATCH_SIZE")) {
            const std::string_view s(v);
            std::from_chars(s.data(), s.data() + s.size(), f.batch_size);   // junk: batch_size stays 64 (Recipe 19)
        }
        return f;
    }
};

class Processor {
public:
    explicit Processor(Features features) : features_(features) {}   // read once, kept as a member

    int process(int sample) const {
        if (features_.fast_path) {       // a branch: free, even on the deadline path
            return sample;
        }
        return sample * 2;
    }

private:
    Features features_;
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Features {
    pub audit: bool,         // the defaults ARE the off state
    pub fast_path: bool,
    pub batch_size: usize,
}

impl Default for Features {
    fn default() -> Self {
        Self { audit: false, fast_path: false, batch_size: 64 }
    }
}

impl Features {
    // One source among several. Whatever the source, it is read HERE, once, and never again.
    pub fn from_environment() -> Self {
        let flag = |name: &str| std::env::var(name).map(|v| v == "1").unwrap_or(false);
        let mut f = Self { audit: flag("MYPLUGIN_AUDIT"), fast_path: flag("MYPLUGIN_FAST_PATH"), ..Self::default() };
        if let Ok(size) = std::env::var("MYPLUGIN_BATCH_SIZE") {
            f.batch_size = size.parse().unwrap_or(f.batch_size);    // junk: batch_size stays 64 (Recipe 19)
        }
        f
    }
}

pub struct Processor {
    features: Features,    // read once, kept as a field
}

impl Processor {
    pub fn new(features: Features) -> Self {
        Self { features }
    }

    pub fn process(&self, sample: i32) -> i32 {
        if self.features.fast_path { sample } else { sample * 2 }    // a branch: free, even on the deadline path
    }
}

Why it looks like this. A feature flag is the first of Chapter 26's four switches — the only one that involves no build — and its whole discipline is when the read happens: once, at startup, into a struct whose defaults are the flags' off state, and from then on a member tested with if; the source is whichever channel the plug-in already has — Recipe 26's JSON, the host's preferences API, the environment as here — and the struct is what makes that not matter. A flag that must change a type's layout is not this recipe but Chapter 26's PUBLIC compile definition. The harness proves the read-once the only way it can be proved — it changes the environment after the constructor ran and asserts the member did not follow — and the broken shape, std::getenv inside process, stays book-only, because it would pass every assertion there. Needs <charconv>, <cstdlib>, <string_view>.

Trap: reading the flag at the point of use — std::getenv in the loop, a configuration lookup per call — is a walk of a shared table — under a lock, on macOS and Windows — on Chapter 36's deadline path, and nothing names it: it compiles, runs, passes, and the sanitizers are silent, so the constructor is the only place the read may live.

Recipe 32 — Combine flags as an enum class

In C#: [Flags] enum Channel { Left = 1, Right = 2 }, then Channel.Left | Channel.Right and set.HasFlag(Channel.Left)

The recipe:

enum class Channel : std::uint8_t { None = 0, Left = 1, Right = 2, Sub = 4 };   // [Flags] enum Channel

constexpr Channel operator|(Channel a, Channel b) {
    return static_cast<Channel>(static_cast<std::uint8_t>(a) | static_cast<std::uint8_t>(b));
}
constexpr Channel operator&(Channel a, Channel b) {
    return static_cast<Channel>(static_cast<std::uint8_t>(a) & static_cast<std::uint8_t>(b));
}
constexpr bool has(Channel set, Channel flag) { return (set & flag) == flag; }   // set.HasFlag(flag)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Channel(u8);    // [Flags] enum Channel: a newtype over the bits, not an enum

impl Channel {
    pub const NONE: Channel = Channel(0);
    pub const LEFT: Channel = Channel(1);
    pub const RIGHT: Channel = Channel(2);
    pub const SUB: Channel = Channel(4);

    pub const fn has(self, flag: Channel) -> bool {
        self.0 & flag.0 == flag.0    // set.HasFlag(flag)
    }
}

impl std::ops::BitOr for Channel {
    type Output = Channel;
    fn bitor(self, rhs: Channel) -> Channel {
        Channel(self.0 | rhs.0)
    }
}

impl std::ops::BitAnd for Channel {
    type Output = Channel;
    fn bitand(self, rhs: Channel) -> Channel {
        Channel(self.0 & rhs.0)
    }
}

Why it looks like this. [Flags] is a promise to the formatter and to HasFlag; the arithmetic itself C# gives every enum for free. An enum class gives you the type and refuses the arithmetic — Channel::Left | Channel::Right is invalid operands to binary expression until you write the operator — and the two operators plus has are the attribute's entire job, once per enum; a plain enum would compile the | and hand back an int, the type gone. The std::uint8_t base fixes the width the standard leaves to the compiler, which is Chapter 39's reason for never publishing an enum across a boundary at all; inside one, it keeps the bits the width the field holds. And has(set, Channel::None) is true for every set, exactly as HasFlag(0) is. Needs <cstdint>. In Rust a [Flags] enum is not an enum — an enum value must be one of its variants — but a newtype over the bits with associated constants and BitOr; the bitflags crate generates exactly that.

Trap: (set & flag) != Channel::None reads as HasFlag and is wrong for a combined flag — with Stereo = Left | Right, a set holding only Left tests true — which is why has compares against the flag itself, as HasFlag does.

Recipe 33 — Hold an owned object as a field

In C#: private readonly Log _log; — and, if Log is IDisposable, an IDisposable on the owner whose Dispose calls _log.Dispose() by hand

The recipe:

class Log {                                  // polymorphic: lives behind a pointer (Chapter 2)
public:
    virtual ~Log() = default;
    virtual void write(const std::string& line) = 0;
};

struct Sink {                                // shared with a callback: co-owned (Chapter 29)
    std::vector<int> samples;
};

class Session {
public:
    Session(std::string name, std::unique_ptr<Log> log, std::shared_ptr<Sink> sink)
        : name_(std::move(name)), log_(std::move(log)), sink_(std::move(sink)) {}

    void record(int sample) {
        sink_->samples.push_back(sample);
        if (log_) {                          // the pointer is where "may be absent" lives
            log_->write(name_ + ": recorded");
        }
    }

private:
    std::string name_;                       // by value: the field IS the object, and dies with the owner
    std::vector<int> history_;               // by value too: its elements are on the heap, the field is three pointers
    std::unique_ptr<Log> log_;               // one owner, polymorphic, optional: behind a unique_ptr
    std::shared_ptr<Sink> sink_;             // co-owned: alive while anyone still holds it
};   // no Dispose to write: the fields die in reverse order of declaration, then the object
pub trait Log {                              // polymorphic: lives behind a Box (Chapter 2)
    fn write(&mut self, line: &str);
}

#[derive(Default)]
pub struct Sink {                            // shared with a callback: co-owned (Chapter 29)
    pub samples: Vec<i32>,
}

pub struct Session {
    name: String,                            // by value: the field IS the object, and dies with the owner
    history: Vec<i32>,                       // by value too: its elements are on the heap, the field is three words
    log: Option<Box<dyn Log>>,               // one owner, polymorphic, optional: Option says "may be absent"
    sink: Rc<RefCell<Sink>>,                 // co-owned: alive while anyone still holds it; RefCell because it is mutated
}

impl Session {
    pub fn new(name: String, log: Option<Box<dyn Log>>, sink: Rc<RefCell<Sink>>) -> Self {
        Self { name, history: Vec::new(), log, sink }
    }

    pub fn record(&mut self, sample: i32) {
        self.history.push(sample);
        self.sink.borrow_mut().samples.push(sample);
        if let Some(log) = &mut self.log {   // the Option is where "may be absent" lives
            log.write(&format!("{}: recorded", self.name));
        }
    }
}   // no Dispose to write: the fields drop in declaration order, then the struct

Why it looks like this. The C# question — can a field own something, and who disposes it — has a shorter answer here: every field is destroyed when its owner is, with nothing to write. What is yours to decide is the field's shape, and it is Appendix H's fourth procedure applied to one member: by value until something forces otherwise — a std::string or std::vector field already keeps its bulk on the heap; behind a unique_ptr when the type is a polymorphic base (Chapter 2), may be absent, is incomplete (Chapter 30's PIMPL) or is too big to carry (Recipe 34); behind a shared_ptr only when co-owned and the cycle question is answered (Chapter 1). A unique_ptr field also settles the class's copies — Chapter 6's Rule of Zero: copy deleted, move generated, nothing written. Needs <memory>, <string>, <vector>. In Rust Box<dyn Log> is the polymorphic owner, Option is where absence lives rather than the pointer, and the shared sink needs Rc<RefCell<Sink>> because sharing and mutating are two separate permissions.

Trap: fields die in reverse declaration order, so a field that another field's destructor uses must be declared before it — declare a by-value Sink after a Log whose destructor writes a last line into it, and that line lands in a dead field; nothing warns, because -Wreorder is about the constructor's list, not the class's, and under libc++ the sanitizers stay quiet too, since the container annotation un-poisons the slot before the write (Chapter 32's first pitfall).

Recipe 34 — An object too big for the stack

In C#: nothing to decide — a class instance is on the heap at forty bytes and at forty megabytes; only a struct or a stackalloc sees the stack, and when one of those is too big the runtime at least says StackOverflowException

The recipe:

struct FrameBuffer {
    std::array<std::uint8_t, 4 * 1024 * 1024> pixels{};   // 4 MB inline: a class this size has no business on a stack
};
static_assert(sizeof(FrameBuffer) > 1024 * 1024, "FrameBuffer is a heap object by design");

std::unique_ptr<FrameBuffer> make_frame() {
    return std::make_unique<FrameBuffer>();  // one owner on the stack, four megabytes on the heap
}
pub struct FrameBuffer {
    pub pixels: Box<[u8]>,    // 4 MB on the heap; the struct itself is two words
}

pub fn make_frame() -> FrameBuffer {
    // vec! allocates the bytes directly on the heap. Box::new([0u8; N]) would
    // build the array on the stack first and then copy it - and 4 MB of stack
    // is the overflow Recipe 34 exists to avoid.
    FrameBuffer { pixels: vec![0u8; 4 * 1024 * 1024].into_boxed_slice() }
}

Why it looks like this. Chapter 1's decision asked whether the object outlives its scope and whether it has one owner; this is the third question, and C# never asked it because the runtime answered it for every class. A stack frame is small — Chapter 3's numbers: 1 MB per thread on Windows, 512 KB for a thread you spawn on macOS, 8 MB for the main thread on both Linux and macOS — and sizeof is transitive, so a std::array member this size makes every object that holds one, and every function that holds one of those, a stack overflow waiting for a thread whose stack you did not size — Chapter 29's driver thread. make_unique puts the bytes on the heap and leaves a pointer-sized owner behind, which is the same ownership as before at a different address. When the size is not a compile-time constant, std::vector<std::uint8_t>(n) is the same answer with the count decided at run time. The static_assert is the reason for the heap written down where it cannot go stale (Chapter 41's judge): a reviewer who changes the array's size meets the sentence. Needs <array>, <cstdint>, <memory>. In Rust the trap has a spelling: Box::new([0u8; N]) builds the array on the stack and then moves it, so a large object is made with vec![0; N].into_boxed_slice(), which allocates in place — the test builds one on a 256 KB thread stack to prove it.

Trap: the failure is a crash on entry to the function, before its first line runs, and the report is none of Chapter 31's four shapes — AddressSanitizer names it stack-overflow only when the faulting write lands within 64 KB of the stack pointer, and a bare SEGV/BUS "on unknown address" otherwise, which depends on what happens to be mapped below the thread's stack rather than on the platform or the frame size (the same binary answers differently between runs on Linux) — with no allocation site to read either way.

Recipe 35 — Walk a JSON document you do not own

In C#: foreach (var p in root.EnumerateObject()), element.TryGetProperty("delay_ms", out var d), EnumerateArray()

The recipe:

struct Channel {
    std::string name;
    double gain = 1.0;
    std::optional<int> delay_ms;          // present on some channels, absent on others
};

std::vector<Channel> read_channels(const json& doc) {
    std::vector<Channel> out;
    for (const auto& [name, node] : doc.at("channels").items()) {   // an object: its keys and values
        Channel c;
        c.name = name;
        c.gain = node.value("gain", c.gain);                       // absent: the default
        if (node.contains("delay_ms")) {                           // TryGetProperty
            c.delay_ms = node.at("delay_ms").get<int>();
        }
        out.push_back(std::move(c));
    }
    return out;
}

int count_numbers(const json& node) {         // walk anything: objects, arrays, scalars, nested
    if (node.is_number()) {
        return 1;
    }
    if (!node.is_structured()) {              // a string, a bool, null: nothing inside
        return 0;
    }
    int n = 0;
    for (const auto& child : node) {          // an array yields its elements, an object its values
        n += count_numbers(child);
    }
    return n;
}

Still no JSON in the standard library (Recipe 25): walking a document you do not own is serde_json::Value, matched on as an enum — the is_structured question becomes a match arm, exhaustively. A dependency this crate does not take.

Why it looks like this. Recipes 25 and 26 mapped a document onto a type you own; this is the other case, a document whose shape belongs to somebody else — the host's project file, a vendor's telemetry — where you walk what is there rather than declare what must be. items() is EnumerateObject, a key and a value per pass, unpacked with Chapter 10's structured bindings; contains plus at is TryGetProperty split into its two halves, and a field that may be absent lands in a std::optional (Recipe 19) rather than in a sentinel — value covers absence only, and it converts by the default's type, so present with the wrong kind throws type_error and 3.5 against an int default truncates (the trap below). A plain range-for over a node is the generic walk: an array yields its elements, an object its values, and is_structured() is the guard that stops a string, a bool or a number from being iterated as a one-element sequence of itself — which the library does, and which turns a recursive walk into Recipe 34's stack-overflow. The walk's depth is the document's nesting, and the parser will not refuse a deep one for you (its callback form takes a depth), so on a hostile document it is the walk, not the parse, that dies. Keys iterate in sorted order, not file order, because the object is a map — Recipe 25's point from the reading side. And items() is a view of the document — Recipe 26's trap in loop form: for (... : json::parse(text).items()) keeps the range expression alive and not the temporary its member call was made on, a stack-use-after-scope under ASan until C++23, so the document is named first. Needs <nlohmann/json.hpp> (-isystem exercises/third_party), <optional>, <string>, <vector>, and using json = nlohmann::json;.

Trap: get<int>() is a static_cast per number kind — on 3.5 it returns 3 and on 3000000000 it wraps, with no error and no sanitizer opinion, while on 1e300 it is undefined behavior that UBSan reports from inside json.hpp — so a field that must be an integer is checked for kind with is_number_integer() and for range by you, and is_number() guards neither.

Recipe 36 — Hash bytes

In C#: SHA256.HashData(bytes), Convert.ToHexString(hash)

The recipe:

using Bytes = std::vector<std::uint8_t>;

std::string hex(const Bytes& bytes) {                        // Convert.ToHexStringLower (.NET 9); ToHexString is UPPER-case
    static constexpr char digits[] = "0123456789abcdef";
    std::string out;
    for (const std::uint8_t b : bytes) {
        out += digits[b >> 4];
        out += digits[b & 0x0F];
    }
    return out;
}

// Recipe 36 - SHA256.HashData(bytes)
Bytes sha256(std::string_view data) {
    Bytes digest(EVP_MAX_MD_SIZE);
    unsigned int written = 0;
    if (EVP_Digest(data.data(), data.size(), digest.data(), &written, EVP_sha256(), nullptr) != 1) {
        throw std::runtime_error("EVP_Digest failed");        // the event pole: the library itself broke
    }
    digest.resize(written);                                    // 32 for SHA-256
    return digest;
}

Rust's standard library has no cryptography, by design. The ecosystem's answers are the RustCrypto crates (sha2 here) and ring; a hash is a dependency, which is Chapter 27's decision, not this page's, and the crate stays dependency-free.

Why it looks like this. There is no System.Security.Cryptography: the standard library ships no hash, no cipher and no random source guaranteed fit for a key, which puts every one of them where Chapter 27 put networking — a dependency you choose, add and build, and read as a Chapter 16 shape when it arrives; this one is OpenSSL's libcrypto through its EVP interface, a Shape 1 C API with an integer status from every operation, and Chapter 27 names the alternatives. The rule is Chapter 27's, and harder than C#'s because nothing is in the box: never write a primitive, and prove the one you chose against a published vector — the harness holds this function to NIST's digest of abc — since a hash that agrees with itself proves nothing about whether it agrees with the .NET side. Compare the bytes, not the strings: Convert.ToHexString is upper-case, this hex is lower-case, and ToHexStringLower arrived in .NET 9. Needs <openssl/evp.h> and a link against libcrypto — pkg-config --cflags --libs libcrypto, which build_all.sh adds under its probe and check.sh does not — <cstdint>, <stdexcept>, <string>, <string_view>, <vector>. In CMake the same link is find_package(OpenSSL) and OpenSSL::CryptoAppendix J's entry.

Trap: sha256(text) hashes bytes, and a C# string is UTF-16 — SHA256.HashData(Encoding.UTF8.GetBytes(s)) and this function agree, SHA256.HashData(MemoryMarshal.AsBytes(s.AsSpan())) does not, and both are correct hashes of different bytes; Chapter 9's rule that the encoding is named applies to every byte that is hashed, signed or sealed.

Recipe 37 — Seal bytes for a reader in C

In C#: new AesGcm(key, tagSizeInBytes: 16).Encrypt(nonce, plaintext, ciphertext, tag) — and the layout of the file or message those three buffers go into, which C# never decided for you either

The recipe:

using Key   = std::array<std::uint8_t, 32>;                    // AES-256: the key size is the algorithm's name
using Nonce = std::array<std::uint8_t, 12>;                    // 96 bits: what GCM and AesGcm both expect
constexpr std::size_t kTagSize = 16;                           // the authentication tag: full length, always

using CipherCtx = std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)>;   // Recipe 7's shape

// The envelope, and the whole of the cross-language contract:
//   nonce (12 bytes) || ciphertext (plain.size() bytes) || tag (16 bytes)
// Every reader - C#, Python, the next version of this plug-in - opens it
// by reading those three lengths back, so the layout is an ICD (Chapter 34).
Bytes seal(const Key& key, const Nonce& nonce, const Bytes& plain) {
    CipherCtx ctx(EVP_CIPHER_CTX_new(), &EVP_CIPHER_CTX_free);
    if (!ctx || EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, key.data(), nonce.data()) != 1) {
        throw std::runtime_error("AES-256-GCM init failed");
    }
    Bytes out(nonce.begin(), nonce.end());
    out.resize(nonce.size() + plain.size() + kTagSize);
    std::uint8_t* const ciphertext = out.data() + nonce.size();
    int n = 0;
    if (EVP_EncryptUpdate(ctx.get(), ciphertext, &n, plain.data(), static_cast<int>(plain.size())) != 1 ||
        EVP_EncryptFinal_ex(ctx.get(), ciphertext + n, &n) != 1 ||                 // GCM: no padding, n is 0 here
        EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, kTagSize, ciphertext + plain.size()) != 1) {
        throw std::runtime_error("AES-256-GCM seal failed");
    }
    return out;
}

// Absence is the verdict: a wrong key, a flipped byte, a truncated envelope
// all come back as nullopt (Recipe 19), and no unauthenticated byte leaves this
// function - DecryptUpdate fills the buffer, DecryptFinal_ex checks the tag, and
// the buffer is returned only past that check, and wiped when it fails.
std::optional<Bytes> open_sealed(const Key& key, const Bytes& sealed) {
    if (sealed.size() < std::tuple_size<Nonce>::value + kTagSize) {
        return std::nullopt;
    }
    const std::uint8_t* const nonce      = sealed.data();
    const std::uint8_t* const ciphertext = nonce + std::tuple_size<Nonce>::value;
    const std::size_t length = sealed.size() - std::tuple_size<Nonce>::value - kTagSize;
    std::array<std::uint8_t, kTagSize> tag{};
    std::copy(sealed.end() - kTagSize, sealed.end(), tag.begin());

    CipherCtx ctx(EVP_CIPHER_CTX_new(), &EVP_CIPHER_CTX_free);
    if (!ctx || EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, key.data(), nonce) != 1) {
        throw std::runtime_error("AES-256-GCM init failed");           // the library, not the envelope: the event pole
    }
    Bytes plain(length);
    int n = 0;
    if (EVP_DecryptUpdate(ctx.get(), plain.data(), &n, ciphertext, static_cast<int>(length)) != 1 ||
        EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, kTagSize, tag.data()) != 1 ||
        EVP_DecryptFinal_ex(ctx.get(), plain.data() + n, &n) != 1) {       // the tag check lives HERE
        OPENSSL_cleanse(plain.data(), plain.size());                       // what AesGcm.Decrypt does before it throws
        return std::nullopt;
    }
    return plain;
}

Rust's standard library has no cryptography either (Recipe 36): aes-gcm from RustCrypto seals and opens the same nonce‖ciphertext‖tag envelope, and the layout stays the ICD it is here. A dependency this crate does not take.

Why it looks like this. The cipher is the easy half — AesGcm with a 32-byte key is AES-256-GCM, and authenticated means a flipped byte is a refusal rather than garbage, Chapter 8's value pole, so the return is an optional (a library that cannot even start is the event pole, and throws, as in Recipe 36). The half nothing decides for you is the envelope: AesGcm.Encrypt hands the C# side three separate buffers and says nothing about how they travel, and the moment your bytes must open on another machine, the nonce length, the tag length and the order the three are written in are a wire format in Chapter 34's sense — documented offsets, and a published test vector as the oracle. The comment above seal is that document — the 16-byte tag is one of its rows, so the C# side hands Decrypt a 16-byte tag span, which the .NET 8 constructor's tagSizeInBytes fixes — and the harness holds the function to the GCM specification's own test cases 13 and 14, because a round trip proves only that seal and open_sealed agree with each other. When the envelope is wrong, the C# side's refusal is AuthenticationTagMismatchException (.NET 8; a bare CryptographicException before), and it names nothing — the envelope is the first suspect. Needs <openssl/evp.h> and libcrypto as Recipe 36, <array>, <algorithm>, <memory>, <optional>.

Trap: a nonce reused under one key breaks GCM outright — not weakens, breaks — and a counter that restarts at process start, or a std::rand() seeded from the clock, will reuse one; the nonce is twelve bytes from the library's own generator (RAND_bytes, which the harness uses), travels in the clear at the front of the envelope, and is never a secret and never repeated — and nothing will tell you when it was.

Recipe 38 — Save a file without losing the old one

In C#: File.Move(tmp, path, overwrite: true) (.NET Core 3.0+), or File.Replace(tmp, path, null) once path exists — the write-then-move everyone ends up writing by hand around File.WriteAllText, once a customer has sent in a half-written preferences file

The recipe:

void save_file(const std::filesystem::path& path, const std::string& text) {
    std::filesystem::path tmp = path;
    tmp += ".tmp";                           // += on purpose: a suffix, not a segment - same directory, same volume
    write_all_text(tmp, text);               // Recipe 9: flushed and checked, or it threw and path is untouched
    std::filesystem::rename(tmp, path);      // one atomic step: a reader sees the old file or the new, never half
}
pub fn save_file(path: &Path, text: &str) -> std::io::Result<()> {
    let mut tmp = path.as_os_str().to_owned();
    tmp.push(".tmp");                       // a suffix, not a segment: same directory, same volume
    let tmp = std::path::PathBuf::from(tmp);
    std::fs::write(&tmp, text)?;            // Recipe 9: written and closed, or the ? returned and path is untouched
    std::fs::rename(&tmp, path)             // one atomic step: a reader sees the old file or the new, never half
}

Why it looks like this. Recipe 9 writes in place, which is fine until the process dies halfway — a crash, a host that kills the plug-in — and leaves a file that is neither the old one nor the new one. The fix is two files and one step: the bytes go to a sibling in the same directory, flushed and checked, and rename moves the name onto them in one step that POSIX rename(2) promises is atomic over an existing file (on Windows the same call is a replace-existing MoveFileEx, which every atomic-save library there relies on without the documentation saying the word) — so any reader sees the old contents or the new, never a torn middle, and a crash before the rename leaves the old file whole and a .tmp beside it that the next save overwrites (unlike File.Replace, this also works when there is no old file yet). That covers the process dying; a power cut is one step further, because the operating system may still hold the temp file's bytes in memory when the rename lands, and the fsync that pins them to disk first has no standard-library spelling — POSIX fsync, Windows FlushFileBuffers — so this recipe is crash-safe and one call short of power-safe. Two smaller things Recipe 9 supplies: its checked flush is why the bytes are complete, and its ofstream closing itself at the end of the call (Chapter 1) is why the handle is gone before rename runs — on Windows a file you still hold open cannot be renamed. Same directory is load-bearing: a rename across volumes is a copy that is not atomic, and the two POSIX standard libraries report it as std::errc::cross_device_link rather than doing it quietly (the harness asserts that on Linux, where the CI runner has a second volume to try) — while MSVC's does the opposite, passing MOVEFILE_COPY_ALLOWED so that across volumes it copies and deletes, silently and non-atomically. Same directory is how you never find out which you got. Needs <filesystem>, <string>, and Recipe 9. In Rust it is the same two calls, fs::write to the sibling name and fs::rename over the target, and ? on each is the part C# hid inside File.Replace.

Trap: the rename gives the name a new file, so anything holding the old one open keeps the old one — on POSIX a stale inode no path reaches any more; on Windows the rename itself fails while a reader holds the target open without FILE_SHARE_DELETE, which a default FileStream does not — and the harness's own judge is that inode: a save that rewrote the file in place would pass every other check and still tear.

Recipe 39 — Create, copy, move and delete, and a whole tree

In C#: Directory.CreateDirectory(dir), File.Copy(src, dst, overwrite: true), File.Move(src, dst), Directory.Delete(dir, recursive: true)

The recipe:

void rotate_export(const fs::path& export_dir, const fs::path& fresh_report) {
    fs::create_directories(export_dir / "archive");           // parents included; already there is not an error
    const fs::path current = export_dir / "report.txt";
    if (fs::exists(current)) {
        fs::copy_file(current, export_dir / "archive" / "previous.txt",
                      fs::copy_options::overwrite_existing);  // File.Copy(overwrite: true): both defaults refuse
    }
    fs::rename(fresh_report, current);                        // File.Move onto the name - which REPLACES here, and throws in C#
}

std::uintmax_t purge(const fs::path& dir) {
    return fs::remove_all(dir);    // Directory.Delete(recursive: true): the count removed, 0 if nothing was there
}
pub fn rotate_export(export_dir: &Path, fresh_report: &Path) -> std::io::Result<()> {
    std::fs::create_dir_all(export_dir.join("archive"))?;    // parents included; already there is not an error
    let current = export_dir.join("report.txt");
    if current.exists() {
        std::fs::copy(&current, export_dir.join("archive").join("previous.txt"))?;    // copy overwrites; C#'s default refuses
    }
    std::fs::rename(fresh_report, &current)    // File.Move onto the name - which REPLACES here, and throws in C#
}

pub fn purge(dir: &Path) -> std::io::Result<()> {
    match std::fs::remove_dir_all(dir) {    // Directory.Delete(recursive: true)
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),    // nothing there is not a failure
        other => other,
    }
}

Why it looks like this. Four calls, four C# names, and two places the defaults differ — in opposite directions. create_directories is Directory.CreateDirectory exactly: parents made, an existing directory not an error. copy_file is File.Copy, defaults included: with no options it refuses an existing target, throwing filesystem_error with errc::file_exists, and overwrite_existing is the line C# spells overwrite: true. rename is File.Move with the opposite default: an existing target is replaced — Recipe 38's atomic replace — where File.Move throws IOException until you pass overwrite: true, so the one call that quietly destroys a file here is the one that would have thrown in C#. remove_all is Directory.Delete(..., recursive: true) and File.Delete in one — it returns the count, and where Directory.Delete throws for a path that was never there, it returns zero. The rest of the family maps by name: file_size, temp_directory_path, and last_write_time, which hands back a file_time_type that C++17 cannot portably print or convert — compare two of them, and leave formatting to C++20's clock_cast or the platform. Every one ships as Chapter 8's pair, throwing or error_code. Needs <filesystem>, <cstdint>, and namespace fs = std::filesystem;. In Rust fs::copy overwrites and fs::rename replaces without being asked, exactly as here, and remove_dir_all on a missing directory is an Err the recipe turns back into Ok — the one place the C++ and Rust defaults differ.

Trap: dir / name with an empty name is dir/ — the separator and nothing after it — so remove_all(dir / entry) where entry came back empty from a lookup deletes the directory itself and everything in it, not one entry, and compiles clean; Path.Combine(dir, "") is dir by a shorter spelling and the same deletion, and the harness asserts this one: two files and a subdirectory gone, and the directory with them.

Recipe 40 — Notice a file changed

In C#: var w = new FileSystemWatcher(dir, "settings.json"); w.Changed += OnChanged; w.EnableRaisingEvents = true;

The recipe:

class FileWatcher {
public:
    FileWatcher(std::filesystem::path path, std::chrono::milliseconds interval,
                std::function<void()> on_change)
        : path_(std::move(path)),
          seen_(Snapshot(path_)),
          worker_([this, interval, on_change = std::move(on_change)] {
              while (!stop_) {
                  std::this_thread::sleep_for(interval);     // Recipe 16: a thread you own, blocked
                  if (stop_) {
                      break;
                  }
                  const Stamp now = Snapshot(path_);
                  if (now != seen_) {                        // !=, never >: a restored backup is OLDER
                      seen_ = now;
                      on_change();                           // on THIS thread - Chapter 29's rules apply
                  }
              }
          }) {}

    ~FileWatcher() {
        stop_ = true;
        worker_.join();    // Chapter 29's obligation, and the promise that no callback follows
    }
    FileWatcher(const FileWatcher&) = delete;
    FileWatcher& operator=(const FileWatcher&) = delete;

private:
    // What "changed" means to a poll: the time, the size, and whether it is
    // there at all. Absence is a state (Chapter 8's error_code overloads),
    // not an exception on the watcher's thread.
    struct Stamp {
        std::filesystem::file_time_type written{};
        std::uintmax_t size = 0;
        bool exists = false;
        bool operator!=(const Stamp& o) const {
            return written != o.written || size != o.size || exists != o.exists;
        }
    };
    static Stamp Snapshot(const std::filesystem::path& p) {
        std::error_code ec;
        Stamp s;
        s.exists = std::filesystem::is_regular_file(p, ec);
        if (s.exists) {
            s.written = std::filesystem::last_write_time(p, ec);
            if (!ec) {
                s.size = std::filesystem::file_size(p, ec);
            }
            if (ec) {
                s = Stamp{};    // it went away between the calls: absent, not a phantom of min() and -1
            }
        }
        return s;
    }

    std::filesystem::path path_;
    Stamp seen_;                        // the worker's alone once it starts
    std::atomic<bool> stop_{false};     // declared before worker_: initialized first (Recipe 16)
    std::thread worker_;
};
// What "changed" means to a poll: the time, the size, and whether it is
// there at all. Absence is a state, not a panic on the watcher's thread.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
struct Stamp {
    written: Option<SystemTime>,
    size: u64,
    exists: bool,
}

fn snapshot(path: &Path) -> Stamp {
    match std::fs::metadata(path) {    // one call answers all three; an Err is "absent"
        Ok(meta) if meta.is_file() => Stamp { written: meta.modified().ok(), size: meta.len(), exists: true },
        _ => Stamp::default(),
    }
}

pub struct FileWatcher {
    stop: Arc<AtomicBool>,
    worker: Option<std::thread::JoinHandle<()>>,
}

impl FileWatcher {
    pub fn new(path: PathBuf, interval: Duration, mut on_change: impl FnMut() + Send + 'static) -> Self {
        let stop = Arc::new(AtomicBool::new(false));
        let seen_stop = Arc::clone(&stop);
        let worker = std::thread::spawn(move || {
            let mut seen = snapshot(&path);
            while !seen_stop.load(Ordering::Relaxed) {
                std::thread::sleep(interval);    // Recipe 16: a thread you own, blocked
                if seen_stop.load(Ordering::Relaxed) {
                    break;
                }
                let now = snapshot(&path);
                if now != seen {    // !=, never >: a restored backup is OLDER
                    seen = now;
                    on_change();    // on THIS thread - Chapter 29's rules apply
                }
            }
        });
        Self { stop, worker: Some(worker) }
    }
}

impl Drop for FileWatcher {
    fn drop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(worker) = self.worker.take() {
            let _ = worker.join();    // Chapter 29's obligation, and the promise that no callback follows
        }
    }
}

Why it looks like this. The standard library has no watcher, and the native ones — inotify on Linux, FSEvents and kqueue on macOS, ReadDirectoryChangesW on Windows — are three shapes with three coalescing rules, which is where FileSystemWatcher's folklore comes from (two Changed events for one save, an Error when the buffer overflows) and why a plug-in that needs one takes a library (efsw is the small portable one) or, better, the host's own change notification if the SDK offers it. A poll is the spelling every platform shares: Recipe 16's worker thread — and Recipe 16's trap with it, because a FileSystemWatcher you forgot to dispose kept raising into a target the GC kept alive, where a watcher outliving its target here is a callback into freed memory, which is what the join in the destructor and the harness's silence after the brace are for — a Stamp of time, size and presence per interval, and a callback delivered on that thread, so everything it touches is Chapter 29's shared state, and in a plug-in the callback posts to the host's queue (Chapter 38) rather than calling the SDK. Absence is read through Chapter 8's error_code overloads, and read, because a file can vanish between the three calls and a stamp assembled from their error returns is a change that never happened — a missing file is a state the watcher must report, not an exception on a thread with no handler. The comparison is !=, not >: a backup restored over the file with its timestamps preserved carries an older time, and a watcher that asked "is it newer?" would sleep through it — the harness restores one and asserts the wake. And a poll can still catch a file half-written by an in-place save and report one change twice, exactly FileSystemWatcher's double event, so the callback should be safe to run twice. Needs <atomic>, <chrono>, <cstdint>, <filesystem>, <functional>, <system_error>, <thread>, <utility>. In Rust the poll is fs::metadata, which answers all three questions in one call and reports absence as an Err the watcher treats as a state; the OS-notification version is the notify crate, and it carries the same two traps.

Trap: a poll reads the timestamp at the filesystem's resolution, not the clock's — nanoseconds on APFS and ext4, hundreds of them on NTFS, whole seconds on HFS+ and many network shares, two on FAT — so two same-size writes inside one tick are one event or none; and many editors save by Recipe 38's rename, so a watch on the inode — what inotify attaches its watch to, and what kqueue's open descriptor names — is watching a ghost after the first save; watch the path, as this one does.

Recipe 41 — Call an HTTP endpoint

In C#: var text = await http.GetStringAsync(url); — one call, and one HttpRequestException for the transport's failures and the server's non-success codes alike (the timeout alone arrives as a TaskCanceledException)

The recipe:

// Recipe 41 - HttpClient.GetStringAsync, through the C API the ecosystem uses
// (curl_global_init(CURL_GLOBAL_DEFAULT) runs once per process before this,
// on the thread that starts the others - see the Why.)
using Easy = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>;   // Recipe 7's shape: the cleanup is the type

// Two verdicts, both kept: the transport's (did the bytes arrive?) and the
// server's (are they the answer?). HttpClient folded them into one
// exception; here each is data, and ok() is the question most callers ask.
struct HttpResult {
    CURLcode transport = CURLE_OK;    // DNS, connect, TLS, timeout: the wire's opinion
    long status = 0;                  // the server's opinion; 0 when there was no server (file://)
    std::string body;
    bool ok() const { return transport == CURLE_OK && status < 400; }
};

// The trampoline (Chapter 18): libcurl calls this with the void* it was
// handed - once per CHUNK, many times per response, never once.
static std::size_t append_chunk(char* data, std::size_t size, std::size_t count, void* userdata) {
    static_cast<std::string*>(userdata)->append(data, size * count);
    return size * count;    // anything less tells libcurl to abort the transfer
}

HttpResult http_get(const std::string& url, std::chrono::milliseconds timeout) {
    Easy easy(curl_easy_init(), &curl_easy_cleanup);
    if (!easy) {
        throw std::runtime_error("curl_easy_init failed");    // the library itself: Chapter 8's event pole
    }
    HttpResult r;
    // setopt's own return is unchecked on purpose: the options below fail
    // only for a build that lacks them, and a URL it cannot parse is
    // refused by perform, where the verdict is read anyway.
    curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str());
    curl_easy_setopt(easy.get(), CURLOPT_WRITEFUNCTION, &append_chunk);
    curl_easy_setopt(easy.get(), CURLOPT_WRITEDATA, &r.body);
    curl_easy_setopt(easy.get(), CURLOPT_FOLLOWLOCATION, 1L);   // HttpClient's default: a 3xx is followed, not returned
    curl_easy_setopt(easy.get(), CURLOPT_TIMEOUT_MS, static_cast<long>(timeout.count()));   // Recipe 30's hand-off
    r.transport = curl_easy_perform(easy.get());              // blocks: this IS the await, spelled as a call
    curl_easy_getinfo(easy.get(), CURLINFO_RESPONSE_CODE, &r.status);
    return r;
}

The standard library has TCP sockets and nothing above them: an HTTP client is ureq (blocking, small) or reqwest (async, large), and the two verdicts — the transport's and the server's — come back as an Err and a status code respectively. A dependency this crate does not take.

Why it looks like this. There is no HttpClient because there are no sockets (Chapter 27), and the library the ecosystem reaches for is libcurl — which arrives as Chapter 16's Shape 2 with every idiom intact: an opaque CURL* from curl_easy_init that curl_easy_cleanup must reach exactly once, options set one call at a time, an integer status from curl_easy_perform, and the write callback carrying the void* you handed it, which is the trampoline Chapter 18 built — called once per chunk, so it appends and never assigns. The two verdicts are the part GetStringAsync hid: CURLcode says whether the wire delivered anything (a name that did not resolve, a timeout, a certificate it would not trust), and CURLINFO_RESPONSE_CODE is what the server thought of the request — CURLE_OK with a 500 is a successful transfer of an error page, and ok() reads both; CURLOPT_FOLLOWLOCATION is there because without it a 301 is a successful transfer of a redirect page, where HttpClient follows by default. The .count() is Recipe 30's: the duration becomes libcurl's bare integer on the one line next to the _MS option. curl_global_init runs once per process before any thread exists — the init entry point in a plug-in, never a static initializer (Chapter 32) — and a POST is the same handle turned around, Recipe 46. The harness needs no network and has two halves: a file:// fixture, the one URL scheme with nothing behind it, exercises the callback and the transport's error path, and a small loopback server — POSIX sockets, since the standard library has none — answers with a redirect to follow, a 500 whose body is an error page, and a stall the deadline cuts short, so both verdicts are judged and the timeout's unit with them. Needs <curl/curl.h> and a link against libcurl — pkg-config --cflags --libs libcurl, which build_all.sh adds under its probe and check.sh does not — <chrono>, <memory>, <stdexcept>, <string>. In CMake the same link is pkg_check_modules(... IMPORTED_TARGET libcurl) and PkgConfig::CURLAppendix J's entry.

Trap: a green CURLcode says the bytes arrived, not that they are the answer — the body of a 404 is an HTML page that parses as JSON about as well as it reads, and a caller that checked only perform's return will feed it to Recipe 26 and file the resulting parse_error under "the server is flaky".

Recipe 42 — Open a local database and run a query

In C#: using var conn = new SqliteConnection("Data Source=cache.db"); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT ..."; cmd.Parameters.AddWithValue("$t", 1); using var reader = cmd.ExecuteReader(); while (reader.Read()) ... — Microsoft.Data.Sqlite, or Dapper over it

The recipe:

// Recipe 42 - SqliteConnection, SqliteCommand, ExecuteReader: the C API underneath
using Db = std::unique_ptr<sqlite3, decltype(&sqlite3_close)>;   // Recipe 7's shape, again

Db open_database(const std::string& path) {                        // ":memory:" is a database too
    sqlite3* raw = nullptr;
    const int rc = sqlite3_open_v2(path.c_str(), &raw, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
    Db db(raw, &sqlite3_close);                                     // own it BEFORE checking: a failed open still allocates
    if (rc != SQLITE_OK) {
        throw std::runtime_error("cannot open " + path + ": " + sqlite3_errmsg(raw));
    }
    return db;
}

// A prepared statement: SqliteCommand with its parameters, owned so that
// finalize runs on every path - and it must, because a database with a live
// statement refuses to close.
class Statement {
public:
    Statement(sqlite3* db, const char* sql) : db_(db) {
        if (sqlite3_prepare_v2(db, sql, -1, &stmt_, nullptr) != SQLITE_OK) {
            throw std::runtime_error(std::string("prepare: ") + sqlite3_errmsg(db));
        }
    }
    ~Statement() { sqlite3_finalize(stmt_); }                      // null-safe by the SDK's contract
    Statement(const Statement&) = delete;
    Statement& operator=(const Statement&) = delete;

    void bind(int index, int value) { check(sqlite3_bind_int(stmt_, index, value)); }
    void bind(int index, const std::string& value) {
        // SQLITE_TRANSIENT: copy the bytes now. SQLITE_STATIC would be a loan
        // (Appendix H) that must outlive every step - the recipe does not
        // make that promise on the caller's behalf.
        check(sqlite3_bind_text(stmt_, index, value.c_str(), -1, SQLITE_TRANSIENT));
    }

    // One row, or done. The two codes are 100 and 101: successes that are not
    // zero, which is Chapter 8's "usually zero is not a contract" in production.
    bool step() {
        const int rc = sqlite3_step(stmt_);
        if (rc == SQLITE_ROW) return true;
        if (rc == SQLITE_DONE) return false;
        throw std::runtime_error(std::string("step: ") + sqlite3_errmsg(db_));   // SQLITE_BUSY too: one connection, so
                                                                                // busy IS exceptional here - see the Why
    }
    int column_int(int i) const { return sqlite3_column_int(stmt_, i); }
    std::string column_text(int i) const {
        // A LOAN (Chapter 33): valid until the next step, reset or finalize.
        // Copied out on the spot, so no caller keeps a pointer into the row.
        const unsigned char* text = sqlite3_column_text(stmt_, i);
        return text ? reinterpret_cast<const char*>(text) : "";   // NULL column: the one null there is
    }
    void reset() { check(sqlite3_reset(stmt_)); }                 // reuse the plan - and rebind EVERY parameter:
                                                                  // a reset keeps the old bindings

private:
    void check(int rc) const {
        if (rc != SQLITE_OK) throw std::runtime_error(std::string("sqlite: ") + sqlite3_errmsg(db_));
    }
    sqlite3* db_;
    sqlite3_stmt* stmt_ = nullptr;
};

// One statement with no rows to read: CREATE, INSERT, BEGIN.
void execute(sqlite3* db, const char* sql) {
    Statement s(db, sql);
    while (s.step()) {}
}

// A transaction that rolls back unless told otherwise: Chapter 1's shape
// over Chapter 8's unwinding, so a throw between BEGIN and commit() leaves
// the database as it was.
class Transaction {
public:
    explicit Transaction(sqlite3* db) : db_(db) { execute(db_, "BEGIN"); }
    ~Transaction() {
        if (!committed_) sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr);   // no throw in a destructor
    }
    void commit() { execute(db_, "COMMIT"); committed_ = true; }
    Transaction(const Transaction&) = delete;
    Transaction& operator=(const Transaction&) = delete;

private:
    sqlite3* db_;
    bool committed_ = false;
};

// The verdict a deleter cannot report: a unique_ptr's deleter returns
// nothing, so the shutdown path takes the handle back and closes it by hand.
// SQLITE_BUSY here is a statement nobody finalized - log it; the handle stays
// open, which is the leak made visible rather than the leak made worse.
int close_database(Db db) {
    return sqlite3_close(db.release());
}

struct Reading {
    int sensor;
    std::string unit;
};

// SELECT with a parameter, the rows copied out row by row.
std::vector<Reading> readings_above(sqlite3* db, int threshold) {
    Statement q(db, "SELECT sensor, unit FROM readings WHERE sensor > ?1 ORDER BY sensor");
    q.bind(1, threshold);
    std::vector<Reading> out;
    while (q.step()) {
        out.push_back({q.column_int(0), q.column_text(1)});
    }
    return out;
}

SQLite from Rust is rusqlite, a safe wrapper over the same C API: Connection, a prepared Statement that finalizes on Drop, and a Transaction that rolls back on Drop unless committed — the three shapes this recipe writes by hand. A dependency this crate does not take.

Why it looks like this. There is no ADO.NET (Chapter 27), and the native default for local storage is SQLite through its C API — Chapter 16's Shape 1 masterclass, which means Chapter 17 already trained every line above: a status from every call, results through pointers, and a matching release for everything you are handed, which three RAII types make structural — one per thing SQLite hands you and wants back: sqlite3_close for the connection, sqlite3_finalize for the statement, ROLLBACK for the transaction nobody committed. Two things the chapter could only foreshadow arrive here for real. sqlite3_step answers with 100 for a row and 101 for done — two successes, neither zero — so Chapter 8's warning that "success is usually zero" is not a contract is the shape of the loop. SQLITE_BUSY is that chapter's drill scenario 3 — a documented steady-state condition the drill files under value — and the recipe throws it anyway, deliberately: with one connection and no other process on the file, busy is exceptional here; a plug-in sharing the file with a host decides the way the drill does, with sqlite3_busy_timeout on the connection or a retry around step, before it reaches for throw. And sqlite3_column_text is the loan Chapter 33 quoted as its in-the-wild example — good until the next step, reset or finalize — so the accessor copies out on the spot; SQLITE_TRANSIENT is the same question asked in the other direction, whether SQLite may keep your pointer, and the harness binds a temporary that dies before the step to make the answer load-bearing. A prepared statement is what SqliteCommand builds on every ExecuteReader (and Prepare builds early): compiled once, rebound through reset — every parameter, because a reset keeps the old bindings. The close code at the end is the recipe's leak detector: a database with a live statement returns SQLITE_BUSY from sqlite3_close, the job FakeSdk_LiveAllocations did in Chapter 17 — and a unique_ptr's deleter returns nothing, which is why the listing ends with a function that takes the handle back and closes it by hand; a product's shutdown path should do the same and log the verdict, or it has no leak detector at all. sqlite_orm and SOCI wrap this; most native codebases speak it raw, and reading it is cheaper than a wrapper nobody else on the team uses. Needs <sqlite3.h> and a link against libsqlite3 — pkg-config --cflags --libs sqlite3, which build_all.sh adds under its probe and check.sh does not — <memory>, <stdexcept>, <string>, <vector>. In CMake the same link is find_package(SQLite3)Appendix J's entry.

Trap: sqlite3_close returning SQLITE_BUSY at shutdown is a statement somebody never finalized — Chapter 35's still-live-at-unload, one library over — and the tempting fix, sqlite3_close_v2, does not fix it: it defers the close until the last statement is finalized, which for a leaked one is never, so the handle, its open file and any lock a SELECT abandoned mid-rows was holding outlive your plug-in in the host's process; and a second close of the same handle is SQLITE_MISUSE returned into a deleter that discards it, read inside a library no sanitizer instruments.

Recipe 43 — Share a buffer with another process

In C#: using var mmf = MemoryMappedFile.CreateOrOpen("frames", size); using var view = mmf.CreateViewAccessor(); view.Write(0, ref frame); — the runtime picks the platform API, keeps the mapping alive, and copies the struct's bytes in exactly as it laid them out, checked against nothing on the other side — which worked, because the other side was usually .NET too (and CreateOrOpen is [SupportedOSPlatform("windows")]: a named map throws PlatformNotSupportedException everywhere else, which is its own hint about how platform-shaped this is)

The recipe:

struct Frame {
    std::uint32_t version;              // sizeof(Frame): a reader built against an older layout can tell
    std::atomic<std::uint32_t> seq;     // bumped by the writer AFTER the payload: the reader's "is it there yet"
    std::uint32_t width;
    std::uint32_t height;
    std::uint8_t  pixels[64];
};
static_assert(std::is_standard_layout<Frame>::value, "a shared layout has no vtable and no surprises");
static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
              "a lock-based atomic holds a lock that exists in ONE process");
static_assert(sizeof(Frame) == 4 + 4 + 4 + 4 + 64, "the layout is the contract; a change here is a version bump");
// Not is_trivially_copyable: an atomic has no copy at all, and what the trait
// says about that differs by standard library. And none of the three refuses
// a pointer or a std::string - both are standard-layout - so that half of
// the rule is yours to keep; the asserts hold the size, the vtable, the lock.

// One name, one mapping, two handles - the object and the view - released
// in reverse on every path. Recipe 7's shape, twice, behind one class.
class SharedRegion {
public:
    SharedRegion(const std::string& name, std::size_t size, bool create)
        : size_(size) {
#if defined(_WIN32)
        SetLastError(0);
        mapping_ = create
            ? CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,   // INVALID_HANDLE_VALUE: the paging file backs it
                                 static_cast<DWORD>(size), name.c_str())
            : OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, name.c_str());
        if (mapping_ == nullptr) {
            throw std::runtime_error("file mapping failed: " + name);
        }
        if (create && GetLastError() == ERROR_ALREADY_EXISTS) {   // Windows has no O_EXCL: a live name is RETURNED, not refused
            CloseHandle(mapping_);
            throw std::runtime_error("file mapping exists: " + name);
        }
        view_ = MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size);
        if (view_ == nullptr) {
            CloseHandle(mapping_);
            throw std::runtime_error("MapViewOfFile failed: " + name);
        }
#else
        const int flags = create ? (O_RDWR | O_CREAT | O_EXCL) : O_RDWR;   // EXCL: a stale name is an error, not a reuse
        fd_ = ::shm_open(name.c_str(), flags, 0600);
        if (fd_ < 0) {
            throw std::runtime_error("shm_open failed: " + name);
        }
        if (create && ::ftruncate(fd_, static_cast<off_t>(size)) != 0) {   // once, at creation: macOS refuses a second (EINVAL)
            ::close(fd_);
            ::shm_unlink(name.c_str());
            throw std::runtime_error("ftruncate failed: " + name);
        }
        view_ = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
        if (view_ == MAP_FAILED) {
            ::close(fd_);
            if (create) ::shm_unlink(name.c_str());
            throw std::runtime_error("mmap failed: " + name);
        }
#endif
    }

    ~SharedRegion() {
#if defined(_WIN32)
        UnmapViewOfFile(view_);
        CloseHandle(mapping_);          // the object dies with its last handle: nothing to unlink (a FILE-backed mapping leaves its file)
#else
        ::munmap(view_, size_);
        ::close(fd_);                   // the NAME stays until someone unlinks it - see unlink()
#endif
    }
    SharedRegion(const SharedRegion&) = delete;
    SharedRegion& operator=(const SharedRegion&) = delete;

    // The creator's last duty: without it the region outlives every process
    // that mapped it (Appendix G's price), and the next create fails on the
    // stale name. Windows has no such step and no such leak.
    static void unlink(const std::string& name) {
#if !defined(_WIN32)
        ::shm_unlink(name.c_str());
#else
        (void)name;
#endif
    }

    void* data() const { return view_; }

private:
    std::size_t size_;
    void* view_ = nullptr;
#if defined(_WIN32)
    HANDLE mapping_ = nullptr;
#else
    int fd_ = -1;
#endif
};

The standard library has no shared memory. memmap2 maps a file (or /dev/shm on Linux), shared_memory wraps the named-segment calls, and the layout rules — #[repr(C)], atomics that are lock-free, a version field first — are the same ICD discipline as here. A dependency this crate does not take.

Why it looks like this. No library, because the platform is the dependency: POSIX shm_open plus mmap on Linux and macOS, a pagefile-backed CreateFileMapping plus MapViewOfFile on Windows, and the first listing in the cookbook with no portable spelling at all — Recipes 29 and 38 guard one call, this one guards every call, since no standard one exists (Boost.Interprocess is the portable wrapper, and the price of it is Boost). The class is Recipe 7 twice — an object handle and a view, each released on every path in reverse; Windows has no O_EXCL, so its create branch reads ERROR_ALREADY_EXISTS back to refuse a live name the way POSIX's flag does — and the lesson that makes it this book's is in the struct, not the class: a shared region is a wire format. The bytes are read by a process with its own compiler, its own build and its own address space, so Chapter 30's one rule applies with Chapter 34's extension — fixed-width fields, a version first, no std::string, no pointer (an address in your process) — and the three static_asserts are Chapter 41's judge on what they can hold: the size, the absence of a vtable, the lock-free atomic. The pointer and the std::string they cannot refuse, since both are standard-layout, and the fork does not reliably catch them either — a child's address read by the parent is garbage, or correct until the day the mapping lands elsewhere; that half of the rule is yours to keep. (Not is_trivially_copyable, which an atomic fails on one standard library and passes on two.) This is also the overlay Chapter 34 bans for a captured wire, and here it is the tool: the region is the object's storage, the atomic must be operated in place, and the second view is read through the cast — the standard has no model of a second mapping, C++23's std::start_lifetime_as is its spelling for one, and the compiler cannot see through mmap. The counter is the whole of the synchronization: a std::atomic that is lock-free on every target here, written with release after the payload and read with acquire before it, because an atomic that needed a lock would hold one that exists in one process only (the standard recommends that lock-free also mean address-free, which is the property a second process needs); a process-shared mutex (pthread_mutexattr_setpshared, a named Win32 mutex) is the next step and not this recipe. The reader's wait is bounded (Chapter 38's judge), and the harness is the one place in this book that forks: the child maps by name, writes, bumps, and leaves, and the parent asserts the frame — on Windows the buildlab-msvc job maps one object twice in one process, which proves the mechanism and states the cross-process half as unverified there. Needs <atomic>, <cstdint>, <string>, <type_traits>; <sys/mman.h>, <fcntl.h>, <unistd.h> on POSIX (and -lrt before glibc 2.34); <windows.h> on Windows.

Trap: the name outlives every process that mapped it — close every handle, exit, and the name is still there holding the last frame, visibly on Linux as /dev/shm/name and with no path to list at all on macOS, until someone calls shm_unlink (a harness killed on an assertion leaves one behind) — and ThreadSanitizer instruments one process, so a race between two is invisible to every tool in the book, Finding 10's family with a process boundary through it. Two smaller ones the harness meets: macOS caps the name at 31 characters and allows ftruncate on the object exactly once (a second returns EINVAL).

Recipe 44 — Match a pattern

In C#: Regex.IsMatch(id, @"^sensor(\d+)$"), Regex.Match(id, @"^sensor(\d+)$").Groups[1].Value, Regex.Replace(text, @"\d+", "#")

The recipe:

std::optional<int> sensor_index(const std::string& id) {
    // Constructed ONCE. Building a std::regex parses the pattern and compiles
    // it, which is the expensive half - a function-local static pays it on the
    // first call only (Chapter 32's construct-on-first-use).
    static const std::regex pattern(R"(^sensor([0-9]+)$)");   // R"(...)" is C#'s @"..."
    std::smatch m;
    if (!std::regex_match(id, m, pattern)) {
        return std::nullopt;                       // IsMatch false: absence, not an error
    }
    const std::string digits = m[1].str();         // Groups[1], copied out: m borrows from id (Chapter 10)
    int value = 0;
    if (std::from_chars(digits.data(), digits.data() + digits.size(), value).ec != std::errc{}) {
        return std::nullopt;                       // matched, but more digits than an int holds
    }
    return value;
}

std::string redact_digits(const std::string& text) {
    static const std::regex digits(R"([0-9]+)");
    return std::regex_replace(text, digits, "#");   // Regex.Replace: every match, a new string
}
// The general answer is the regex crate, a dependency this crate does not
// take. This pattern - a fixed prefix and digits - does not need one: the
// standard library's strip_prefix and a digit check say it exactly.
pub fn sensor_index(id: &str) -> Option<u32> {
    let digits = id.strip_prefix("sensor")?;    // the anchor and the literal: None if absent
    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
        return None;    // IsMatch false: absence, not an error
    }
    digits.parse().ok()    // matched, but more digits than a u32 holds: None as well
}

pub fn redact_digits(text: &str) -> String {
    let mut out = String::with_capacity(text.len());
    let mut in_run = false;
    for c in text.chars() {    // Regex.Replace("[0-9]+", "#"): every run of digits becomes one '#'
        match (c.is_ascii_digit(), in_run) {
            (true, false) => { out.push('#'); in_run = true; }
            (true, true) => {}
            (false, _) => { out.push(c); in_run = false; }
        }
    }
    out
}

Why it looks like this. std::regex is the Regex class with the static helpers removed: the object is the compiled pattern, so the shape that matters is where it lives. Regex.IsMatch(s, pattern) hides a cache of compiled patterns behind the static call; here nothing caches for you, and a std::regex built inside the function it serves is parsed and compiled on every call — a function-local static const builds it once, on first use, thread-safely since C++11 (Chapter 32's shape). regex_match is IsMatch with ^ and $ built in — the pattern keeps them so it reads as the C# one — and regex_search is the unanchored one; smatch is the Match object, and m[1] is Groups[1]: a pair of iterators into the string you matched, Chapter 10's view, so the digits are copied out on the spot. The class is written [0-9] where C# wrote \d because that is all \d means here — bytes, the ten ASCII digits, never a Unicode category — where .NET's \d is \p{Nd} and takes every script's digits (Chapter 9's rule: a std::string is bytes). The dialect is ECMAScript, close enough to .NET's for the everyday subset — except $, which here does not match before a final \n. Needs <regex>, <optional>, <charconv>, <string>. In Rust this particular pattern needs no regex at all — strip_prefix is the anchor and the literal, and an all-digits check is the class — which is worth knowing before reaching for the regex crate, whose compile-once rule is spelled OnceLock or LazyLock rather than a function-local static.

Trap: std::regex is slow and it allocates — on this machine a match through the static const above costs about 800 nanoseconds and eleven heap allocations, where starts_with plus Recipe 19's from_chars on the same input costs a few nanoseconds and none, which the harness counts with Chapter 36's replaced operator new; and one hostile line against a pattern with nested repetition backtracks for seconds under libstdc++ and, under libc++, throws std::regex_error out of regex_match in milliseconds, which this recipe does not catch — so it belongs in a config parser and never on the per-sample path, and a regex that must be fast is a Chapter 27 dependency, RE2 or PCRE2.

Recipe 45 — Trim, compare ignoring case, prefix and suffix

In C#: s.Trim(), string.Equals(a, b, StringComparison.OrdinalIgnoreCase), s.StartsWith("sensor", StringComparison.Ordinal), s.EndsWith(".txt", StringComparison.Ordinal)

The recipe:

std::string_view trim(std::string_view s) {
    constexpr std::string_view blank = " \t\r\n";
    const auto first = s.find_first_not_of(blank);
    if (first == std::string_view::npos) {
        return {};                                 // all blank: empty - substr(npos) would throw
    }
    const auto last = s.find_last_not_of(blank);
    return s.substr(first, last - first + 1);      // a VIEW into s: the caller's string must outlive it
}

bool equals_ignore_case(std::string_view a, std::string_view b) {
    if (a.size() != b.size()) {
        return false;
    }
    for (std::size_t i = 0; i < a.size(); ++i) {   // ASCII only: bytes, not characters (Chapter 9)
        if (std::tolower(static_cast<unsigned char>(a[i])) !=      // unsigned char first: Chapter 19's UB
            std::tolower(static_cast<unsigned char>(b[i]))) {
            return false;
        }
    }
    return true;
}

bool starts_with(std::string_view s, std::string_view prefix) {
    return s.substr(0, prefix.size()) == prefix;   // C++20 spells it s.starts_with(prefix)
}

bool ends_with(std::string_view s, std::string_view suffix) {
    return s.size() >= suffix.size() && s.substr(s.size() - suffix.size()) == suffix;
}
pub fn trim(s: &str) -> &str {
    s.trim_matches(|c| c == ' ' || c == '\t' || c == '\r' || c == '\n')    // a &str INTO s: the borrow checker holds the lifetime
}

pub fn equals_ignore_case(a: &str, b: &str) -> bool {
    a.eq_ignore_ascii_case(b)    // ASCII only, and the name says so (Chapter 9)
}

pub fn starts_with(s: &str, prefix: &str) -> bool {
    s.starts_with(prefix)
}

pub fn ends_with(s: &str, suffix: &str) -> bool {
    s.ends_with(suffix)
}

Why it looks like this. Four one-liners C# has and C++17's std::string does not, each with the same shape: a string_view in, so a literal, a std::string and a substring all bind without a copy (Appendix H's view branch). trim hands back a view rather than a new string — free, and honest about what Trim allocated for you — so its result lives as long as its argument and no longer, which is Chapter 10's dangling view the moment the argument was a temporary; copy into a std::string where the trimmed text must outlive the line. Trim strips every Unicode white-space character where blank here is four bytes, so widen it if a no-break space (C2 A0 in UTF-8) can reach you. The find_first_not_of / find_last_not_of pair is the idiom, and the npos check comes first because substr(npos) throws out_of_range: without it an all-blank input would throw where Trim returns "". The case-insensitive compare is ordinal and byte-wise — tolower on an unsigned char, Chapter 19's cast, because a negative char is undefined behavior there — so it is OrdinalIgnoreCase for ASCII and not for anything else: ü and Ü differ as bytes, and the harness asserts that they do. starts_with and ends_with are C++20 members of string and string_view; on C++17 these two lines are them — and ordinal always, where a bare s.StartsWith("x") in .NET is culture-sensitive, which is what the analyzers nag about. Needs <cctype>, <string_view>. In Rust trim returns a &str into its argument the same way, and the lifetime the C++ comment asks you to remember is one the borrow checker refuses to let you forget; eq_ignore_ascii_case puts the ASCII limitation in the name.

Trap: auto t = trim(read_line()); is a view of a string that died at the semicolon — a stack-use-after-scope or heap-use-after-free under ASan, and plausible text until then; name the string first, or have your own trim return a std::string if callers keep the result.

Recipe 46 — Post a JSON body and read a JSON reply

In C#: var resp = await http.PostAsJsonAsync(url, body); resp.EnsureSuccessStatusCode(); var reply = await resp.Content.ReadFromJsonAsync<Reply>();

The recipe:

using HeaderList = std::unique_ptr<curl_slist, decltype(&curl_slist_free_all)>;   // Recipe 7's shape: a second handle type

HttpResult http_post_json(const std::string& url, const json& body, std::chrono::milliseconds timeout) {
    Easy easy(curl_easy_init(), &curl_easy_cleanup);
    if (!easy) {
        throw std::runtime_error("curl_easy_init failed");
    }
    HeaderList headers(curl_slist_append(nullptr, "Content-Type: application/json"), &curl_slist_free_all);
    if (!headers) {
        throw std::runtime_error("curl_slist_append failed");   // a null list means "no custom headers": a silent form post
    }
    const std::string payload = body.dump();        // NAMED: libcurl borrows these bytes until perform returns
    HttpResult r;
    curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str());
    curl_easy_setopt(easy.get(), CURLOPT_HTTPHEADER, headers.get());
    curl_easy_setopt(easy.get(), CURLOPT_POSTFIELDS, payload.c_str());          // a loan, not a copy (Chapter 33)
    curl_easy_setopt(easy.get(), CURLOPT_POSTFIELDSIZE, static_cast<long>(payload.size()));
    curl_easy_setopt(easy.get(), CURLOPT_WRITEFUNCTION, &append_chunk);
    curl_easy_setopt(easy.get(), CURLOPT_WRITEDATA, &r.body);
    curl_easy_setopt(easy.get(), CURLOPT_TIMEOUT_MS, static_cast<long>(timeout.count()));
    r.transport = curl_easy_perform(easy.get());
    curl_easy_getinfo(easy.get(), CURLINFO_RESPONSE_CODE, &r.status);
    return r;
}

// The third verdict, after the transport's and the server's: are the bytes
// JSON at all? A 200 whose body is an HTML page is a value here, not a
// throw - the caller asked a server a question and got a non-answer.
std::optional<json> json_reply(const HttpResult& r) {
    if (!r.ok()) {
        return std::nullopt;                        // the wire or the server said no: the body is not the answer
    }
    json parsed = json::parse(r.body, nullptr, false);   // false: no exceptions - junk is a value here
    if (parsed.is_discarded()) {
        return std::nullopt;
    }
    return parsed;
}

The standard library has TCP sockets and nothing above them (Recipe 41), so the client is a crate: ureq::post(url).send_json(body) and into_json() on the reply, with the same two verdicts kept apart, and serde_json for the body as in Recipe 25. Dependencies this crate does not take.

Why it looks like this. Recipe 41 with the request turned around and Recipe 25 on both ends of it. The header list is one more C handle with a matching free — curl_slist_free_all, Recipe 7's shape for the second time on one page — and CURLOPT_POSTFIELDS is the reason the serialized body has a name: libcurl keeps the pointer, not a copy, and reads the bytes during perform (Chapter 33's loan, with the SDK on the borrowing side). PostAsJsonAsync set the content type for you; here it is the one header the list carries, because without it libcurl's default is application/x-www-form-urlencoded, and a JSON body under that header is a form with one nonsense field to any server that decodes forms. A body of a megabyte or more also gets Expect: 100-continue, and a server that never answers it costs a one-second wait per request — curl_slist_append(headers, "Expect:") removes it. The reply is Chapter 8's decision made three times: the transport's verdict and the server's are Recipe 41's two, and the third — is this JSON at all — is a value too, because a maintenance page with a 200 on it is an answer the server gave, not an event; so json_reply parses with exceptions off and returns nullopt, where Recipe 26's parse throws because load_config is the deepest frame and a broken config abandons the whole load — json_reply stands at the edge, where Chapter 8 turns a throw into a value. What comes back is Recipe 26's document, and the required key is still at(), which throws out_of_range naming it. EnsureSuccessStatusCode and ReadFromJsonAsync folded the three verdicts into HttpRequestException for the first two and JsonException for the third (with Recipe 41's TaskCanceledException for the timeout); HttpResult keeps the first two apart and the optional carries the third, so the everyday call is r.ok(), then json_reply(r), and the reason for a nullopt is still in r. It is also the shape of a call to a hosted language model — JSON in, JSON out, a vendor's endpoint in the URL, its schema in the body and its credential as one more entry on the header list — and Chapter 27 says where in a plug-in such a call runs. Needs <curl/curl.h> and libcurl as Recipe 41, <nlohmann/json.hpp> as Recipe 25, <chrono>, <memory>, <optional>, <string>.

Trap: curl_easy_setopt(easy, CURLOPT_POSTFIELDS, body.dump().c_str()) compiles, and the temporary dies at the semicolon — libcurl reads dead memory during perform: a heap-use-after-free under ASan, or stack-use-after-scope for a body short enough to fit the small-string buffer, which is also the body that works without ASan until the payload grows; name the string, and keep it alive until perform returns.

Recipe 47 — Derive a key

In C#: Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, 32) for a password; HKDF.DeriveKey(HashAlgorithmName.SHA256, secret, 32, salt, info) for a secret that already has entropy

The recipe:

Key key_from_password(std::string_view password, const Bytes& salt, int iterations) {
    Key key{};
    if (PKCS5_PBKDF2_HMAC(password.data(), static_cast<int>(password.size()),
                          salt.data(), static_cast<int>(salt.size()),
                          iterations, EVP_sha256(),
                          static_cast<int>(key.size()), key.data()) != 1) {
        throw std::runtime_error("PBKDF2-HMAC-SHA256 failed");   // the library, not the input: the event pole
    }
    return key;
}

using DeriveCtx = std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)>;   // Recipe 7's shape, again

Key key_from_secret(const Bytes& secret, const Bytes& salt, const Bytes& info) {
    DeriveCtx ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr), &EVP_PKEY_CTX_free);
    Key key{};
    std::size_t length = key.size();
    if (!ctx || EVP_PKEY_derive_init(ctx.get()) != 1 ||
        EVP_PKEY_CTX_set_hkdf_md(ctx.get(), EVP_sha256()) != 1 ||
        EVP_PKEY_CTX_set1_hkdf_salt(ctx.get(), salt.data(), static_cast<int>(salt.size())) != 1 ||
        EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), secret.data(), static_cast<int>(secret.size())) != 1 ||
        EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data(), static_cast<int>(info.size())) != 1 ||
        EVP_PKEY_derive(ctx.get(), key.data(), &length) != 1 || length != key.size()) {
        throw std::runtime_error("HKDF-SHA256 failed");
    }
    return key;
}

Rust's standard library has no cryptography (Recipe 36): pbkdf2 and hkdf from RustCrypto, each a few lines held to the same published vectors as here. Dependencies this crate does not take.

Why it looks like this. Recipe 37 took a Key and never said where one comes from; these are the two answers, and which one is a question about the input rather than the output. A password has almost no entropy, so PBKDF2 spends time on it — iterations rounds of HMAC-SHA-256, the count chosen so one derivation costs tens of milliseconds on the machine that will run it rather than chosen as a number, since any figure quoted today is too few in a few years — to make each guess cost the attacker what it cost you; a secret that already has entropy (a key agreed elsewhere, a master key from the platform's store) only needs condensing and separating: HKDF extracts a uniform key from it and expands that to the length wanted, in a few hashes, with info naming the purpose so one secret yields different keys for different jobs. Both can produce any length; the 32 bytes here are Recipe 37's, because that is the key these two exist to feed. The two shapes are two ages of the same library: PKCS5_PBKDF2_HMAC is one call in the old style, and HKDF is the EVP_PKEY derivation context, the spelling that still builds on 1.1.1 (OpenSSL 3 also fetches a KDF by name, the way Recipe 48 fetches its MAC) — a Chapter 16 Shape 2 handle, set up one option at a time, with a status from every call and one free, which is why the chain of != 1 reads the way Recipe 37's seal does: one || per call, the first failure ending the chain. The reflex to check at the door is C#'s defaults: the older new Rfc2898DeriveBytes(password, salt) chose SHA-1 and 1000 iterations for you, which is where a drifted count on the C# side usually comes from, and the C++ call has no defaults at all — every parameter is yours to write down. Salt, iteration count and info are not secrets, and they travel: a key that must be re-derived on the C# side needs the same three, so they are a wire format in Chapter 34's sense, written down next to the envelope of Recipe 37. The harness holds both functions to published vectors — RFC 7914's PBKDF2-HMAC-SHA-256 cases and RFC 5869's first HKDF case — because a derivation that agrees with itself proves nothing about whether it agrees with .NET's — and then the Trap as a value: the same password one iteration off, and Recipe 37 refuses to open. Needs <openssl/evp.h>, <openssl/kdf.h> and libcrypto as Recipe 36, <array>, <memory>, <string_view>, <vector>.

Trap: the iteration count is part of the key — change it on one side, or let a config default drift, and the two sides derive different keys from the same password with no error anywhere, only Recipe 37's open_sealed returning nullopt; store the count and the salt beside the ciphertext, as part of the envelope.

Recipe 48 — Sign and verify bytes

In C#: new HMACSHA256(key).ComputeHash(data) (or HMACSHA256.HashData(key, data)), and CryptographicOperations.FixedTimeEquals(expected, tag) to check one

The recipe:

using Mac    = std::unique_ptr<EVP_MAC, decltype(&EVP_MAC_free)>;
using MacCtx = std::unique_ptr<EVP_MAC_CTX, decltype(&EVP_MAC_CTX_free)>;

Bytes hmac_sha256(const Bytes& key, const Bytes& data) {
    Mac mac(EVP_MAC_fetch(nullptr, "HMAC", nullptr), &EVP_MAC_free);
    MacCtx ctx(mac ? EVP_MAC_CTX_new(mac.get()) : nullptr, &EVP_MAC_CTX_free);
    char digest[] = "SHA256";                              // a writable char* by signature; a name goes here, the key through init
    const OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, digest, 0),
                                 OSSL_PARAM_construct_end()};
    Bytes tag(EVP_MAX_MD_SIZE);
    std::size_t written = 0;
    if (!ctx || EVP_MAC_init(ctx.get(), key.data(), key.size(), params) != 1 ||
        EVP_MAC_update(ctx.get(), data.data(), data.size()) != 1 ||
        EVP_MAC_final(ctx.get(), tag.data(), &written, tag.size()) != 1) {
        throw std::runtime_error("HMAC-SHA256 failed");
    }
    tag.resize(written);                                   // 32 for SHA-256
    return tag;
}

bool verify_hmac_sha256(const Bytes& key, const Bytes& data, const Bytes& tag) {
    const Bytes expected = hmac_sha256(key, data);
    // CRYPTO_memcmp, never ==: a comparison that stops at the first wrong
    // byte tells an attacker how many bytes were right (FixedTimeEquals).
    // No harness can see this line change - constant time is not a value.
    return expected.size() == tag.size() && CRYPTO_memcmp(expected.data(), tag.data(), tag.size()) == 0;
}

Rust's standard library has no cryptography (Recipe 36): hmac with sha2, whose verify_slice is the constant-time comparison this recipe's == warns about — the trap answered by the API rather than by remembering. Dependencies this crate does not take.

Why it looks like this. An HMAC is the answer to a question Recipe 37 does not ask — did the bytes I can read come from someone holding the key? — for the file or the message that is not secret but must not be forged: a settings file the plug-in wrote and must trust on re-read, telemetry for a backend of your own, a request between two processes of yours. EVP_MAC is OpenSSL 3's spelling: fetch the algorithm by name, make a context, initialise it with the key and a parameter list naming the digest — the OSSL_PARAM array is the C API's way of passing options without a function per option, and its string slot is a writable char* by declaration, which is why the name sits in a local array rather than a literal — then update and finalise, a handle with a status from every call, one more time. The verifier is the half that matters. == on two vectors stops at the first differing byte, and how long it took is something an attacker can measure, given enough samples, even over a network; CRYPTO_memcmp compares every byte whatever the answer, which is what FixedTimeEquals exists for in .NET and what SequenceEqual is not. The harness holds the function to RFC 4231's vectors and then to its own verifier: a flipped byte, a wrong key, a changed message and a short tag all refuse. Needs <openssl/evp.h>, <openssl/core_names.h>, <openssl/params.h>, <openssl/crypto.h> and libcrypto 3 as Recipe 36, <memory>, <vector>.

Trap: verify_hmac_sha256 on a licence blob compiles, runs and verifies — and the key that verifies is the key that signs, so the plug-in checking the licence on the customer's machine carries everything needed to forge one; when the verifier must not be able to sign, that is a signature, which is Recipe 54.

Recipe 49 — Read a large file without copying it

In C#: using var mmf = MemoryMappedFile.CreateFromFile(path, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); using var view = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); — or, the reflex, File.ReadAllBytes(path), fine until the file was the size of the machine's memory; the mapped form pages the file in as it is touched, and the runtime holds the file and mapping handles until the usings end, where the recipe closes both as soon as the view exists

The recipe:

// Recipe 49 - MemoryMappedFile.CreateFromFile: a file's bytes as a view,
// mapped rather than read. Pages arrive as they are touched and leave with
// the object; nothing is copied into the heap, and the file may be closed -
// or, on POSIX, deleted - the moment the mapping exists.
class MappedFile {
public:
    explicit MappedFile(const std::filesystem::path& path) {
#if defined(_WIN32)
        HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
                                  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
        if (file == INVALID_HANDLE_VALUE) throw std::runtime_error("cannot open " + path.string());
        LARGE_INTEGER size{};
        if (!GetFileSizeEx(file, &size)) { CloseHandle(file); throw std::runtime_error("cannot size " + path.string()); }
        if (size.QuadPart == 0) { CloseHandle(file); return; }      // a zero-length mapping is refused: an empty view instead
        HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr);
        CloseHandle(file);                                           // the mapping object holds its own reference
        if (mapping == nullptr) throw std::runtime_error("cannot map " + path.string());
        view_ = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
        CloseHandle(mapping);                                        // and the view holds its own
        if (view_ == nullptr) throw std::runtime_error("cannot view " + path.string());
        size_ = static_cast<std::size_t>(size.QuadPart);
#else
        const int fd = ::open(path.c_str(), O_RDONLY);
        if (fd < 0) throw std::runtime_error("cannot open " + path.string());
        struct stat st{};
        if (::fstat(fd, &st) != 0) { ::close(fd); throw std::runtime_error("cannot size " + path.string()); }
        if (st.st_size == 0) { ::close(fd); return; }                // mmap of length 0 is EINVAL: an empty view instead
        void* view = ::mmap(nullptr, static_cast<std::size_t>(st.st_size), PROT_READ, MAP_PRIVATE, fd, 0);
        ::close(fd);                                                 // the mapping keeps its own reference to the file
        if (view == MAP_FAILED) throw std::runtime_error("cannot map " + path.string());
        view_ = view;
        size_ = static_cast<std::size_t>(st.st_size);
#endif
    }
    ~MappedFile() {
        if (view_ == nullptr) return;
#if defined(_WIN32)
        UnmapViewOfFile(view_);
#else
        ::munmap(const_cast<void*>(view_), size_);
#endif
    }
    MappedFile(const MappedFile&) = delete;
    MappedFile& operator=(const MappedFile&) = delete;

    // A view into the mapping: valid exactly as long as this object is.
    std::string_view bytes() const { return {static_cast<const char*>(view_), size_}; }

private:
    const void* view_ = nullptr;
    std::size_t size_ = 0;
};

The standard library has no memory mapping. memmap2::Mmap is the view, unsafe because the file can change under it — the SIGBUS this recipe's trap names is why the constructor is unsafe there — and the honest std-only alternative is std::fs::read, which is the copy this recipe exists to avoid. A dependency this crate does not take.

Why it looks like this. Recipe 1 copies the file into a std::string, the right shape for a config and the wrong one for a capture, a log or a media file: the copy costs heap allocations the size of the file and a read of every byte before the first is looked at. A mapping asks the OS to make the file's pages appear in the process's address space as they are touched — the same mmap and MapViewOfFile as Recipe 43, with a file where that recipe had a name, and read-only, private, so the file cannot change through the view. The class is Recipe 7 for a view: the descriptor is closed the moment the mapping exists, because the mapping holds its own reference to the file — which is also why the file can be deleted under a live mapping and the bytes still read, which the harness asserts on every platform it runs on: on POSIX by design, and on Windows because the STL's remove asks for POSIX delete semantics on NTFS (the older DeleteFile refused a mapped file with ERROR_USER_MAPPED_FILE, which is what a hand-rolled delete still meets). The empty file is the branch a first draft lacks: mmap of zero bytes is EINVAL and CreateFileMapping of an empty file fails outright, so an empty file is an empty view, not an exception. bytes() is a string_view, Chapter 10's non-owning window with that chapter's rule attached: valid exactly as long as the MappedFile is, and a view kept past the object reads unmapped memory, which ASan reports as a SEGV on unknown address with no allocation site — the pages were the kernel's, never the allocator's — or, if the allocator has since reused the range, as an overflow on some unrelated heap object. The other side of that: a munmap left out of the destructor is a leak no sanitizer counts, which is the destructor's whole reason to exist. The harness maps four megabytes, compares every byte against Recipe 1's copy, and — Chapter 36's instrument — counts heap allocations across the mapping with a replaced operator new: zero, which is the recipe's whole claim over ReadAllBytes (both forms of operator new are replaced, because under ASan the array form does not route through the scalar one, and a copy made with new char[] would otherwise count as zero). Needs <filesystem>, <stdexcept>, <string>, <string_view>; <sys/mman.h>, <sys/stat.h>, <fcntl.h>, <unistd.h> on POSIX; <windows.h> on Windows.

Trap: a file that shrinks while it is mapped — another process truncating the log you are reading — is, on Linux, a SIGBUS on the first touch of a page past the new end: plain memory, no allocation site, none of Chapter 31's shapes, and no sanitizer names it; on macOS the same read completes with the old byte — scripts/check_platform_claims.sh holds each platform to its own answer. Map files nobody else writes, or copy what you need out of the view before anyone can — a mapping is not a copy, and the bytes change under you if the writer keeps writing.

Recipe 50 — Keep a helper out of every other file

In C#: internal static int ClampToRange(...) — or private static on the class that uses it; either way the compiler decides who may call it, and the unit is the assembly

The recipe:

namespace {
    // Internal linkage: this name exists in this translation unit and in no
    // other, so the identically-named helper in namespaces_other.cpp is a
    // different function and the two never collide at link time. `static` at
    // namespace scope says the same thing and is the older spelling; the
    // unnamed namespace also works for types, which `static` cannot do.
    int clamp_to_range(int value, int low, int high) {
        return value < low ? low : (value > high ? high : value);
    }
}

int normalize_reading(int raw) {
    return clamp_to_range(raw, 0, 100);   // this file's clamp, always
}
pub mod reading {
    // No `pub`: private to this module, and the compiler - not the linker -
    // is what enforces it. `pub(crate)` is C#'s `internal`; bare `pub` is
    // public. Another module may define its own `clamp_to_range` freely.
    fn clamp_to_range(value: i32, low: i32, high: i32) -> i32 {
        value.clamp(low, high)
    }

    pub fn normalize(raw: i32) -> i32 {
        clamp_to_range(raw, 0, 100)
    }
}

Why it looks like this. There is no access keyword for a free function, because the unit of privacy here is not a type or an assembly but a translation unit — the .cpp and everything it included (Chapter 12). An unnamed namespace gives its contents internal linkage: the name is this file's, so another .cpp may define clamp_to_range with a different body and the linker is never asked to choose. That is the property the harness demonstrates the only way it can, with a second translation unit that does exactly that and a main() asserting each caller reached its own. static at namespace scope means the same thing and is the older spelling, still correct and everywhere in C; the unnamed namespace is preferred because it also works for types, which static cannot do. The gain is not privacy for its own sake: a name with internal linkage cannot collide, cannot be called by code you have not read, and does not appear in the symbol table for anyone to depend on. Needs nothing. In Rust the question does not reach the linker at all — items are private to their module unless marked pub, pub(crate) is C#'s internal exactly, and the compiler rather than the linker is what enforces it.

Trap: a unity build (Appendix J) concatenates translation units before compiling them, so two file-private helpers that never met are suddenly one translation unit apart — a redefinition error naming a file that exists in no directory, or, if the signatures differ, a silent change of which one a call reaches.

Recipe 51 — Tell the compiler what a function promises

In C#: [DoesNotReturn] on the throw helper, [Obsolete("use X")] on the old overload, and — for "the caller must look at this" — an analyzer attribute from a package, because the language has none

The recipe:

// Never comes back. The caller needs no `return` after it, and the compiler
// stops asking about the path that falls off the end.
[[noreturn]] void fatal(const char* why) {
    std::fprintf(stderr, "fatal: %s\n", why);
    std::abort();
}

// The result is the whole point of the call: ignoring it is a warning, and a
// build with -Werror refuses it. This is the compiler enforcing the sentence
// Chapter 8 spends a page on - a failure that is a value has to be looked at.
[[nodiscard]] Status session_status(int id) {
    return id > 0 ? Status::Ok : Status::Failed;
}

const char* describe(Status status, [[maybe_unused]] int verbosity) {
    switch (status) {
        case Status::Ok:
            return "ok";
        case Status::Busy:
            ++busy_seen;              // a statement, so the fall is real...
            [[fallthrough]];          // ...and this is how you say it was meant
        case Status::Failed:
            return "not available";
    }
    // Not dead code: an enum may hold a value no enumerator names, which is
    // Chapter 8's tenth scenario and the reason a switch over one is not a
    // proof of coverage.
    return "unknown";
}

// Where [[noreturn]] earns its keep: a function with nothing to return on the
// failing path, because the caller's contract is broken and there is no
// sensible value (Chapter 8's assert row, in a codebase that ships with
// NDEBUG). There is no `return` after the call, and
// with the attribute the compiler needs none; without it, it warns that
// control reaches the end of a non-void function - which is the whole
// difference an attribute makes, and the reason one of the refusals below
// removes it.
int required_channel(int configured) {
    if (configured > 0) {
        return configured;
    }
    fatal("channel not configured - the caller's contract, broken");
}

// Still compiles, still links, still ships; every call site gets a warning
// naming the replacement. C#'s [Obsolete], with the message in the same place.
[[deprecated("use session_status")]] Status retire_session(int id) {
    return session_status(id);
}
pub enum Status {
    Ok,
    Busy,
    Failed,
}

// `!` is the never type: this function has no return value because it does
// not return, and that is a fact about its TYPE, so the caller needs no
// `return` after it and the compiler needs no attribute to be told.
pub fn fatal(why: &str) -> ! {
    panic!("fatal: {why}");
}

// `#[must_use]` is `[[nodiscard]]`, and the lint is on by default.
#[must_use]
pub fn session_status(id: i32) -> Status {
    if id > 0 { Status::Ok } else { Status::Failed }
}

pub fn describe(status: &Status, _verbosity: i32) -> &'static str {
    // No fallthrough to say out loud: arms do not fall through, and the
    // `|` pattern is how two of them share a body. The match is exhaustive
    // or it does not compile, which is the other half of `[[fallthrough]]`'s
    // job done by the language.
    match status {
        Status::Ok => "ok",
        Status::Busy | Status::Failed => "not available",
    }
}

pub fn required_channel(configured: i32) -> i32 {
    if configured > 0 {
        return configured;
    }
    fatal("channel not configured - the caller's contract, broken")
}

#[deprecated(note = "use session_status")]
pub fn retire_session(id: i32) -> Status {
    session_status(id)
}

Why it looks like this. These are standard attributes: the same double-bracket syntax on every compiler since C++11, with no package and no analyzer to install, which is the part a C# developer will not expect — [MustUseReturnValue] comes from JetBrains and [NotNull] from somewhere else, and here the compiler has the whole set built in. Four of them earn their keep in SDK work. [[nodiscard]] is the one to reach for most: Chapter 8 spends a page on the fact that an error returned as a value can be ignored, and this is the compiler declining to let it be. [[noreturn]] is for the function with nothing to return — a fatal handler, an abort wrapper, the default: that cannot happen — and it pays for itself immediately, because without it every caller needs a return after the call to keep -Wreturn-type quiet, and that unreachable return is the line a reader stops at. [[maybe_unused]] is the answer to a parameter Release does not read (Recipe 24's assert argument, exactly), and [[fallthrough]] says that a switch case falling into the next was meant, which -Wimplicit-fallthrough demands and which is otherwise a comment nobody enforces. [[deprecated("use X")]] is [Obsolete], message included, and matters here for Chapter 30's reason: you may not delete an exported function, so saying so at every call site is the whole migration. Needs nothing — no header, no library. In Rust two of the four are not attributes at all: ! as a return type is [[noreturn]] moved into the type system, and an exhaustive match makes [[fallthrough]] unnecessary by refusing the missing arm; #[must_use] and #[deprecated] are the same idea under the same names.

Trap: an attribute changes no instruction and no value, so nothing in a running program can tell you one is missing, or that a careless edit dropped it — the clean build stays exactly as clean. exercises/cookbook/attributes.cpp is therefore judged by three builds that must be refused under -Werror, one per attribute, each asserted to name its own warning.

Recipe 52 — Round a number, and turn it into an integer

In C#: Math.Round(v), Math.Floor(v), Math.Ceiling(v), Math.Truncate(v), and (int)v — or Convert.ToInt32(v), which is not the same thing as the cast

The recipe:

// "Round it" is five different questions, and C# has a name for each.
double to_nearest_even(double v) { return std::nearbyint(v); }  // Math.Round(v)
double to_nearest_away(double v) { return std::round(v); }      // Math.Round(v, AwayFromZero)
double down_always(double v)     { return std::floor(v); }      // Math.Floor(v)
double up_always(double v)       { return std::ceil(v); }       // Math.Ceiling(v)
double toward_zero(double v)     { return std::trunc(v); }      // Math.Truncate(v)

// And the conversion, which is where the undefined behavior lives: a double
// that does not fit in an int is UB to cast - not wrapped, not clamped, not
// an exception.
//
// The two comparisons are exact for `int` and only for `int`: INT_MIN is
// -2^31 and INT_MAX is 2^31-1, and a double holds both to the bit. Retype
// this for int64_t and it breaks silently - (double)INT64_MAX rounds UP to
// 2^63, so the bound admits a value one past the end and the cast below is
// undefined after all. The Trap in the appendix has the fix.
std::optional<int> to_int(double value) {
    if (!std::isfinite(value)) {                   // NaN and the infinities
        return std::nullopt;
    }
    const double whole = std::trunc(value);        // toward zero, like C#'s (int)
    if (whole < static_cast<double>(std::numeric_limits<int>::min())
        || whole > static_cast<double>(std::numeric_limits<int>::max())) {
        return std::nullopt;
    }
    return static_cast<int>(whole);
}
// The same five questions, and Rust names four of them the same way.
pub fn to_nearest_even(v: f64) -> f64 { v.round_ties_even() }   // Math.Round(v)
pub fn to_nearest_away(v: f64) -> f64 { v.round() }             // Math.Round(v, AwayFromZero)
pub fn down_always(v: f64) -> f64 { v.floor() }
pub fn up_always(v: f64) -> f64 { v.ceil() }
pub fn toward_zero(v: f64) -> f64 { v.trunc() }

// The conversion needs no guard: `as` saturates at the target's bounds and
// maps NaN to zero, all defined. The guard is still worth writing when
// "it did not fit" is a different outcome from "it was very large" - which
// in a plug-in reading a user's number it usually is.
pub fn to_int(value: f64) -> Option<i32> {
    if !value.is_finite() {
        return None;
    }
    let whole = value.trunc();
    if whole < i32::MIN as f64 || whole > i32::MAX as f64 {
        return None;
    }
    Some(whole as i32)
}

Why it looks like this. Four of the five translate on sight and one does not, and the one that does not is the one everybody reaches for first. std::round is not Math.Round. C#'s Math.Round(2.5) is 2 — it rounds a halfway value to the nearest even number, which is banker's rounding and the default nobody remembers choosing — while std::round(2.5) is 3, because it rounds halves away from zero. Math.Round(-2.5) is -2; std::round(-2.5) is -3. The C++ spelling of C#'s default is std::nearbyint. And the split is already there in C# before C++ is involved: Convert.ToInt32(3.5) is 4 because it rounds to even, while (int)3.5 is 3 because a cast truncates — two conversions, one language, different answers. Nothing warns about any of it. The difference is one unit on a halfway value, it appears in a total somewhere downstream, and it gets blamed on the data. The other four are the same function under a different name, and the one worth pausing on is that a cast truncates toward zero while floor goes down, so they disagree on every negative value — static_cast<int>(-2.7) is -2 and std::floor(-2.7) is -3, which is the off-by-one that reaches a report. to_int exists because the conversion itself is Chapter 3's undefined behavior in a place nobody looks for it: a double that does not fit in an int is not wrapped, not clamped and not an exception, and the bounds compare exactly because INT_MIN and INT_MAX sit either side of a power of two and land in a double to the bit. Needs <cmath>, <limits>, <optional>. In Rust neither hazard survives: round_ties_even names the C# behaviour rather than leaving it to a process-wide mode, and as saturates at the target's bounds with NaN going to zero — defined where the C++ cast is not.

Trap: std::nearbyint reads the process's floating-point rounding mode, and a plug-in does not own that any more than it owns its locale (Chapter 42) — a host or a library that has called fesetround(FE_UPWARD) turns nearbyint(2.5) into 3, silently, in that process. round, floor, ceil and trunc are defined by their own rule and ignore the mode; reach for them where the answer must not depend on who else is in the process.

Trap: to_int's bounds are exact for int and only for int — retype it for int64_t and it breaks silently. INT64_MAX is 2^63 - 1, which no double holds, so static_cast<double>(INT64_MAX) rounds up to 2^63: the guard admits a value one past the end and the cast it protects is undefined after all. Compare against the power of two instead — whole >= 9223372036854775808.0 — or convert through long double where it is wider. The harness asserts the equality that makes the naive guard fail.

Trap: the unguarded static_cast<int>(1e20) is undefined behavior, and this is one of the rare traps in this appendix the tools do catch: under -fsanitize=undefined it reports 1e+20 is outside the range of representable values of type 'int'. In a Release build with no sanitizer it is whatever the instruction happened to do — 2147483647 on this machine, and not a number you may rely on.

Recipe 53 — Serialize a type you do not own

In C#: class InstantConverter : JsonConverter<DateTimeOffset> registered in JsonSerializerOptions.Converters, because you cannot put an attribute on a type from someone else's assembly

The recipe:

// Recipe 25's two free functions need a namespace you are allowed to add to.
// For a type from the standard library, or from a vendor's header, you are
// not - so the library's other customization point is a specialization of
// its own template, which needs no cooperation from the type at all.
namespace nlohmann {
    template <>
    struct adl_serializer<std::chrono::system_clock::time_point> {
        using TimePoint = std::chrono::system_clock::time_point;

        // Whole seconds since the epoch: a number, not a formatted string,
        // because a wire format is a decision (Chapter 34) and this one has
        // no locale, no time zone and no parser to get wrong. Recipe 29 is
        // the other choice, made the other way, for a log a human reads.
        static void to_json(json& j, const TimePoint& value) {
            j = std::chrono::duration_cast<std::chrono::seconds>(
                    value.time_since_epoch()).count();
        }

        static void from_json(const json& j, TimePoint& value) {
            value = TimePoint{std::chrono::seconds{j.get<long long>()}};
        }
    };
}

serde has the same wall and the same two ways round it: #[serde(with = "module")] on the field, or #[serde(remote = "Type")] on a local mirror of the foreign type — because the orphan rule forbids implementing Serialize for a type from another crate, which is Recipe 25's namespace problem under a different name. Both are serde, a dependency this crate does not take.

Why it looks like this. Recipe 25's to_json and from_json are found by argument-dependent lookup (Chapter 12), which means they must live in the type's own namespace — fine for a type you wrote, impossible for std::chrono::system_clock::time_point, for a vendor's struct, or for anything in a namespace you are not allowed to add to. Specializing the library's own adl_serializer is the way in that needs no cooperation from the type at all: the template belongs to nlohmann, so you are extending your own dependency rather than someone else's namespace, and the two static functions have the same shape as the free pair. What you get for it is composition — the specialization is found for the type on its own, inside a std::vector, and inside another struct's document, which the harness asserts all three ways and which a conversion written at each call site never gives you. The format here is whole seconds rather than a formatted string, because a serialization format is a decision in Chapter 34's sense and a number has no locale, no time zone and no parser to get wrong; Recipe 29 makes the other choice, for a log a human reads. Needs <nlohmann/json.hpp> and <chrono>.

Trap: the specialization is found at the point of instantiation, so it must be visible wherever the conversion happens and not merely where you wrote it — a translation unit that converts the type without having seen your specialization gets the library's default answer, or a compile error, and neither points at the file you forgot to include. Put it in a header beside the type's other adaptations, and include that header rather than remembering to.

Recipe 54 — Sign so that the verifier cannot forge

In C#: Ed25519.SignData(privateKey, data) and Ed25519.VerifyData(publicKey, data, signature) in .NET 10; before it, ECDsa over a named curve, or a signature algorithm from a package

The recipe:

using PrivateKey = std::array<std::uint8_t, 32>;   // Ed25519's seed: kept, never shipped
using PublicKey  = std::array<std::uint8_t, 32>;   // ships with the plug-in, in the clear
using Signature  = std::array<std::uint8_t, 64>;

using PKey  = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;       // Recipe 7's shape
using MdCtx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>;

// The public half, derived from the private one - so the two cannot drift.
PublicKey public_key_of(const PrivateKey& secret) {
    PKey key(EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, secret.data(), secret.size()),
             &EVP_PKEY_free);
    PublicKey pub{};
    std::size_t size = pub.size();
    if (!key || EVP_PKEY_get_raw_public_key(key.get(), pub.data(), &size) != 1 || size != pub.size()) {
        throw std::runtime_error("Ed25519 public key derivation failed");
    }
    return pub;
}

// Signing needs the private key. Nothing on the customer's machine has it.
Signature sign_ed25519(const PrivateKey& secret, const Bytes& message) {
    PKey key(EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, secret.data(), secret.size()),
             &EVP_PKEY_free);
    MdCtx ctx(EVP_MD_CTX_new(), &EVP_MD_CTX_free);
    Signature signature{};
    std::size_t size = signature.size();
    // No digest argument: Ed25519 is a one-shot over the whole message, which
    // is why this is EVP_DigestSign and not the init/update/final of Recipe 48.
    if (!key || !ctx
        || EVP_DigestSignInit(ctx.get(), nullptr, nullptr, nullptr, key.get()) != 1
        || EVP_DigestSign(ctx.get(), signature.data(), &size,
                          message.data(), message.size()) != 1
        || size != signature.size()) {
        throw std::runtime_error("Ed25519 signing failed");
    }
    return signature;
}

// Verifying needs only the public key - and a wrong answer is a return value,
// not an exception, because a bad signature is a value (Chapter 8).
bool verify_ed25519(const PublicKey& pub, const Bytes& message, const Signature& signature) {
    PKey key(EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, pub.data(), pub.size()),
             &EVP_PKEY_free);
    MdCtx ctx(EVP_MD_CTX_new(), &EVP_MD_CTX_free);
    if (!key || !ctx
        || EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, key.get()) != 1) {
        throw std::runtime_error("Ed25519 verification could not start");
    }
    return EVP_DigestVerify(ctx.get(), signature.data(), signature.size(),
                            message.data(), message.size()) == 1;
}

Rust's standard library has no cryptography (Recipe 36): ed25519-dalek is the crate, SigningKey and VerifyingKey are these two halves with the asymmetry written into the type names, and a Signature is the same 64 bytes. Dependencies this crate does not take.

Why it looks like this. Recipe 48's Trap is this recipe's whole reason for existing: an HMAC is verified with the key that signs it, so a plug-in checking a licence on a customer's machine is carrying everything needed to mint one. A signature splits that in two. The private key stays on your build machine and signs; the public key ships inside the binary, in the clear, verifies, and cannot sign — which is the one sentence a shared secret can never say, and the reason this is a separate recipe rather than a parameter to that one. Ed25519 is the default worth having: fixed 32-byte keys and 64-byte signatures, no curve to name, no padding mode, no parameters to get wrong, and no random number needed at signing time — which removes the failure that has broken deployed ECDSA more than once. The EVP shape is Recipe 48's with one visible difference: EVP_DigestSign is a single call rather than init/update/final, because Ed25519 hashes the whole message itself and there is no digest to name. public_key_of exists so the two halves cannot drift — derive the public key from the private one rather than storing both and trusting them to match. And verification returns a bool rather than throwing, because a bad signature is Chapter 8's value: it is the expected outcome of checking something you did not write. The harness holds all of it to RFC 8032's own vectors — the public keys derived here must equal the published ones and the signatures must match byte for byte, so it is the RFC checking the code rather than the code checking itself. Needs <openssl/evp.h> and libcrypto as Recipe 36, plus <array>, <memory>, <stdexcept> and <vector>. Where the private key lives is Chapter 27's question, not this page's.

Trap: a signature proves who wrote the bytes and nothing else. A signed licence file is still a file: it can be copied to another machine, restored after an uninstall, or replayed a year later, and every one of those verifies perfectly. Whatever must not be replayed — a machine id, an expiry, a nonce — has to be inside the signed bytes, because a signature over the wrong bytes is a check that always passes.