On this page
References and Borrowing
The Rust borrow checker follows a single rule: at any given moment, you can have either one mutable reference or multiple immutable references, but never both (aliasing XOR mutation). NLL refines the checking granularity from lexical scope to actual usage intervals, and reborrowing eliminates the need for explicit annotations in nested borrows—these three elements together constitute Rust’s "memory safety without a GC."
& vs &mut
let mut s = Stringfrom;
let r1 = &s; // Immutable borrow (shared reference)
let r2 = &s; // Multiple immutable borrows are allowed
println!; // OK
let r3 = &mut s; // Mutable borrow: requires s to be declared mut
// let r4 = &s; // ERROR: Cannot create & while &mut exists
r3.push;
Core rule — Aliasing XOR Mutation: At any given moment, you can have either multiple shared references (read-only) or exactly one mutable reference (writable), but never both. This rule prevents data races and iterator invalidation.
How the Borrow Checker Works
The compiler performs borrow checking on the MIR (Mid-level IR). When each borrow is created, it records: which variable was borrowed, whether it is shared or mutable, and until where it lives.
let mut v = vec!;
let first = &v; // Immutable borrow of v starts
v.push; // ERROR: Mutable borrow of v — first is still live
println!; // Last use of first
// first's borrow ends here
Reason for the error: v.push(4) requires &mut v, but first is still in scope holding &v. The borrow checker does not allow &v and &mut v to coexist—even if you know that push will not affect v[0], the borrow checker does not know that.
Non-Lexical Lifetimes (NLL, 2018 Edition)
Before NLL, the lifetime of a borrow was determined by scope (lexical—the entire block from declaration to last use). NLL changes this to determined by the last use:
let mut v = vec!;
let first = &v;
println!; // Last use of first
// first's borrow ends here (not at the end of the scope!)
v.push; // OK: first is no longer live, borrow has ended
// This would not compile before NLL
Reborrow
When passing &mut T to a function, the borrow is "reborrowed":
let mut v = vec!;
push; // Reborrow: &mut *v → push's &mut v
// After push returns, the reborrow ends → v's ownership is restored
// Equivalent to:
// let reborrow = &mut *v;
// push(reborrow);
// drop(reborrow); ← reborrow is dropped here, v is restored
Memory Layout of References
&T and &mut T are just regular pointers at runtime (8 bytes on 64-bit). All borrow checker checks occur at compile time, with zero runtime overhead.
let x = 42;
let r = &x; // Assembly: lea rax, [rsp+4]; mov [rsp+8], rax
Fat pointers: &[T] and &dyn Trait are 16 bytes—storing an extra length or vtable pointer.
Common Pitfalls
1. Cannot modify the original value during an immutable borrow
let mut v = vec!;
let r = &v;
v.clear; // ERROR: &v (immutable) is active, cannot &mut v
2. Iterator invalidation is prevented by the borrow checker
let mut v = vec!;
for item in &v
3. Returning references from functions requires explicit lifetimes
// The returned reference borrows from s → the compiler guarantees that the &str does not outlive s
References
- Rust Book: Chapter 4.2 — References and Borrowing
- NLL RFC: RFC 2094
- Polonius: github.com/rust-lang/polonius (next-gen borrow checker)
Keywords: borrow checker, shared reference, mutable reference, NLL, reborrow, aliasing XOR mutation, fat pointer