Rust / the practical course

01 / Foundations

Borrowing and reborrowing

Use shared and exclusive references, and see when a borrow ends.

55 min + practiceRust 1.98.1 · Edition 2024

By Robert DeVore · Download Markdown

Access has a duration

A reference gives access to another value without taking ownership of that value. &T provides shared access; &mut T provides exclusive access subject to reborrowing. The distinction controls who may access the value, not just whether a method writes to it.

While a shared reference remains usable, other code cannot change ordinary data in ways that violate that reference. An exclusive reference prevents conflicting access through other paths. This does not mean only one pointer can exist. Later lessons cover interior mutability and the rules unsafe code must follow.

Run the example

cargo run --locked --example 04_borrowing
fn append(text: &mut String) {
    text.push('!');
}
fn main() {
    let mut text = String::from("ready");
    let view = &text;
    assert_eq!(view, "ready");
    append(&mut text);
    let exclusive = &mut text;
    append(&mut *exclusive);
    assert_eq!(exclusive, "ready!!");
}

View the tested source

The last use of view occurs before append. The compiler therefore permits the mutation without waiting for the closing brace of main. This is called non-lexical lifetime analysis: the compiler follows where you use a borrow, not just where its variable was declared.

The second call passes &mut *exclusive. It temporarily reborrows through the exclusive reference. During that smaller borrow, the original reference cannot be used for conflicting access. After the call, the original reference becomes usable again. Function calls commonly insert reborrows implicitly; writing one explicitly helps you see the relationship.

Why a push can invalidate a reference

A vector or string may need a larger allocation when it grows. A reference into its old allocation could then point at freed storage. But the access rules do more than prevent reallocation: even a mutation that fits in existing capacity may conflict with a live shared borrow. Reserving capacity is not permission to violate a reference's access contract.

The borrow checker reasons about places with varying precision. It can understand disjoint struct fields and many slice splits exposed through safe APIs. For arbitrary indices, it needs to know that they differ. Use a library method that checks this. A borrow-checking error may mean the compiler needs a clearer way to see that the accesses are separate.

Compiler drill

Intentionally fails on Rust 1.98.1. Run rustc --edition=2024 drills/alias.rs from the repository.

fn main() {
    let mut text = String::from("record");
    let shared = &text;
    text.push('!');
    println!("{shared}");
}
Actual compiler diagnostic · Rust 1.98.1
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable
 --> drills/alias.rs:4:5
  |
3 |     let shared = &text;
  |                  ----- immutable borrow occurs here
4 |     text.push('!');
  |     ^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{shared}");
  |                ------ immutable borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

Read the spans in order: creation of the shared borrow, attempted conflicting mutation, and later use keeping the shared borrow live. Moving the print earlier may end the borrow requirement before mutation. Cloning changes ownership and allocation; it is often unnecessary here.

Exercise

Fix the drill by changing operation order. Then take a mutable slice of [1, 2, 3, 4], call split_at_mut(2), and change both halves. Explain why the two returned slices can coexist.

Solution and acceptance check

Print shared before text.push('!'). The earlier print reads record, and the owned string ends as record!. For the slice, bind (left, right) from split_at_mut(2), then update left[0] and right[0]. The function's contract and implementation establish nonoverlapping element ranges; no element has two independent exclusive access paths. The unsafe lab later examines that boundary.

Sources: borrowing, borrow-checking internals, and split_at_mut.

Find a lesson