Mutable Borrowing (&mut)
Mutable Borrowing (&mut)
Level 3 — Ownership & Borrowing Creating an exclusive mutable reference; only one
&mutis allowed at a time.
1. Prerequisites
- Borrowing (
&) — The concept of passing references instead of Ownership. - Mutability (
mut) — The keyword required to allow data to change. - Expressions — Knowing how
{}scopes work is critical for managing how long a borrow lasts.
2. Term Category
Rust-specific (the strict safety rules): While pointers in C++ allow unrestricted and highly dangerous mutation, Rust introduces a strict rule known in computer science as "Aliasing XOR Mutability". You can have many readers, OR exactly one writer, but never both at the same time.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In languages like Java, Python, or C++, multiple variables or threads can hold a reference to the exact same list. If Thread A is looping over the list while Thread B is simultaneously deleting items from it, the program will crash or produce garbage data. This is called a Data Race, and it is one of the hardest bugs to track down in programming.
Rust was designed to completely eliminate Data Races at compile time. It does this via the strict rules of Mutable Borrowing (&mut).
Rust guarantees that a mutable reference is exclusive. If you hold a &mut reference to data, no one else is allowed to hold any reference to that data (not even a read-only one!). Because the compiler mathematically proves that the writer is the only person accessing the memory, data races are impossible.
(2) Reality Metaphor
Imagine a whiteboard in a conference room.
Standard Borrowing (&) is like opening the window blinds. You can have 10 people looking at the whiteboard simultaneously, because looking doesn't change anything.
Mutable Borrowing (&mut) is handing one person a marker. If someone has a marker and is actively changing the board, you must close the blinds. No one else is allowed to even look at the board while it's being written to, because they might read half-finished, incorrect data.
The Golden Rule: You can have many readers, or exactly one writer. Never both.
(3) Rust Code Examples
Short Snippet (Modifying Data via Reference)
To mutably borrow data, both the original variable AND the reference must be marked mut.
fn add_world(s: &mut String) {
s.push_str(" World!");
} // The mutable borrow ends here.
fn main() {
// 1. The original variable MUST be `mut`
let mut greeting = String::from("Hello");
// 2. We pass a mutable reference `&mut`
add_world(&mut greeting);
println!("{}", greeting); // Prints: Hello World!
}
Fuller Example (The Exclusivity Rule)
The compiler will aggressively stop you if you try to break the Golden Rule.
fn main() {
let mut book = String::from("The Rust Book");
// Bob borrows the book mutably (He has the marker!)
let bob_editor = &mut book;
// Alice tries to borrow the book to read it...
// let alice_reader = &book; // COMPILER ERROR! You cannot read while Bob is editing!
// Bob tries to give his friend Charlie a marker too...
// let charlie_editor = &mut book; // COMPILER ERROR! Only ONE marker allowed!
bob_editor.push_str(" - 2nd Edition");
// Bob's borrow ends here. Now it's safe to read again!
println!("Final book: {}", book);
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Mutable Borrowing Scoping and Lifecycle Rules
The mistake: Assuming Mutable Borrowing instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("mutable_borrowing_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("mutable_borrowing_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Mutable Borrowing State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Mutable Borrowing through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Mutable Borrowing Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Mutable Borrowing instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Zero-Copy Ingest Buffer Framing & Disjoint Mutable Slices
Scenario: In high-throughput network packet ingestion pipelines, raw byte arrays must be processed in-place without memory allocation. A contiguous buffer &mut [u8] contains a fixed header followed by frame payload data. Naive indexing attempts like let h = &mut buf[..4]; let p = &mut buf[4..]; fail compile-time borrow checks because the compiler treats buf as being borrowed mutably twice.
Task: Write a production-grade PacketStreamProcessor struct that wraps a mutable slice borrow &'a mut [u8]. Implement split_header_and_payload(&mut self, header_len: usize) using split_at_mut to produce disjoint header and payload mutable slices without violating the aliasing XOR mutability invariant. Implement transform_in_place to invert header flags and apply an XOR cipher mask to payload bytes. Include comprehensive unit tests with explicit assertions (assert!, assert_eq!, assert_ne!, matches!).
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub enum PacketError {
BufferTooSmall,
InvalidHeader,
}
pub struct PacketStreamProcessor<'a> {
buffer: &'a mut [u8],
}
impl<'a> PacketStreamProcessor<'a> {
pub fn new(buffer: &'a mut [u8]) -> Self {
Self { buffer }
}
pub fn split_header_and_payload(
&mut self,
header_len: usize,
) -> Result<(&mut [u8], &mut [u8]), PacketError> {
if self.buffer.len() < header_len {
return Err(PacketError::BufferTooSmall);
}
// Safe split into disjoint, non-overlapping mutable slices
let (header, payload) = self.buffer.split_at_mut(header_len);
Ok((header, payload))
}
pub fn transform_in_place(
&mut self,
header_len: usize,
key: u8,
) -> Result<(), PacketError> {
let (header, payload) = self.split_header_and_payload(header_len)?;
if header.is_empty() {
return Err(PacketError::InvalidHeader);
}
// Invert header flags byte in-place
header[0] ^= 0xFF;
// Apply XOR key transformation across payload slice elements
for byte in payload.iter_mut() {
*byte ^= key;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_packet_transformation_success() {
let mut raw_bytes = vec![0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB, 0xCC];
let mut processor = PacketStreamProcessor::new(&mut raw_bytes);
let res = processor.transform_in_place(4, 0x55);
assert!(res.is_ok());
assert_eq!(raw_bytes[0], 0xFE);
assert_eq!(raw_bytes[1], 0x02);
assert_eq!(raw_bytes[4], 0xFF);
assert_ne!(raw_bytes[4], 0xAA);
}
#[test]
fn test_buffer_too_small_error() {
let mut raw_bytes = vec![0x01, 0x02];
let mut processor = PacketStreamProcessor::new(&mut raw_bytes);
let res = processor.split_header_and_payload(4);
assert!(res.is_err());
assert!(matches!(res, Err(PacketError::BufferTooSmall)));
}
}
Technical Explanation
-
Disjoint Mutable Slices & Aliasing XOR Mutability: The Rust borrow checker enforces that no two mutable references (
&mut T) can alias the same memory location simultaneously. Sub-slicing a slice twice via index notation (&mut buf[..4]and&mut buf[4..]) tries to borrowbuftwice in the same scope. The standard library methodslice::split_at_mutinternally uses safe raw pointer offsets (from_raw_parts_mut) to split a single slice pointer into two non-overlapping mutable slices(&mut [u8], &mut [u8]). Because the compiler can prove the memory regions do not overlap, both mutable references remain valid without violating exclusivity invariants. -
Reborrowing Dynamics & Lifetime Elision: Inside
split_header_and_payload, passing&mut selftemporarily reborrows the underlyingself.bufferfor the duration of the method call. The returned slices(&mut [u8], &mut [u8])carry lifetimes tied to the reborrow ofself, ensuring that caller access toprocessoris suspended until the borrowed slices fall out of scope. -
In-Place Memory Layout & Dereferencing: The iterator
payload.iter_mut()yields mutable reference elements (&mut u8). Using the dereference operator*byte ^= keymutates the underlying heap buffer memory directly, executing with zero temporary memory allocations (O(1)space complexity). -
Edge Cases: If
self.buffer.len() < header_len,split_header_and_payloadfails gracefully returningErr(PacketError::BufferTooSmall)before any slice split or unchecked indexing occurs.
Exercise 2: Disjoint Memory Grid Mutation & Simultaneous Index Swapping
Scenario: In spatial partitioning grid algorithms (such as game physics grids or 2D matrix solvers backed by flat 1D vectors), two distinct grid cells must be swapped or mutated concurrently. A simple attempt like let ref_a = &mut grid[i]; let ref_b = &mut grid[j]; causes compiler error E0499 (cannot borrow grid as mutable more than once) because index resolution happens at runtime, while lifetime exclusivity is checked statically.
Task: Implement a safe, production-grade SpatialGridBuffer struct containing a flat Vec<u32> grid representation. Implement get_mut_two(&mut self, idx_a: usize, idx_b: usize) using split_at_mut to obtain simultaneous exclusive &mut u32 references to two distinct elements. Implement swap_cells to perform zero-allocation element swapping via std::mem::swap. Include unit tests verifying valid swaps, out-of-bounds protection, and identical index rejection using explicit assertions (assert!, assert_eq!, assert_ne!, matches!).
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub enum GridError {
OutOfBounds,
IdenticalIndices,
}
pub struct SpatialGridBuffer {
cells: Vec<u32>,
width: usize,
height: usize,
}
impl SpatialGridBuffer {
pub fn new(width: usize, height: usize, default_val: u32) -> Self {
Self {
cells: vec![default_val; width * height],
width,
height,
}
}
pub fn get_mut_two(
&mut self,
idx_a: usize,
idx_b: usize,
) -> Result<(&mut u32, &mut u32), GridError> {
if idx_a >= self.cells.len() || idx_b >= self.cells.len() {
return Err(GridError::OutOfBounds);
}
if idx_a == idx_b {
return Err(GridError::IdenticalIndices);
}
let (first_idx, second_idx, is_swapped) = if idx_a < idx_b {
(idx_a, idx_b, false)
} else {
(idx_b, idx_a, true)
};
// Split at the higher index to create non-overlapping left and right sub-slices
let (left_slice, right_slice) = self.cells.split_at_mut(second_idx);
let ref_first = &mut left_slice[first_idx];
let ref_second = &mut right_slice[0];
if is_swapped {
Ok((ref_second, ref_first))
} else {
Ok((ref_first, ref_second))
}
}
pub fn swap_cells(&mut self, idx_a: usize, idx_b: usize) -> Result<(), GridError> {
let (ref_a, ref_b) = self.get_mut_two(idx_a, idx_b)?;
std::mem::swap(ref_a, ref_b);
Ok(())
}
pub fn get(&self, idx: usize) -> Option<u32> {
self.cells.get(idx).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_disjoint_swap_success() {
let mut grid = SpatialGridBuffer::new(2, 2, 0);
grid.cells[0] = 10;
grid.cells[3] = 99;
let res = grid.swap_cells(0, 3);
assert!(res.is_ok());
assert_eq!(grid.get(0), Some(99));
assert_eq!(grid.get(3), Some(10));
assert_ne!(grid.get(0), grid.get(3));
}
#[test]
fn test_identical_index_rejection() {
let mut grid = SpatialGridBuffer::new(2, 2, 5);
let res = grid.swap_cells(1, 1);
assert!(res.is_err());
assert!(matches!(res, Err(GridError::IdenticalIndices)));
}
#[test]
fn test_out_of_bounds_rejection() {
let mut grid = SpatialGridBuffer::new(2, 2, 5);
let res = grid.swap_cells(0, 10);
assert!(res.is_err());
assert!(matches!(res, Err(GridError::OutOfBounds)));
}
}
Technical Explanation
-
Runtime Index Disjointness vs Static Borrow Rules: The Rust compiler cannot verify whether runtime variable indices
idx_aandidx_brefer to different memory locations. Attempting to index a collection mutably twice within the same scope violates static aliasing rules. Splittingself.cellsatsecond_idxdivides the vector memory intoleft_slice(indices0..second_idx) andright_slice(indicessecond_idx..len). Becausefirst_idx < second_idx,first_idxis safely contained inleft_slicewhilesecond_idxcorresponds to index0ofright_slice. -
Memory Layout & In-Place Bitwise Swap:
std::mem::swaptakes two exclusive mutable references&mut Tand swaps their binary bit patterns directly in memory using temporary register moves without heap allocation. -
Non-Lexical Lifetimes (NLL) & Retaining Original Ordering: The boolean flag
is_swappedensures that returned mutable reference tuples(ref_a, ref_b)correctly correspond to requested parameter order(idx_a, idx_b). -
Edge Cases: When
idx_a == idx_b, returning two mutable references to the exact same memory element would violate Rust's core safety guarantee (aliasing mutability). ReturningErr(GridError::IdenticalIndices)prevents undefined behavior and dynamic aliasing bugs.
Exercise 3: Circular Ring Buffer Batch Reborrowing & Partitioned Sub-slice Mutations
Scenario: Real-time telemetry systems and DSP audio processors operate on contiguous circular ring buffers. Processing modules must overwrite incoming samples, track evicted metrics, and mutably reborrow buffer halves to apply signal gains or vector DSP operations in-place without taking ownership or reallocating memory.
Task: Build a TelemetryRingBuffer<'a> struct wrapping a slice reference &'a mut [f64]. Implement push(&mut self, val: f64) -> f64 to write sample data, update ring head, and return the evicted sample value. Implement split_half_mut to split the borrowed slice into two distinct mutable halves for parallel sub-slice gain adjustment, and apply_gain_in_place(&mut self, gain: f64). Include full unit tests with explicit assertions (assert!, assert_eq!, assert_ne!, matches!).
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub enum RingBufferError {
EmptyBuffer,
InvalidChunkSize,
}
pub struct TelemetryRingBuffer<'a> {
data: &'a mut [f64],
head: usize,
}
impl<'a> TelemetryRingBuffer<'a> {
pub fn new(data: &'a mut [f64]) -> Result<Self, RingBufferError> {
if data.is_empty() {
return Err(RingBufferError::EmptyBuffer);
}
Ok(Self { data, head: 0 })
}
pub fn push(&mut self, val: f64) -> f64 {
let evicted = self.data[self.head];
self.data[self.head] = val;
self.head = (self.head + 1) % self.data.len();
evicted
}
pub fn split_half_mut(
&mut self,
) -> Result<(&mut [f64], &mut [f64]), RingBufferError> {
let len = self.data.len();
if len < 2 {
return Err(RingBufferError::InvalidChunkSize);
}
let mid = len / 2;
Ok(self.data.split_at_mut(mid))
}
pub fn apply_gain_in_place(&mut self, gain: f64) {
for sample in self.data.iter_mut() {
*sample *= gain;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ring_buffer_push_and_eviction() {
let mut storage = vec![1.0, 2.0, 3.0];
let mut ring = TelemetryRingBuffer::new(&mut storage).unwrap();
let evicted1 = ring.push(10.0);
assert_eq!(evicted1, 1.0);
assert_ne!(storage[0], 1.0);
assert_eq!(storage[0], 10.0);
ring.apply_gain_in_place(2.0);
assert_eq!(storage[0], 20.0);
assert_eq!(storage[1], 4.0);
}
#[test]
fn test_split_half_reborrowing() {
let mut storage = vec![10.0, 20.0, 30.0, 40.0];
let mut ring = TelemetryRingBuffer::new(&mut storage).unwrap();
let res = ring.split_half_mut();
assert!(res.is_ok());
let (left, right) = res.unwrap();
left[0] += 5.0;
right[0] += 5.0;
assert_eq!(storage[0], 15.0);
assert_eq!(storage[2], 35.0);
}
#[test]
fn test_empty_buffer_error() {
let mut storage: Vec<f64> = vec![];
let res = TelemetryRingBuffer::new(&mut storage);
assert!(res.is_err());
assert!(matches!(res, Err(RingBufferError::EmptyBuffer)));
}
}
Technical Explanation
-
Mutable Slice Struct Wrappers & Explicit Lifetime
'a:TelemetryRingBuffer<'a>encapsulates a mutable slice borrow&'a mut [f64]. The lifetime parameter'aguarantees thatstoragememory owned by the caller outlives the ring buffer instance. Whileringexists and mutably borrowsstorage, the caller cannot accessstoragedirectly due to mutable borrow exclusivity. -
Reborrowing & Transitive Lifetime Exclusivity: In
split_half_mut(&mut self), callingself.data.split_at_mut(mid)reborrows the inner slice&mut [f64]. The returned pair(&mut [f64], &mut [f64])allows modifying the first and second halves of the buffer independently. Whileleftandrightexist,ringitself is temporarily reborrowed and cannot be used untilleftandrightfall out of scope. -
In-Place Sample Overwrite & Ring Modulo Arithmetic:
pushoverwritesself.data[self.head]in-place without allocation. Ring wrap-around is achieved via(self.head + 1) % len. -
Edge Cases: Passing an empty slice
&mut []causesTelemetryRingBuffer::newto fail immediately withErr(RingBufferError::EmptyBuffer), ensuring all subsequent slice operations like% self.data.len()are protected from divide-by-zero panics.
6. Related Terms
- Borrow Checker — The strict compiler component that enforces the "One Mutable Borrow" rule.
- Interior Mutability — (Future reference) Advanced patterns that bypass these strict compile-time rules using runtime checks instead.
- Borrowing (
&) — Related concept: Borrowing (&). - Partial Moves & Partial Borrows — Related concept: Partial Moves & Partial Borrows.
- Reborrowing & Two-Phase Borrows — Related concept: Reborrowing & Two-Phase Borrows.
- Lifetime Variance — Related concept: Lifetime Variance.
- References and Borrowing (
&,&mut) — Related concept: References and Borrowing (&,&mut).
7. Key Takeaways
- Mutable Borrowing (
&mut) allows a function to temporarily modify data without taking ownership. - The original variable itself must be declared with
mut. - Both the sender and receiver must explicitly use
&mutsyntax. - The Golden Rule: You can have either many immutable borrows (
&) OR exactly one mutable borrow (&mut). Never both at the same time. - This strict exclusivity rule completely eliminates Data Races at compile time.