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 JsonNode — fields() / 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:
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:
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:
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:
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;
}
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";
}
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:
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:
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:
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:
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:
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:
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:
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:
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
}
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:
/// 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:
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
pin — Chapter 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:
// 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::Crypto — Appendix 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(¤t, export_dir.join("archive").join("previous.txt"))?; // copy overwrites; C#'s default refuses
}
std::fs::rename(fresh_report, ¤t) // 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::CURL — Appendix 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.