gap·map

Rust ownership & borrowing

[ middle depth ]

The three ownership rules

Every value in Rust has exactly one owner. There is one owner at a time, and when the owner goes out of scope the value is dropped (its Drop runs, its heap memory is freed). That is the whole model, and everything else follows from keeping those three facts true.

Assignment is where it bites. For a type that owns a heap resource, let b = a moves ownership to b and invalidates a:

let a = String::from("hi");
let b = a;            // ownership moves a -> b
// println!("{a}");   // compile error: value borrowed after move

Wrong "the assignment copied the string, so both are usable." A bitwise copy would leave two owners of one heap buffer, and both would try to free it at scope end (a double free). Rust forbids that by moving instead. If you truly want two independent owners, ask for it with a.clone(), which allocates a second buffer.

Move vs Copy vs Clone

Fixed-size, stack-only types (i32, bool, char, f64, tuples of those) implement Copy. For them let b = a duplicates the bits and leaves a valid, because there is no heap resource to double-free. String, Vec<T>, and Box<T> own heap data, so they move and do not implement Copy. They do implement Clone, which is the explicit deep copy you call by hand. clone() costs an allocation, so treat it as a real choice, not the reflex you use to quiet the compiler.

Borrowing: shared XOR mutable

Instead of transferring ownership you can borrow a reference. The rule holds at every point in the program: any number of shared references &T, XOR exactly one mutable reference &mut T, never both at once.

let mut v = vec![1, 2, 3];
for x in &v {          // shared borrow, live for the whole loop
    v.push(*x);        // error: needs &mut v while &v is live
}

This is the classic mutate-while-iterating fight, and the compiler is protecting you from a real bug: push can reallocate the vector's buffer, which would dangle the iterator mid-loop. Restructure instead of fighting it. Collect what you need first (let extra: Vec<_> = v.clone(); then extend), iterate by index, or use retain/drain when you are filtering.

Two more fights you will hit early. Use-after-move (let a = s; use(s);) is fixed by borrowing (&s) or cloning. Returning a reference to a local (fn f() -> &str { let s = String::from("x"); &s }) cannot work because s drops when f returns, leaving the reference dangling; return the owned String instead. In both cases the fix is to change who owns what, not to sprinkle & until it builds.

[ senior depth ]

What a move actually compiles to

A move is a shallow bitwise copy of the value (for a String, the three words: pointer, length, capacity) followed by the compiler statically marking the source binding invalid. No move constructor, no hook, no runtime cost. The heap buffer is untouched; only ownership of it changes hands. Copy types are the case where the source is NOT invalidated after the copy, which is why Copy and Drop are mutually exclusive: a type needing custom cleanup cannot be one whose bits are freely duplicable. Move and copy are the same machine operation; the difference is only whether the compiler lets you keep using the source.

Why aliasing XOR mutation, beyond safety

The usual answer is memory safety, and that is true: a live &Vec while something calls push could dangle when the buffer reallocates. The deeper answer interviewers want is that &mut T is an aliasing guarantee the optimizer relies on. A &mut is effectively noalias: the compiler may assume nothing else reads or writes that memory for the reference's lifetime, so it can keep values in registers across calls and reorder loads and stores. This is why the rule is enforced even in single-threaded code where no data race is possible. You are signing an optimization contract, not just dodging a race.

The borrow checker got sharper with non-lexical lifetimes: a borrow ends at its last use, not at the closing brace. That is why taking &mut v on one line and &v on the next now compiles when the first reference is never touched again.

Lifetimes describe, they do not extend

Wrong "adding 'a makes the reference live longer." Lifetime annotations change nothing at runtime. They are a way to state a relationship the compiler must verify: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str promises the result will not outlive either input. Most of the time you write none, because elision fills them in. The three rules (from the Rustonomicon): each elided input lifetime becomes its own parameter; if there is exactly one input lifetime it is given to all elided outputs; if a method takes &self or &mut self, self's lifetime is given to all elided outputs. When none apply and you return a reference, you must annotate, and that is the only time 'a appears in ordinary code.

RefCell moves the check to runtime

When the ownership graph genuinely needs shared mutation (a node reachable through several Rc pointers), RefCell<T> is the escape hatch. It does not remove the shared-XOR-mutable rule; it moves the check from compile time to run time. borrow() and borrow_mut() hand back Ref/RefMut guards and track how many are live, and violating the rule panics with already borrowed: BorrowMutError instead of failing to build. Cell<T> is the leaner, Copy-only variant with no guards and no borrow counting, just get/set/replace. Interviewers read the choice closely: reaching for RefCell trades a compile-time guarantee for a runtime one, so you should be able to say why the static version could not express your graph.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

unlocks

more Rust

was this useful?