Lecture 17 - &mut and the Borrow Checker

Logistics

  • Last time: Immutable references (&) and basic borrowing
  • Today: Mutable references (&mut) and complete borrowing rules

Things "in flight"

  • HW1 correction grading should be complete - if not let me know - your corrected % was in the update email
  • HW2 correction grading is in progress, will be done by Thursday - status was in update email
  • HW3 grading has started, should be done by Friday
  • HW4 is open, due on 10/24 - you should have everything you need after tomorrow (Strings)
  • Exam 1 corrections are due this Wednesday evening - if you're interested in an oral exam please fill out that part of the assignment.
    • There's no 48-hour late window
    • There was apparently a bug in the correction item for question 1.8 - if you already submitted a correction for that please double-check that it's there.

Learning Objectives

By the end of today, you should be able to:

  • Use mutable references (&mut T) to modify borrowed data
  • Understand and apply Rust's borrowing rules (the borrow checker)
  • Debug more borrowing compiler errors

Let's review (and clarify) immutable borrows (TC 12:25)

Question 1 from Friday

fn main() {
    let data = vec![1, 2, 3];
    print_data(data);
    println!("{:?}", data); // Fix this!
}

fn print_data(v: Vec<i32>) {
    println!("{:?}", v);
}

Question 2 from Friday

fn main() {
    let scores = vec![85, 92, 78];
    let first = scores[0];  // This works, but...

    let names = vec![String::from("Alice")];
    let first_name = names[0];  // This doesn't! Fix it

    println!("First score: {}", first);
    println!("First name: {}", first_name);
}

A note about Question 5:

fn main() {
    let pairs = vec![(1, 2), (3, 4), (5, 6)];

    for (a, b) in pairs.iter() {
        let sum = a + b;  // Error! Can't add references
        println!("{} + {} = {}", a, b, sum);
    }

    println!("Pairs still available: {:?}", pairs);
}

This actually compiles, and practically anything I tried to do to break it compiles too.

In fact, it looks like + actually auto-dereferences &i32 (in contrast with what I said Friday).

Clarifying how *&x != x exactly

While * and & are inverse operations, dereferencing doesn't transfer ownership:

fn main() {
    let x = 42;
    let x_ref = &x;

    let y = *x_ref;  // For Copy types, this copies the value
    println!("x: {}, y: {}", x, y);  // Both work

    let name = String::from("Bob");
    let name_ref = &name;

    // let owned = *name_ref;  // ERROR! Would need to move, but we only borrowed
    let cloned = (*name_ref).clone();  // Must explicitly clone
    println!("name: {}, cloned: {}", name, cloned);
}

Stack/heap diagram:

For: let name_ref = &name; and accessing *name_ref

Stack:                               Heap:
┌─────────────────┐
│ name_ref ───────┼──┐               ┌─────┬─────┬─────┐
├─────────────────┤  │               │ 'B' │ 'o' │ 'b' │
│ name: String    │◄─┘               └─────┴─────┴─────┘
│ ├ ptr ──────────┼─────────────────▶   
│ ├ len: 3        │                     
│ └ cap: 3        │                     
└─────────────────┘                     

Summary:

  • *&x gives you access to x's value
  • For Copy types: makes a copy
  • For non-Copy types: gives access but can't take ownership (you only borrowed!)

Draw out copy case on the board

Pattern Matching with &: Only for Copy Types!

When you use for &val in arr.iter(), the & pattern extracts the value from the reference:

fn main() {
    // With Copy types - works!
    let numbers = vec![1, 2, 3];
    for &num in numbers.iter() {
        // num is i32 (copied from &i32)
        println!("{}", num * 2);
    }

    // With non-Copy types - ERROR!
    let names = vec![String::from("Alice"), String::from("Bob")];
    // for &name in names.iter() {  // Can't move out of borrowed reference
    //      let name_copy = name.clone();
    //      println!("{}", name_copy);
    // }

    // Must keep as reference for non-Copy types
    for name in names.iter() {
        // name is &String
        let name_copy = (*name).clone();  // Need * to clone
        println!("Cloned: {}", name_copy);
    }
}

What's happening:

  • &val pattern matching extracts the value, not a reference
  • For Copy types: copies the value out → val is T
  • For non-Copy types: would try to move → ERROR (can't move from borrowed content)
  • Solution for non-Copy: use val without &val is &T

Mutable References (&mut T) (TC 12:30)

Use &mut to create a mutable reference that allows modification:

fn main() {
    let mut data = vec![1, 2, 3];  // Must be mut to begin with!
    let data_ref = &mut data;      // Mutable reference 

    // Can modify through mutable reference:
    data_ref.push(4); 
    data_ref[0] = 10;

    println!("Modified: {:?}", data_ref);

    add_item(&mut data, 5);
    println!("After adding: {:?}", data);  // [10, 2, 3, 4, 5]
}

fn add_item(data: &mut Vec<i32>, item: i32) {  // Mutable reference parameter
    data.push(item);  // Can modify!
}

Stack/Heap with Mutable References

fn main() {
    let mut message = String::from("Hello");
    modify_string(&mut message);
    println!("{}", message);  // "Hello - modified!"
}

fn modify_string(text: &mut String) {
    text.push_str("!!!");
}

Memory progression through the function call:

Step 1: Before calling modify_string()

Stack:                               Heap:
┌─────────────────┐                 ┌─────┬─────┬─────┬─────┬─────┐
│ message: String │                 │ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │
│ ├ ptr ──────────┼────────────────▶│     │     │     │     │     │
│ ├ len: 5        │                 └─────┴─────┴─────┴─────┴─────┘
│ └ cap: 5        │
└─────────────────┘

Step 2: During modify_string() call (before push_str)

Stack:                               Heap:
┌─────────────────┐ 
│ text: &mut String─────┐
├─────────────────┤     │           
│ message: String │◄────┘           ┌─────┬─────┬─────┬─────┬─────┐
│ ├ ptr ──────────┼────────────────▶│ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │
│ ├ len: 5        │                 └─────┴─────┴─────┴─────┴─────┘
│ └ cap: 5        │    
└─────────────────┘

Step 3: After text.push_str(" - modified!")

Stack:                               Heap:
┌─────────────────┐ 
│ text: &mut String─────┐
├─────────────────┤     │        
│ message: String │◄────┘           ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ ├ ptr ──────────┼────────────────▶│ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │ '!' │ '!' │ '!' │
│ ├ len: 17       │                 └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ └ cap: 18       │     
└─────────────────┘

Step 4: After function returns

Stack:                               Heap:
┌─────────────────┐    
│ message: String │                 ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ ├ ptr ──────────┼────────────────▶│ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │ '!' │ '!' │ '!' │
│ ├ len: 17       │                 └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ └ cap: 18       │
└─────────────────┘

Borrowing Rules (The Borrow Checker) (TC 12:35)

Rust enforces strict rules about borrowing to prevent memory corruption:

  • Rule 1: You can have EITHER many immutable references OR ONE mutable reference
  • Rule 2: References must be valid (they cannot outlive what they refer to)

Rule 1: You can have EITHER many immutable references OR ONE mutable reference

This works (multiple immutable references):

fn main() {
    let data = vec![1, 2, 3];
    let ref1 = &data;
    let ref2 = &data;
    let ref3 = &data;

    println!("{:?} {:?} {:?}", ref1, ref2, ref3);  // All read-only
}

This works (one mutable reference):

fn main() {
    let mut data = vec![1, 2, 3];
    let ref1 = &mut data;

    ref1.push(4);
    println!("{:?}", ref1);  // Only one mutable reference
}

This does NOT work:

fn main() {
    let mut data = vec![1, 2, 3];
    let ref1 = &data;      // Immutable reference
    let ref2 = &mut data;  // ERROR! Can't have both!

    println!("{:?} {:?}", ref1, ref2);
}

Rule 2: References must be valid (they cannot outlive what they refer to)

This does NOT work:

fn main() {
    let reference;
    {
        let value = vec![1, 2, 3];
        reference = &value;  // ERROR! value will be dropped
    }  // value goes out of scope here
    // println!("{:?}", reference);  // reference would be dangling!
}

This is why you can't return from a function a reference to a variable you defined inside the function

Why These Rules Matter

Without these rules, you could have:

  1. Data races: Two threads modifying the same data simultaneously
  2. Use-after-free: Using memory that's been freed
  3. Iterator invalidation: Modifying a collection while iterating

Rust prevents all of these at compile time!

Important: Modifying Through the Original Name

Even modifying through the original variable name counts as a mutable borrow!

fn main() {
    let mut x = 10;
    println!("{}", x);  
    let y = &x;           // Immutable borrow
    x = 20;               // ERROR! Tries to mutably borrow x
    println!("{}", y);    // y is still being used
}

Error: "cannot assign to x because it is borrowed"

Why? When you have an active reference (y), Rust must guarantee that reference stays valid. Modifying x directly would be a mutable operation, which conflicts with the immutable borrow.

fn main() {
    let mut x = 10;
    let y = &x;
    println!("{}", y);    // Last use of y

    x = 20;               // OK! y is no longer used
    println!("{}", x);
}

Key insight: The original variable name doesn't give you special privileges! While a reference exists and is being used, you can't modify the data through any path—not even the original name.

Mutable and immutable borrowing in practice

fn main() {
    let mut scores = vec![85, 92, 78, 96, 88];

    // Analyze first (immutable borrow)
    let (total, average) = analyze_data(&scores);
    println!("Total: {}, Average: {:.1}", total, average);

    // Then normalize (mutable borrow)
    normalize_data(&mut scores);
    println!("Normalized: {:?}", scores);
}

fn analyze_data(data: &Vec<i32>) -> (i32, f64) {
    let sum: i32 = data.iter().sum();
    let avg = sum as f64 / data.len() as f64;
    (sum, avg)
}

fn normalize_data(data: &mut Vec<i32>) {
    let max = *data.iter().max().unwrap();
    for item in data.iter_mut() {
        *item = *item * 100 / max;
    }
}

This works because:

  • analyze_data finishes before normalize_data starts
  • No overlap between immutable and mutable borrows
  • Original data stays accessible in main

Note on function signatures: In these examples we use &Vec<i32> for clarity. In practice, Rust developers usually use slices (&[i32]) which are more flexible. We'll cover slices in the next lecture!

Think-Pair-Share: Mutable Borrowing vs Ownership (TC 12:40)

Question: How are mutable borrowing and transferring ownership the same, and how are they different? When should you use one vs the other?

Think (1 minute): Consider these two function signatures:

#![allow(unused)]
fn main() {
fn process_data(data: Vec<i32>) -> Vec<i32>  // Takes ownership
fn process_data(data: &mut Vec<i32>)         // Borrows
}

What are the tradeoffs? When would you choose each approach?

Mutable Borrowing vs Ownership

Similarities:



Differences:



Use &mut T (mutable borrow) when:



Use T (transfer ownership) when:



Rule of thumb:

Mixing Immutable and Mutable References

The timing matters! This works:

fn main() {
    let mut integer = 10;

    // Use immutable reference first
    let ir = &integer;
    println!("Reading: {}", ir);
    // ir is no longer used after this point

    // Now we can create a mutable reference
    let mr = &mut integer;
    *mr += 5;
    println!("After modification: {}", mr);

    // Can create new immutable references after mutable is done
    let ir2 = &integer;
    println!("Reading again: {}", ir2);
}

But this doesn't work:

fn main() {
    let mut integer = 10;

    let ir = &integer;      // Immutable reference
    let mr = &mut integer;  // ERROR! Can't create mutable while immutable exists

    println!("{}", ir);     // ir is still being used
    *mr += 5;
}

Key insight: Rust tracks when references are last used, not just when they go out of scope!

More on iter() and iter_mut() (TC 12:45)

We saw .iter() last time - now we'll add .iter_mut():

  • .iter(): Gives you immutable references (&T) to each element
  • .iter_mut(): Gives you mutable references (&mut T) to each element
fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];

    // .iter() - read-only access
    for num in numbers.iter() {
        println!("{}", num);  // num is &i32
        // *num += 1;  // ERROR! Can't modify through immutable reference
    }

    // .iter_mut() - mutable access
    for num in numbers.iter_mut() {
        *num += 10;  // num is &mut i32 - can modify!
    }

    println!("Modified: {:?}", numbers);  // [11, 12, 13, 14, 15]
}

Dereferencing with iter_mut()

Unlike with .iter() where you can use for &num in... pattern matching (for copy types!), with .iter_mut() you always need to dereference with * to modify the value:

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];

    // Must use * to modify through mutable reference
    for num in numbers.iter_mut() {
        *num *= 2;  // num is &mut i32, *num is i32
    }

    println!("{:?}", numbers);  // [2, 4, 6, 8, 10]
}

Why no pattern matching? With .iter() you can work off a copy because you're just reading. With .iter_mut() you need the mutable reference itself to assign through it, so you must use *.

Enumerate with iter_mut()

You can combine .iter_mut() with .enumerate() to get both the index and a mutable reference:

fn main() {
    let mut scores = vec![78, 85, 92, 67, 88];

    // enumerate gives (usize, &mut i32)
    for (i, score_ref) in scores.iter_mut().enumerate() {
        println!("Score {}: {}", i, score_ref);

        // Modify based on index
        if i == 0 {
            *score_ref += 10;  // Bonus for first student
        }
    }

    println!("Updated scores: {:?}", scores);  // [88, 85, 92, 67, 88]
}

Type breakdown:

  • i is usize (the index)
  • score_ref is &mut i32 (mutable reference to the element)
  • Use *score_ref to modify the value

Activity Time

Key Takeaways

  1. Mutable references (&mut T): Allow modification of borrowed data
  2. Borrowing rules prevent bugs: No data races, no use-after-free, no iterator invalidation
  3. Two key rules: Many readers OR one writer (and not both!), references must live long enough
  4. Timing matters: Rust tracks when references are last used, not just scope
  5. Sequential borrowing: You can have mutable then immutable, or vice versa
  6. Design principle: Separate read-only and modification phases in your code