all_lessons/Rust/22 · Capstone and the honest costslesson 23 / 23

Capstone — a small concurrent service, and the honest costs

Lessons 01 to 21 built one mechanism at a time; this one assembles them into a single program that compiles, runs, and prints the same thing every time, then does what the series has trained you for: it adds up the bill. The five questions explain why each piece is the one the law forced; then the bargain is priced — promises that cost nothing at run time (identical assembly), checks that cost a measured handful of nanoseconds, and what the promise never covered. The point is not the service; it is that you can now read any design as a ledger — proof paid at compile time, cycles at run time, a learning curve by people — and say where that trade is right.

The thesis, here
No new mechanism appears in this lesson; every one is linked to the lesson that derived it. What is new is the accounting. Rust's promise — safe code has no undefined behavior — is kept by a proof at compile time, and the whole argument of the track is that the proof is cheap: free where it can be, named where it cannot. This lesson makes "free" and "named" concrete, with assembly you can diff and timings you can re-run, and then it states what the promise never covered and what the learning curve costs.
Linear position
Forced by: We now have every mechanism. What does the whole language look like assembled in one program, and what does the bargain honestly cost — where is it the wrong tool, and what should a careful engineer still weigh?
New idea: assemble the mechanisms into one working service, walk the five questions over it to show each piece was forced, then price the bargain in three currencies — compile-time proof (0 ns), run-time checks (a measured few ns each), and the human learning curve — and name where the bargain is the wrong one.
Forces next: Trust, constrain, verify: which belongs where in your own work, and what will you now ask of every line of code?
The plan
Six moves. (1) The program: a small concurrent service that runs deterministically. (2) The five questions over it, one column each. (3) The widget: price your own workload. (4) The honest bill: what costs 0 ns, shown in assembly, and what costs real nanoseconds. (5) What the promise does not cover, and the human costs. (6) When Rust is the wrong tool, and how to choose on purpose.

1 · The program

A job server. The main thread hands out work; three worker threads price grocery items against a shared read-only table and total lists of numbers; results come back, are sorted, and are printed. It uses most of the track: an enum for the closed set of requests (Lesson 09), a small error enum with ? (Lesson 10), a generic helper with a trait bound (Lessons 11–12), one Box<dyn Fn> handler (Lessons 13–14), an Arc<Mutex<_>> counter and a read-only table shared through a scope (Lessons 15–17), a Drop guard for shutdown (Lesson 03), and no unsafe. It is made deterministic on purpose — results are collected and sorted before printing — so its output is a fact the compiler check can verify:

use std::collections::HashMap;
use std::fmt;
use std::sync::{mpsc, Arc, Mutex};
use std::thread;

enum Job { Word(String), Sum(Vec<u32>), Stop }         // a closed set of requests (L09)

#[derive(Debug)]
enum JobError { Unknown(String), Overflow(u64) }       // failure is a value (L10)

impl fmt::Display for JobError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            JobError::Unknown(w) => write!(f, "unknown item {w:?}"),
            JobError::Overflow(n) => write!(f, "total {n} overflows u32"),
        }
    }
}

#[derive(Default)]
struct Stats { ok: u32, failed: u32 }

struct Report(Arc<Mutex<Stats>>);                      // prints the tally when dropped (L03)
impl Drop for Report {
    fn drop(&mut self) {
        let s = self.0.lock().unwrap();
        println!("shutdown: {} ok, {} failed", s.ok, s.failed);
    }
}

fn total<T: Copy + Into<u64>>(xs: &[T]) -> Result<u32, JobError> {   // one generic, checked once (L12)
    let sum: u64 = xs.iter().map(|&x| x.into()).sum();
    u32::try_from(sum).map_err(|_| JobError::Overflow(sum))
}

fn price(item: &str, prices: &HashMap<&str, u32>) -> Result<String, JobError> {
    let p = prices.get(item).ok_or_else(|| JobError::Unknown(item.to_string()))?;   // ? (L10)
    Ok(format!("{item} costs {p}"))
}

fn main() {
    let prices: HashMap<&str, u32> = HashMap::from([("apple", 3), ("bread", 4), ("milk", 2)]);
    let stats = Arc::new(Mutex::new(Stats::default()));
    let _report = Report(Arc::clone(&stats));           // declared first, so dropped last (L03)
    let on_done: Box<dyn Fn(bool) + Send + Sync> = Box::new(move |ok| {   // a handler value (L13–14)
        let mut s = stats.lock().unwrap();              // one writer at a time (L17)
        if ok { s.ok += 1 } else { s.failed += 1 }
    });
    let (job_tx, job_rx) = mpsc::channel();
    let job_rx = Mutex::new(job_rx);                    // one queue, three takers: in turns (L17)
    let (out_tx, out_rx) = mpsc::channel();
    thread::scope(|s| {                                 // workers borrow prices/on_done; joined here (L17)
        for _ in 0..3 {
            let (prices, job_rx, on_done, out_tx) = (&prices, &job_rx, &on_done, out_tx.clone());
            s.spawn(move || loop {
                let job = job_rx.lock().unwrap().recv().unwrap();   // the guard dies at this `;`
                let result = match job {
                    Job::Word(item) => price(&item, prices),
                    Job::Sum(xs) => total(&xs).map(|t| format!("sum of {} is {t}", xs.len())),
                    Job::Stop => break,
                };
                on_done(result.is_ok());
                out_tx.send(result).unwrap();          // the result moves to main (L17)
            });
        }
        let jobs = [Job::Word("bread".into()), Job::Sum(vec![1, 2, 3]), Job::Word("cake".into()),
                    Job::Sum(vec![u32::MAX, 1]), Job::Word("milk".into())];
        for job in jobs { job_tx.send(job).unwrap(); }
        for _ in 0..3 { job_tx.send(Job::Stop).unwrap(); }
    });                                                 // every worker is joined here
    drop(out_tx);
    let mut lines: Vec<String> = out_rx.iter()
        .map(|r| r.unwrap_or_else(|e| format!("error: {e}")))
        .collect();
    lines.sort();
    for line in &lines { println!("{line}"); }
}
bread costs 4
error: total 4294967296 overflows u32
error: unknown item "cake"
milk costs 2
sum of 3 is 6
shutdown: 3 ok, 2 failed

The Sum of [u32::MAX, 1] sums into a u64 and then fails u32::try_from, so overflow is a returned JobError, not a panic (Lesson 02: overflow is defined). The three workers race to pull from one queue, finish in any order, and the sort makes the print order a fact. Now break it by one line — drop the Mutex around the receiver, so three workers share a bare Receiver:

Reading the error
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<u32>();          // the service wraps rx in a Mutex; drop that,
    thread::scope(|s| {                             // and three workers share one bare Receiver
        for _ in 0..3 {
            let rx = &rx;
            s.spawn(move || while let Ok(n) = rx.recv() { println!("{n}"); });
        }
        for n in 0..3 { tx.send(n).unwrap(); }
    });
}
error[E0277]: `std::sync::mpsc::Receiver<u32>` cannot be shared between threads safely
  = help: the trait `Sync` is not implemented for `std::sync::mpsc::Receiver<u32>`
  = note: required for `&std::sync::mpsc::Receiver<u32>` to implement `Send`
Run the triage from Lesson 21. E0277 is on its local list, because most of them are one missing trait on one line. This one names Sync, which puts it on the law side (Lesson 16): the standard library marks Receiver not Sync because a receiver belongs to one thread, and sharing &rx would let several threads take from one queue at once. The compiler cannot know which of Lesson 17's regimes you meant. A scoped borrow needs a Sync value (§4), so either one thread owns the receiver and fans the jobs out (§2), or the takers take turns behind a Mutex (§3), the service's choice. A third copy is not on offer: a Receiver does not clone. The message names the missing trait, not the design that would supply it.

2 · The five questions, one column each

Every value in the service earned its type by answering Lesson 00's five questions. Read the table as the design being derived: for each value, who ends it, who else reaches it, what the signature promises, whether the proof is paid now or later, and where it stops. Each answer links the lesson that made it.

Value1 · owns it / ends it2 · who else reaches it3 · signature promises4 · proven now, or checked later5 · proof stops at
prices tablemain; dropped at the end (L03)3 workers, shared &prices (L05)price(&str, &HashMap) reads onlynow: scope proves it outlives the threads — 0 ns (L17)the standard library's own audited unsafe (L19)
stats counterthe Arc: several owners, last drop frees (L15)the handler, exclusive in turns via the lockMutex<Stats> is Sync when Stats: Send (L16)later: the lock + atomics — a few ns each (§4)Arc/Mutex unsafe impls
a Job / its Stringone name at a time; send moves it (L04)nobody in parallel — the move ends the sender's nameSender<Job>: Send when Job: Sendnow: the move rule; price is a queue op (§4)nowhere — pure safe code
on_done handlermain owns the Box; dropped at the end (L13)3 workers, shared &on_donedyn Fn(bool) + Send + Sync (L13, L16)later: one indirect call, plus the lock it takesthe closure body
_report guardmain; declared first, dropped last (L03)no other name — a lone ownerDrop runs the shutdown printnow: drop order is static — 0 ns beyond the worklocal to main

Give each worker its own prices instead of lending one and you pay a clone per worker; drop the Mutex around stats and the += 1 becomes an assignment through a shared Arc, which the compiler refuses (E0594; Lesson 15 is why the Mutex is there); take the guard out and the shutdown line is gone. Every piece is the cheapest answer the law left, and the next section prices it.

3 · What does the bargain cost at your workload?

The widget is the service's ledger, driven by numbers measured on this machine (§4). You set the request rate and the operations per request; it computes the nanoseconds per second each mechanism adds, as a share of one CPU core, and states the verdict. The proofs that cost nothing at run time sit in their own band at zero. Change the rate and predict the bill before you read it.

The cost ledger
Slide the request rate; pick a workload, or dial the three operations a request does most. Bars are nanoseconds per second (a share of one core); the grey band is the compile-time proofs, which stay at 0 ns. The readout names the largest line and what to do about it.
run-time bill
—
of one core
—
largest line
—
compile-time proofs
0 ns
Show the core JS
// Price a workload: requests/s x operations/request x ns/op = ns/s, and ns/s / 1e7 = % of one core.
function ledger(rps, counts) {
  var rows = CLASSES.map(function (c) {
    var n = counts[c.id] || 0, nsps = rps * n * c.ns;
    return { id: c.id, name: c.name, group: c.group, lesson: c.lesson, count: n, nsPerOp: c.ns, nsPerSec: nsps, pctCore: nsps / 1e7 };
  });
  var check = 0, design = 0, perReq = 0, top = null;
  rows.forEach(function (r) {
    if (r.group === 'check') check += r.pctCore; else design += r.pctCore;
    perReq += r.count * r.nsPerOp;
    if (r.nsPerSec > 0 && (!top || r.nsPerSec > top.nsPerSec)) top = r;
  });
  var total = check + design;
  return { rps: rps, rows: rows, checkPct: check, designPct: design, totalPct: total, perRequestNs: perReq,
           nsPerSec: total * 1e7, top: top, topShare: top ? top.pctCore / total : 0 };
}

What to try. Three worked workloads, each computed by the engine above and pasted here unchanged. §1’s service does about 55.1 ns of priced work per request (two locks, two allocations, two channel hops, one dyn call, one push); at 10,000 requests/s that is 0.055% of one core, none of it the safety proofs, and the biggest single line is Box::new at half the bill. Push it to a million requests/s and it is still only 5.5% of a core. Table lookups — a million bounds-checked indexes per request — puts the bounds check at 1.2% of a core at 100 requests/s and 12% at 1,000: the one check the promise cannot do for free (Lesson 02), now visible. Arc fan-out — sixteen Arc::clones a request — costs 0.59% of a core at 100,000 requests/s, 90% of it the atomic counts inside the clones; lend with & inside a scope instead of cloning and the same workload drops to 5.80 ns a request.

4 · The honest bill

The bill has two halves, and the split is the whole thesis. One half is paid before the program runs and costs nothing after; the other is paid every time, in cycles you can measure and name.

What costs 0 ns: proofs, shown in assembly

Borrow checking, lifetimes, Send/Sync and exhaustiveness are compile-time proofs: they change whether the program is accepted, never what it does. The evidence is that turning the proof off leaves the machine code identical. Three pairs from the service — each differs only by a proof — compiled at full optimization:

pub struct Stats { pub ok: u32, pub failed: u32 }
pub enum Job { Word(String), Sum(Vec<u32>), Stop }

#[inline(never)]                    // the borrow checker proves: s is valid and unaliased
pub fn record(s: &mut Stats, ok: bool) { if ok { s.ok += 1 } else { s.failed += 1 } }
#[inline(never)]                    // raw pointers: nobody proves anything
pub unsafe fn record_raw(s: *mut Stats, ok: bool) { unsafe { if ok { (*s).ok += 1 } else { (*s).failed += 1 } } }

#[inline(never)]                    // Send + Sync bound: this handler may cross threads
pub fn notify(on_done: &(dyn Fn(bool) + Send + Sync), ok: bool) { on_done(ok) }
#[inline(never)]                    // no bound: it may not
pub fn notify_local(on_done: &dyn Fn(bool), ok: bool) { on_done(ok) }

#[inline(never)]                    // exhaustive match: a new variant is a compile error here
pub fn size(job: &Job) -> usize { match job { Job::Word(w) => w.len(), Job::Sum(xs) => xs.len(), Job::Stop => 0 } }
#[inline(never)]                    // a wildcard arm instead
pub fn size_wild(job: &Job) -> usize { match job { Job::Word(w) => w.len(), Job::Sum(xs) => xs.len(), _ => 0 } }
$ rustc --edition 2024 -C opt-level=3 --crate-type=lib --emit asm -o proofs.s proofs.rs
$ # record (borrow-checked) vs record_raw (raw pointers): diff the two bodies, names normalized
$ diff <(body proofs.s record) <(body proofs.s record_raw) && echo identical
identical
$ grep ' = ' proofs.s      # the other two pairs never got their own code at all:
_..._notify    = _..._notify_local      # the Send + Sync bound emitted nothing extra
_..._size_wild = _..._size              # exhaustive match and wildcard compiled to one function
record:                      record_raw:            ; byte-for-byte identical
  tbz  w1, #0, LBB               tbz  w1, #0, LBB     ; if !ok jump
  ldr  w8, [x0]                  ldr  w8, [x0]        ; s.ok
  add  w8, w8, #1                add  w8, w8, #1
  str  w8, [x0]                  str  w8, [x0]
  ret                           ret
LBB:                          LBB:
  ldr  w8, [x0, #4]             ldr  w8, [x0, #4]     ; s.failed
  add  w8, w8, #1              add  w8, w8, #1
  str  w8, [x0, #4]            str  w8, [x0, #4]
  ret                          ret

The borrow checker, the Send + Sync bound and the exhaustiveness proof left no trace in the output: record and record_raw are the same instructions, and the other two pairs were folded into one function each. That is "the price is paid at compile time, in the shape of your code" (Lesson 00) made literal — and lifetimes join them, since Lesson 06 showed four functions differing only in their annotations compiling identically.

What costs real nanoseconds: checks, measured

The other half is the run-time price this track named at each step, now measured. A benchmark program times each operation — median of 31 timed runs after warm-up, every input and result through std::hint::black_box so nothing is optimized away — and the widget above reads its numbers. The method:

const RUNS: usize = 31;
fn median_ns_per_op(ops: usize, mut run: impl FnMut()) -> f64 {
    for _ in 0..WARMUP { run(); }                                 // warm the caches and the clock
    let mut t: Vec<f64> = (0..RUNS)
        .map(|_| { let s = Instant::now(); run(); s.elapsed().as_nanos() as f64 / ops as f64 })
        .collect();
    t.sort_by(|a, b| a.partial_cmp(b).unwrap());
    t[RUNS / 2]                                                   // the median, not the mean
}
// e.g. one uncontended lock/unlock:  *black_box(&mutex).lock().unwrap() += 1;

On one machine — Apple M5 (aarch64), macOS 26.5.2, rustc 1.98.1, -C opt-level=3, 2026-09-30, uncontended, single thread — the ledger reads: an extra bounds check costs about 0.12 ns per indexed access, a RefCell borrow about 0.19 ns, an Rc::clone+drop 0.91 ns against an Arc's 3.33 ns (the atomic is the difference), a Mutex lock/unlock 4.11 ns, an AtomicUsize::fetch_add 1.69 ns, a dyn call about 0.42 ns over an inlined generic, a Box::new+drop 13.7 ns, and a channel send+recv 8.57 ns. The machine was shared with other jobs while these ran; the published figure for each is the median of three independent program invocations, and its spread is recorded in tools/rust_verify/22_bench.json. Debug-build overflow checks are the one cost that vanishes in release, where arithmetic wraps instead of panicking (Lesson 02).

One machine, one run — not a law
Every number here is one configuration on one laptop on one afternoon, and the benchmark loop is not your program. A dyn call measured 0.42 ns here, in a loop whose iterations do not wait on each other; Lesson 13 measured 0.14 ns in a loop that sums into one running total, which hides part of a call, and several times more once the types were mixed at random. Read the ledger as "these operations are cheap, in this order, and here is how to measure yours" — never as "Arc costs 3.33 ns" stated as a fact about Rust.

5 · What the promise does not cover

The ledger prices what Rust checks. It cannot price what Rust never promised. The promise is exactly "safe code has no undefined behavior"; everything below is defined behavior, so safe Rust allows all of it — the honest list, one line each with the lesson that owns it:

Still yours to preventWhy the promise does not reach it
Deadlocka lock order is a whole-program property; no local rule sees it (L17).
Race conditionsan order-dependent wrong answer with no data race is defined; load-then-store loses updates (L17).
Leaksleaking is safe: an Rc cycle or mem::forget skips a destructor and nothing is undefined (L03, L15).
Panics & overflow in releasea panic is defined — it unwinds, or aborts if so configured; release overflow wraps to a wrong-but-defined number (L10, L02).
Logic errorsa wrong answer is a bug a test finds, not UB; the compiler never promised correctness (L00).
Resource exhaustiona thread per idle connection, an unbounded queue: defined, and still fatal (L17, L18).

Three more bills sit underneath the promise, and honesty requires naming them. The compiler is a program with bugs. As of 2026-09 a type-system soundness hole (rust-lang/rust #25860), open since May 2015, lets safe code stretch a reference's lifetime through a specially shaped function pointer; the original example is now rejected, but a variant still compiled on rustc 1.98.1 when this was checked, and the Rust project's "Project Zero" goal aims to close such holes. The same 1.98 series shipped a rustc miscompilation — a null pointer where a vtable function belongs — that was in stable for two weeks before the 1.98.1 fix. You inherit your dependencies' proofs. Safe Rust's guarantee is conditional on the soundness of the unsafe code beneath it (Lessons 19–20), and a study of crates.io as of September 2018 (Evans et al.) found that fewer than 30% of crates used unsafe themselves, yet over half could not be fully checked by the compiler because unsafe hid somewhere in their call chain. And the supply chain — a crate you did not audit runs its build script on your machine — is a trust boundary the language does not police at all.

The largest bill is paid by people. The measured weak link is the counterfactual: in Brown's Ownership Inventory, 36 learners who had finished the Book could say why the compiler rejects a program about 64% of the time, but only 31% could write a program that would misbehave if it were accepted — which is why this track is built on counterexamples. The Book's own quiz data showed chapter 4 (ownership) as a drop-off point where readers leave, with only 2% reaching chapter 19; and in the 2020 Rust survey, 61.4% of respondents rated lifetimes tricky or very difficult, the hardest topic named. Compile time is a standing cost: this 77-line service took, on the machine above, about 59 ms to type-check, 145 ms for a debug build and 372 ms for a release build — trivial here, but in the 2024 survey slow compilation was the most-cited productivity obstacle. Async is close to a second dialect (Lesson 18), and the checker will reject some safe programs until you restructure them (Lesson 07). None of these is undefined behavior; all of them are real, and they go on the same ledger as the nanoseconds.

6 · Where it is the wrong tool, and how to choose

The five questions are a decision procedure, not just a reading habit. Ask them of a project and the currencies fall out: run time (question 4 — what is checked later, and how often), safety (questions 1, 2, 5 — whose proof keeps the contract, and where it stops), and people (the learning curve and compile times of §5). Weigh those three, not a slogan.

When another tool wins
  • A collector is right when the scarce resource is the team's attention, not the machine's cycles. The Go track's bargain is simplicity as the tool: a small language and a run-time system, so that many people can change a large program for years, at the price of a pause you can measure. Deeply cyclic, aliased, shared-mutable graphs fight Rust's law (Lesson 15) and are exactly where a collector is easiest.
  • C++ is right when the codebase, the libraries and the team are already C++ — rewriting a working system to change languages is a cost with its own risks, and interop at the boundary is often the better move (C++ track).
When Rust pays
  • Long-lived and concurrent — code that will be changed for years by people who did not write it, where proving there is no data race is worth the up-front checking (Lesson 17).
  • Security-relevant, no runtime — where memory-safety bugs are the threat and a garbage collector is not an option: a kernel module, a browser engine, a sandbox. Safe Rust rules out the class of bug — memory unsafety — that was about 70% of the CVEs Microsoft fixed in its own products over 2006–2018 (one vendor's figure, not an industry-wide one), at C++'s run-time budget.

That is the whole series in one judgment. Lesson 00 put three languages side by side: C++ trusts the programmer to keep the safety contract, Go constrains the program with a run-time system, Rust verifies the contract with a proof at compile time. None is the right answer everywhere; each spends a different scarce thing — the programmer's attention, the machine's cycles, or the compiler's, and yours, up front. You now know what the trade is, priced in three currencies, and the question left is not which language to use next.

Common mistakes / failure modes

"Rust is faster than everything"
Rust removes some checks by proving them unnecessary and keeps the ones it cannot prove away (bounds, atomics, dyn). It is competitive with C++'s budget, not magic.
"Rust has no run-time cost"
It has no hidden cost. Lesson 00's correction, now quantified: a bounds check ~0.12 ns, an Arc clone ~3.33 ns, a lock ~4.11 ns — each spelled in the source where you asked for it (§4).
"unsafe makes Rust as dangerous as C"
The unsafe surface is small and auditable, bounded by privacy to a module (Lesson 20). But you inherit your dependencies' unsafe, which is why the 2018 study's "over half of crates cannot be fully checked" is the honest caveat (§5).
"Fighting the borrow checker never ends"
The Rust project's 2026 write-up describes the borrow checker as a beginner problem that fades: a topic every newcomer struggles with and experienced users stop mentioning.
"Safe Rust guarantees correctness"
Safe means not undefined, nothing more (Lessons 00–01). The service can still deadlock, leak, lose an update or compute the wrong total; §5 is the list the promise never covered.

Checkpoint exercise

Try it
In the widget, pick Arc fan-out and read the bill at 100,000 requests/s; note the largest line and its share. Predict the request rate at which the run-time bill first crosses 1% of one core, then check it against the slider’s steps. Next pick §1’s service and predict which single change cuts the most — setting boxes / request to 0 (the widget prices each allocation as a Box::new and drop) or removing the one dyn call — before you try the first. Finally, take the service program, change one line so it no longer compiles, and classify the error with Lesson 21’s triage: local, law, or boundary? (Check: 1% of a core is 10,000,000 ns/s and Arc fan-out costs about 59 ns a request, so the bill crosses 1% near 170,000 requests/s, between the slider’s 100,000 and 300,000 steps, about 90% of it the Arc clones’ atomic counts. In the service the two allocations are about half of the 55.1 ns and the dyn call is 0.42 ns: allocation dominates.)

Where this points next

There is no next lesson. The track derived Rust from one promise and one budget, and every rule arrived as the only move left: an owner because something must end each value once, a move because a copied owner double-frees, a borrow because moving to read is wasteful, a lifetime because a borrow must not outlive its owner, traits and generics and dyn because abstraction must not cost the guarantee, Send and Sync and scopes because the law must cross threads, unsafe and privacy because a human sometimes takes the proof back. You have now seen the whole thing assembled and priced. What remains is the habit: ask of every line who owns it, who else can reach it and how, what the signature promises, whether it is proven now or checked later and at what price, and where the proof stops. Trust, constrain, verify — decide, deliberately, which belongs where in your own work, and what you will now ask of every line of code.

Takeaway
One 77-line program holds most of the language, with no unsafe, and every piece is the cheapest answer the five questions left. The bill has two halves. Compile-time proofs cost 0 ns: borrow checking, lifetimes, Send/Sync and exhaustiveness compile to assembly identical to the unchecked version. Run-time checks cost a measured few nanoseconds each, spelled in the source: a bounds check, an Arc atomic, a lock, a dyn call. The promise does not cover deadlock, races, leaks, panics, logic errors or exhaustion, rests on sound unsafe beneath you and on a compiler with known holes, and is bought with a real learning curve and compile time. So choose in three currencies — run time, safety, people: a collector when its bounded bill buys correctness, C++ when the ecosystem is there, Rust when code is long-lived, concurrent, security-relevant and cannot afford a runtime. C++ trusts, Go constrains, Rust verifies; which belongs where is now your call.

Interview prompts

Companion reads: Go · 17 Capstone (the same exercise, one language over, with Go's bill), Go · 16 Idiomatic Go (complexity as the currency), C++ · 19 Undefined behavior (the bargain Rust re-made), and C++ · 00 Orientation (C++ makes the bill itemized).