---
title: "Rust"
book: "The Interview Book"
subject: quant
language: en
chapter: 23
exercises: 0
source: https://one-course.com/books/quant/18/en/chapter/23-rust
---

# Chapter 23 — Rust

The code looks harmless: take a reference to the best bid in a vector of price levels, push a new level, then print the reference. The compiler refuses it with error E0502, and the candidate is asked two questions: what bug in the equivalent C++ the refusal just prevented, and how to write the function so that it compiles without cloning the book. Rust interviews at trading and crypto firms test whether the candidate thinks in the language’s rules (ownership, borrowing, lifetimes, the traits that mark what may cross threads) rather than fighting them, and whether the candidate knows where the guarantees stop: at `unsafe` and at logic errors. The performance side is One Quant Book 13, chapter 9; every snippet here is compiled by the chapter’s tests with the pinned toolchain and its error code or output asserted.

## 23.1 Ownership and borrowing, and the errors that teach them

Every value has one owner; assignment of a non-`Copy` value moves it and the old name can no longer be used (E0382). A value may be borrowed either by any number of shared references or by exactly one mutable reference at a time (E0499, E0502), and no reference may outlive what it refers to (E0515, E0597). The rules are the static version of the defects C++ finds with sanitizers ([Chapter 22](https://one-course.com/books/quant/18/en/chapter/22-c#ch-iv-cpp)): a shared reference into a vector that is then pushed to is exactly the dangling reference of a reallocated buffer.

```rust
fn main() {
    let mut levels: Vec<i64> = vec![101, 100];
    let best = &levels[0];
    levels.push(99);
    println!("{best}");
}
```

***Listing 23.1.** A shared borrow of a level held across a push: error E0502. code/interviews/23-rust/rust/snippets/borrow_conflict.rs*

**Method 23.1 (Fixing a borrow error without cloning).**

1. Shorten the borrow: copy the value you need out of the reference before mutating (a `Copy` type costs nothing).
2. Split the borrow: `split_at_mut` , destructuring a struct into its fields, or iterating with `iter_mut` .
3. Use indices instead of references when the collection must change underneath.
4. Use the entry API for “look up, then insert or update” on maps.
5. Restructure ownership (move the data into the function that mutates it) before reaching for `RefCell` .

```rust
/// Update two different elements of one slice mutably at once by splitting the borrow.
pub fn move_qty(qty: &mut [i64], from: usize, to: usize, amount: i64) {
    assert!(from != to, "from and to must differ");
    let (lo, hi) = qty.split_at_mut(from.max(to));
    let (a, b) = if from < to {
        (&mut lo[from], &mut hi[0])
    } else {
        (&mut hi[0], &mut lo[to])
    };
    *a -= amount;
    *b += amount;
}
```

***Listing 23.2.** Two mutable references into one slice, legally: split the borrow. code/interviews/23-rust/rust/src/lib.rs*

## 23.2 Lifetimes in signatures and structs

A lifetime parameter names the region a reference is valid for, and the compiler checks that callers respect it. When a function takes two references and returns one, the compiler cannot guess which input the output borrows from (E0106), and the signature must say. A struct that holds a reference carries a lifetime too, which is how a zero-copy parser returns fields that borrow from the input buffer instead of allocating.

```rust
/// A zero-copy parser of '|'-separated fields that borrow from the buffer.
pub struct Fields<'a> {
    buf: &'a [u8],
    pos: usize,
}

impl<'a> Fields<'a> {
    pub fn new(buf: &'a [u8]) -> Self {
        Fields { buf, pos: 0 }
    }
}

impl<'a> Iterator for Fields<'a> {
    type Item = &'a [u8];
    fn next(&mut self) -> Option<&'a [u8]> {
        if self.pos > self.buf.len() {
            return None;
        }
        let rest = &self.buf[self.pos..];
        let end = rest.iter().position(|&c| c == b'|').unwrap_or(rest.len());
        self.pos += end + 1;
        Some(&rest[..end])
    }
}
```

***Listing 23.3.** A zero-copy field parser: the returned slices borrow from the buffer for its lifetime `’a`. code/interviews/23-rust/rust/src/lib.rs*

## 23.3 Traits, generics and dynamic dispatch

A generic function bounded by a trait is monomorphised: one copy per concrete type, calls resolved and inlined at compile time. A trait object `&dyn Trait` dispatches through a vtable: one copy of the code, an indirect call, and a restriction that the trait be object-safe (no generic methods, no `Self` by value in signatures). Drop order is part of the language: local variables are dropped in reverse order of declaration and struct fields in declaration order, the opposite of C++ members ([Interview question 23.7](#iq-iv-rust-7)).

## 23.4 Unsafe, Send and Sync: what the guarantees are and where they stop

Safe Rust guarantees memory safety and the absence of data races (One Quant Book 13, chapter 9). `Send` marks a type whose values may be moved to another thread, `Sync` one whose shared references may be; the compiler derives them, and a type such as `Rc` (a non-atomic reference count) is neither, so sending it to a thread is rejected (E0277). `unsafe` blocks let the programmer assert what the compiler cannot check (an index in bounds, a pointer valid); a sound abstraction keeps every such assertion justified by checks the safe interface enforces. The guarantees stop at logic: deadlocks, wrong memory orderings in lock-free code that is otherwise safe, and integer overflow in release builds (which wraps) remain the programmer’s.

```rust
/// Sum of the first n prices without per-element bounds checks: n is checked once.
pub fn sum_first(prices: &[i64], n: usize) -> i64 {
    assert!(n <= prices.len());
    let mut s = 0;
    for i in 0..n {
        // SAFETY: i < n <= prices.len(), checked by the assert above.
        s += unsafe { *prices.get_unchecked(i) };
    }
    s
}
```

***Listing 23.4.** An unchecked index made sound by one check at the boundary. code/interviews/23-rust/rust/src/lib.rs*

## 23.5 Question bank

**Interview question 23.1 ★ developer • crypto firm.**

Why does [Listing 23.1](#lst-iv-rust-borrow) not compile, what C++ bug does the refusal prevent, and how do you fix it without cloning the vector?

**Solution of Interview question 23.1.**

`best` borrows the vector immutably and is used after `push`, which needs a mutable borrow: E0502. In C++ the push could reallocate and leave `best` dangling ([Interview question 22.9](https://one-course.com/books/quant/18/en/chapter/22-c#iq-iv-cpp-9)). Fix by ending the borrow before mutating: copy the `i64` out (`let best = levels[0];`), or read it after the push by index.

```rust
/// Push a new level and return the old best: copy the value out, then mutate.
pub fn push_level(levels: &mut Vec<i64>, new_level: i64) -> Option<i64> {
    let best = levels.first().copied();
    levels.push(new_level);
    best
}
```

*Copy the value, then mutate. code/interviews/23-rust/rust/src/lib.rs*

*What the interviewer is looking for: the aliasing rule, the C++ bug it prevents, and a zero-cost fix.*

```rust
fn main() {
    let fills = vec![1, 2, 3];
    let archived = fills;
    println!("{} {}", fills.len(), archived.len());
}
```

***Listing 23.5.** Archiving the fills. code/interviews/23-rust/rust/snippets/use_after_move.rs*

**Interview question 23.2 ★ developer • proprietary firm.**

Why does [Listing 23.5](#lst-iv-rust-moved) not compile? Give two fixes and say what each costs.

**Solution of Interview question 23.2.**

`let archived = fills;` moves the vector; `fills` is no longer valid (E0382). Fixes: borrow instead (`let archived = &fills;`), which costs nothing but ties `archived`’s life to `fills`; or clone (`fills.clone()`), which copies the heap buffer. Use `fills.len()` before the move if only the length is needed.

*What the interviewer is looking for: move semantics, and choosing between borrowing and cloning by cost.*

```rust
fn main() {
    let px = 100;
    let px = px * 2;
    {
        let px = px + 1;
        println!("{px}");
    }
    println!("{px}");
}
```

***Listing 23.6.** Shadowing. code/interviews/23-rust/rust/snippets/shadowing.rs*

**Interview question 23.3 ★ developer • any.**

What does [Listing 23.6](#lst-iv-rust-shadow) print? Is `px` mutable?

**Solution of Interview question 23.3.**

`201` then `200`. Each `let` creates a new binding that shadows the previous one; the inner block’s shadow ends with the block. No binding is mutable: shadowing is not mutation, and the values are not overwritten.

*What the interviewer is looking for: shadowing versus mutability and scope.*

```rust
fn main() {
    let mut qty = vec![10, 20];
    let a = &mut qty[0];
    let b = &mut qty[1];
    *a += *b;
}
```

***Listing 23.7.** Two mutable borrows. code/interviews/23-rust/rust/snippets/two_mut.rs*

**Interview question 23.4 ★ developer • market maker.**

Why is [Listing 23.7](#lst-iv-rust-twomut) rejected although the two elements differ, and how do you write it legally?

**Solution of Interview question 23.4.**

The borrow checker reasons about the vector as a whole: two `&mut` into it at once are two mutable borrows of the same value (E0499), even for different indices. Split the borrow with `split_at_mut`, which returns two disjoint mutable slices ([Listing 23.2](#lst-iv-rust-split)), or use indices and do the update in two statements.

*What the interviewer is looking for: why disjointness is not visible to the checker, and `split_at_mut`.*

```rust
fn best(a: &str, b: &str) -> &str {
    if a > b { a } else { b }
}

fn main() {
    println!("{}", best("x", "y"));
}
```

***Listing 23.8.** Returning one of two references. code/interviews/23-rust/rust/snippets/missing_lifetime.rs*

**Interview question 23.5 ★★ developer • crypto firm.**

Fix [Listing 23.8](#lst-iv-rust-lifetime). What does the lifetime you add promise the caller?

**Solution of Interview question 23.5.**

`fn best<’a>(a: &’a str, b: &’a str) -> &’a str`. The lifetime promises that the result is valid as long as both inputs are, so the caller may not use it after either input is gone; the compiler needs it because the result may borrow from either (E0106).

*What the interviewer is looking for: the lifetime annotation and its meaning for callers.*

```rust
fn best_level(levels: &[i64]) -> &i64 {
    let top = levels[0] + 1;
    &top
}

fn main() {
    println!("{}", best_level(&[100]));
}
```

***Listing 23.9.** Returning a reference to a local. code/interviews/23-rust/rust/snippets/return_local.rs*

**Interview question 23.6 ★★ developer • proprietary firm.**

Why is [Listing 23.9](#lst-iv-rust-local) rejected, and what should the function return instead?

**Solution of Interview question 23.6.**

`top` is a local that is dropped when the function returns, so a reference to it would dangle: E0515. Return the value, `-> i64`, which for an integer costs nothing; for a large value return it by value (moved) or a reference into the input.

*What the interviewer is looking for: the lifetime of locals and returning by value.*

```rust
struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    _first: Noisy,
    _second: Noisy,
}

fn main() {
    let _a = Noisy("a");
    let _p = Pair { _first: Noisy("first"), _second: Noisy("second") };
    let _b = Noisy("b");
    let _ = Noisy("ignored");
    println!("end of main");
}
```

***Listing 23.10.** Who is dropped when. code/interviews/23-rust/rust/snippets/drop_order.rs*

**Interview question 23.7 ★★ developer • market maker.**

What does [Listing 23.10](#lst-iv-rust-drop) print? How does the order of the struct’s fields compare with C++?

**Solution of Interview question 23.7.**

`drop ignored`, `end of main`, `drop b`, `drop first`, `drop second`, `drop a`. `let _ =` binds nothing, so the value is dropped at once; locals are dropped in reverse declaration order (`_b`, `_p`, `_a`); a struct’s fields are dropped in declaration order (`first` then `second`), whereas C++ destroys members in reverse declaration order ([Interview question 22.2](https://one-course.com/books/quant/18/en/chapter/22-c#iq-iv-cpp-2)).

*What the interviewer is looking for: the three drop rules, including the `let _` trap and the contrast with C++.*

**Interview question 23.8 ★★ developer • crypto firm.**

Write an iterator over the `|`-separated fields of a message in a byte buffer that allocates nothing. What lifetime does each field have?

**Solution of Interview question 23.8.**

The iterator holds `&’a [u8]` and a position and returns `&’a [u8]` slices of the buffer ([Listing 23.3](#lst-iv-rust-fields)): each field lives as long as the buffer, not as long as the iterator, so fields can be kept after the iterator is gone. The crate’s tests check the fields of a sample message and empty fields.

*What the interviewer is looking for: a struct with a lifetime and returned slices tied to the buffer.*

**Interview question 23.9 ★★ developer, mle • any.**

Count total volume per symbol from a slice of (symbol, volume) pairs with one map lookup per trade.

**Solution of Interview question 23.9.**

`*map.entry(sym).or_insert(0) += v;`: the entry API finds or creates the slot once and returns a mutable reference to it, where a `get` followed by an `insert` would hash twice. The crate’s `volume_by_symbol` does this.

*What the interviewer is looking for: the entry API and why it avoids a second lookup.*

**Interview question 23.10 ★★★ developer • proprietary firm.**

A strategy interface is called once per market-data message. Compare a generic function bounded by the trait with a `&dyn` trait object: code size, speed, flexibility. What makes a trait unusable as a trait object?

**Solution of Interview question 23.10.**

Generic: one copy per strategy type, calls resolved at compile time and inlinable, larger code, the strategy fixed at compile time. Trait object: one copy, an indirect call through a vtable per message (a few nanoseconds and no inlining), strategies chosen at run time from configuration. On a hot path with few strategy types, generics; for a plug-in architecture, `dyn`. A trait is not object-safe if a method is generic, returns `Self`, or takes `self` by value without a `Sized` bound. The crate checks that both dispatches give the same result.

*What the interviewer is looking for: monomorphisation against vtables, the trade-off, and object safety.*

```rust
use std::rc::Rc;
use std::thread;

fn main() {
    let book = Rc::new(vec![100, 101]);
    let h = thread::spawn(move || book.len());
    println!("{}", h.join().unwrap());
}
```

***Listing 23.11.** Sharing a book with a thread. code/interviews/23-rust/rust/snippets/rc_not_send.rs*

**Interview question 23.11 ★★★ developer • crypto firm.**

Why is [Listing 23.11](#lst-iv-rust-rc) rejected? Fix it, and explain the difference between `Send` and `Sync`.

**Solution of Interview question 23.11.**

`Rc` counts references non-atomically, so two threads cloning or dropping it could corrupt the count: it is not `Send`, and `thread::spawn` requires its closure to be `Send` (E0277). Use `Arc`, whose count is atomic. `Send`: a value may be moved to another thread. `Sync`: a shared reference may be used from several threads at once (a type is `Sync` if `&T` is `Send`); `Cell` is `Send` but not `Sync`, `Mutex<T>` makes a `Send` `T` usable as `Sync`.

*What the interviewer is looking for: atomic against non-atomic counting, and the precise definitions of the two traits.*

**Interview question 23.12 ★★★ developer • market maker.**

Justify the `unsafe` block of [Listing 23.4](#lst-iv-rust-unsafe). What would make it unsound, and how would you check it mechanically?

**Solution of Interview question 23.12.**

The assertion that $n$ does not exceed the slice’s length, checked once before the loop, guarantees that every index $i < n$ is in bounds, so the unchecked read never goes out of bounds; the safety comment states the invariant. It would be unsound if the check were removed, weakened to a `debug_assert!` (absent in release builds), or placed after the loop, or if the slice could shrink during the loop (it cannot, being borrowed immutably). Check mechanically with Miri, which interprets the code and detects out-of-bounds reads, and with a test that the function panics on a bad `n` (the crate has one). Often the compiler removes the bounds checks of the safe version anyway (One Quant Book 13, chapter 9).

*What the interviewer is looking for: the invariant that makes `unsafe` sound, where it would break, and Miri.*

**Interview question 23.13 ★★★ developer • market maker.**

Several threads increment a shared counter. Write it without a lock. Which memory ordering do you use, and when would a weaker or stronger one be wrong?

**Solution of Interview question 23.13.**

```rust
/// A counter shared by threads: fetch_add loses no increment; Relaxed suffices for a count.
pub fn count_in_threads(threads: usize, per_thread: u64) -> u64 {
    let c = Arc::new(AtomicU64::new(0));
    let handles: Vec<_> = (0..threads)
        .map(|_| {
            let c = Arc::clone(&c);
            thread::spawn(move || {
                for _ in 0..per_thread {
                    c.fetch_add(1, Ordering::Relaxed);
                }
            })
        })
        .collect();
    for h in handles {
        h.join().expect("thread panicked");
    }
    c.load(Ordering::Relaxed)
}
```

*A shared counter with an atomic `fetch_add`. code/interviews/23-rust/rust/src/lib.rs*

`fetch_add` is a single atomic read-modify-write, so no increment is lost whatever the ordering; `Relaxed` is enough for a pure count read after the threads are joined (joining synchronises). A stronger ordering is needed when the counter publishes other data: a producer that writes a message and then increments a sequence number needs `Release` on the increment and the consumer `Acquire` on the read (One Quant Book 13, chapter 11). The crate’s test counts 400 000 increments from four threads exactly.

*What the interviewer is looking for: atomic read-modify-write, and when ordering matters beyond the counter itself.*

Sources and further reading

- The Rust Reference and the Rustonomicon (for the toolchain pinned by the series, 1.97.1), and the rustc error index (E0106, E0277, E0382, E0499, E0502, E0515).
- One Quant Book 13, chapters 9 and 11 (Rust for low latency; lock-free programming).
