References and Borrowing

Aliasing and Mutation

Aliasing occurs when two or more pointers or references refer to the same memory region at the same time. Mutation occurs when the contents of that memory region are modified. While aliasing alone (shared read-only access) and mutation alone (exclusive write access) are each completely harmless, combining unrestricted aliasing and mutation simultaneously breaks both memory safety and compiler optimizations.

Three Hazards of Simultaneous Aliasing and Mutation

1. Memory Reallocation and Dangling Pointers

Consider a growable collection like Vec<T> or String. If reader holds a reference &vec[0] to the first element while vec.push(x) mutates the vector, push may fill the current heap capacity, allocate a new heap buffer at a different address, copy the elements over, and free the old buffer—leaving reader pointing to freed memory:

1. Before `v.push(40)`:
   reader: &i32 ─────────────┐
                             v
   v: Vec<i32>  ───> Heap: [ 10 | 20 | 30 ] (len = 3, cap = 3 — FULL!)

2. If `v.push(40)` were allowed while `reader` existed:
   reader: &i32 ───────────> [ FREED MEMORY! ]  (❌ Use-After-Free!)
   v: Vec<i32>  ───> New Heap: [ 10 | 20 | 30 | 40 ] (cap = 6)

Rust's borrow checker prevents this at compile time (E0502): v.push(40) requires &mut v, which is forbidden while reader: &i32 is still active.

2. Enum Variant Overwriting (Type Confusion)

Suppose an enum Message holds Message::Text(String::from("hello")), and r: &String borrows the inner String. If the enum were mutated in place to Message::Number(42) while r still existed, r would attempt to interpret the integer 42 as a String (ptr, len, cap) pointer—causing instant memory corruption:

// ❌ does not compile
enum Value {
    Text(String),
    Number(u64),
}

fn main() {
    let mut val = Value::Text(String::from("hello"));

    if let Value::Text(ref inner_str) = val {
        // Error: cannot overwrite `val` while `inner_str: &String` borrows its payload!
        val = Value::Number(99999);
        println!("{inner_str}");
    }
}
error[E0506]: cannot assign to `val` because it is borrowed
  --> src/main.rs:11:9
   |
 9 |     if let Value::Text(ref inner_str) = val {
   |                        ------------- `val` is borrowed here
10 |         // Error: cannot overwrite `val` while `inner_str: &String` borrows its payload!
11 |         val = Value::Number(99999);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ `val` is assigned to here but it was already borrowed
12 |         println!("{inner_str}");
   |                   ----------- borrow later used here

error: aborting due to 1 previous error

3. Broken Logic & Missed Compiler Optimizations (noalias)

In C and C++, when a function takes void compute(const int* input, int* output), the compiler must conservatively assume that input and output might point to the exact same address in memory. Consequently, after every write to *output, the C compiler is forced to reload *input from RAM instead of keeping it in a CPU register or auto-vectorizing the loop (unless the programmer manually adds C's unsafe restrict keyword).

In Rust, because &T and &mut T can never alias the same memory, rustc automatically passes LLVM's noalias attribute on every reference parameter!

pub fn add_scaled(input: &[i32; 4], output: &mut [i32; 4], factor: i32) {
    // Because `input: &[i32; 4]` and `output: &mut [i32; 4]` are GUARANTEED never to overlap,
    // LLVM can load all 4 ints into a single SIMD register without reloading after each write!
    for i in 0..4 {
        output[i] += input[i] * factor;
    }
}

fn main() {
    let src = [1, 2, 3, 4];
    let mut dst = [10, 20, 30, 40];
    add_scaled(&src, &mut dst, 2);
    println!("dst = {dst:?}");
}
dst = [12, 24, 36, 48]