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 = String::from("hello");
let r1 = &s;                         // Immutable borrow (shared reference)
let r2 = &s;                         // Multiple immutable borrows are allowed
println!("{} {}", r1, r2);           // 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![1, 2, 3];
let first = &v[0];                   // Immutable borrow of v starts
v.push(4);                           // ERROR: Mutable borrow of v — first is still live
println!("{}", first);               // 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![1, 2, 3];
let first = &v[0];
println!("{}", first);               // Last use of first
// first's borrow ends here (not at the end of the scope!)

v.push(4);                           // 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":

fn push(v: &mut Vec<i32>) { v.push(5); }

let mut v = vec![1, 2, 3];
push(&mut v);                        // 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![1, 2, 3];
let r = &v[0];
v.clear();                           // ERROR: &v (immutable) is active, cannot &mut v

2. Iterator invalidation is prevented by the borrow checker

let mut v = vec![1, 2, 3];
for item in &v {                     // The for loop implicitly creates a borrow of &v
    v.push(4);                       // ERROR: &v is active, cannot &mut v
}

3. Returning references from functions requires explicit lifetimes

fn first_word(s: &str) -> &str {     // Lifetime elision: actually fn<'a>(s: &'a str) -> &'a str
    &s[..s.find(' ').unwrap_or(s.len())]
}
// 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