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:
*&xgives you access tox's value- For
Copytypes: makes a copy - For non-
Copytypes: 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:
&valpattern matching extracts the value, not a reference- For Copy types: copies the value out →
valisT - For non-Copy types: would try to move → ERROR (can't move from borrowed content)
- Solution for non-Copy: use
valwithout&→valis&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:
- Data races: Two threads modifying the same data simultaneously
- Use-after-free: Using memory that's been freed
- 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_datafinishes beforenormalize_datastarts- 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:
iisusize(the index)score_refis&mut i32(mutable reference to the element)- Use
*score_refto modify the value
Activity Time
Key Takeaways
- Mutable references (&mut T): Allow modification of borrowed data
- Borrowing rules prevent bugs: No data races, no use-after-free, no iterator invalidation
- Two key rules: Many readers OR one writer (and not both!), references must live long enough
- Timing matters: Rust tracks when references are last used, not just scope
- Sequential borrowing: You can have mutable then immutable, or vice versa
- Design principle: Separate read-only and modification phases in your code