all_lessons/Rust/05 · Borrowing — the lawlesson 6 / 23

Borrowing — the law

Lesson 04 left ownership airtight and awkward: a function that only wants to read a value must take it and hand it back. The missing tool is a second kind of name, one that reaches a place without being responsible for ending it — a borrow. Unchecked, that is exactly the hazard of Lesson 01: a name made earlier and used later while another name writes or ends the place. This lesson lets the compiler enforce the law on borrows and finds that two kinds are enough: shared borrows, which may pile up, and an exclusive borrow, which may not. The result is one law, a handful of error codes for the ways to break it, and one old bug — a container changed under the loop that walks it — that no longer compiles. What the law cannot yet say is how long a borrow lasts.

The thesis, here
A borrow costs nothing to make — it is an address — so the whole price is the proof. The compiler keeps a record of every borrow, a loan of a place, and applies the law to the loans: while a loan is still needed, no other name may do to the place anything the loan's kind excludes.
Linear position
Forced by: Assignment moves ownership, which makes ownership safe but awkward: a function that only wants to read a value must take it and hand it back. How can code use a value it does not own, and stay safe while the owner is still alive and free to change it?
New idea: a borrow is a name that reaches a place without owning it, and it comes in exactly two kinds — shared (&T, any number at once) and exclusive (&mut T, alone) — with the law enforced on the loans. Nothing else is needed to keep an owner and its borrowers safe together.
Forces next: The law says many readers or one writer, never both — but it constrains borrows that overlap in time, and we have not said how long a borrow lasts. Nothing yet stops a reference from outliving the thing it points to. How long may a borrow live?
The plan
Seven moves. (1) Say what a name that does not own must promise, and find that the hazard rule leaves room for exactly two kinds. (2) Give them syntax and price them: an address, nothing more, and signatures that say what a call may do. (3) Apply the law to every way a second name can act on a borrowed place: one table, a few codes. (4) Separate the two things called mutable. (5) Follow &mut through calls: reborrowing, and the three kinds of method receiver. (6) Watch the law stop the bug it was written for, iterator invalidation. (7) Find the question it cannot answer alone.

1 · What a name that does not own has to promise

Lesson 04 ended on measure, a function that wants to read the caller's string and return a number, and must therefore take the string and give it back. What measure needs is a name for the caller's place that is not responsible for ending the value, while the caller keeps its value, unchanged, afterwards. There are four ways to hand a function such a name:

What the callee getsCostWhat goes wrong
a copy of the value (clone)an allocation and a copy per calla hidden cost, and edits to the copy vanish (Lesson 04)
the address, unchecked (C's T*, C++'s T&)nothingdangling, invalidation, races: the hazard of Lesson 01, left to the programmer
a co-owner (shared_ptr, a garbage collector)a counter or a collector on every valuethe budget of Lesson 00
the address, checkednothing at run time; a proof at compile timethe checker must know what everyone may do meanwhile

Only the last row is inside the budget without giving up the promise, so it is the design: a borrow is an address the compiler has proved safe to use. What does the proof need to know? The hazard rule says the damage comes from an effect on a place through one name while another name, made earlier and used later, can still reach it. So try every pair of names that can reach one place at the same time. Two readers: harmless, since reading changes nothing. A reader and a writer: the reader can see a half-finished write, or a buffer that the write just moved. Two writers: an update is lost, or two endings free the same memory. The names that may share a place are therefore any number of readers, or exactly one name that writes or ends. That is the law, and it dictates the design of the borrow itself: a borrow must say, when it is made, which of the two it is. A shared borrow is a name that promises only to read, so others may hold one too. An exclusive borrow asks to be the only name, and so may write. There is no third kind: a writer that tolerates readers breaks the first case, and a reader that may also write is a writer.

The owner is a name like any other. Its own reads, writes and endings are effects, and while a borrow is outstanding they are constrained exactly as anyone's are. That is the price of the baton's condition, "the owner is still alive and free to change it": free, but not while someone is looking.

2 · Two kinds of borrow

A shared borrow of a is written &a and has type &String; an exclusive one is &mut a, of type &mut String. The expression *r names the place a reference points to. Here is Lesson 04's measure without the give-back:

fn measure(text: &String) -> usize {
    text.len()                       // reads through the loan; owns nothing
}

fn main() {
    let a = String::from("ada");
    let n = measure(&a);             // lend a, keep it
    println!("{a} {n}");
}
ada 3

Nothing was moved, and measure drops nothing at its brace, because it owns nothing. Set the three signatures a caller can meet side by side and they answer the first two of the five questions without opening the body:

SignatureThe callWhat the caller learns
fn f(x: T)f(a)a is gone afterwards (moved, Lesson 04)
fn f(x: &T)f(&a)the call cannot change or end a
fn f(x: &mut T)f(&mut a)the call may change a, and cannot end it

Both kinds cost an address and nothing else. The size test confirms that a reference to a String is 8 bytes whichever kind it is (a &str also carries a length, Lesson 02), and there is no counter, flag or check behind it: the law is a property of the program text, and it is gone by the time the program runs.

use std::mem::size_of;

fn main() {
    println!("{} {} {}", size_of::<&String>(), size_of::<&mut String>(), size_of::<&str>());
}
8 8 16
fn main() {
    let mut note = String::from("ada");
    let r1 = &note;                  // many readers at once
    let r2 = &note;
    println!("{r1} {r2} {note}");    // and the owner may read too
    let w = &mut note;               // one writer, alone
    w.push('!');
    println!("{note}");
}
ada ada ada
ada!

Lesson 04's rule about copying carries over without change. A shared reference is Copy: copying it makes another reader, which the law allows. An exclusive reference is not, since a copy would be a second writer, so assigning one is a move, and the old name is dead:

fn main() {
    let mut note = String::from("ada");
    let w = &mut note;
    let w2 = w;                      // a &mut is not Copy: this moves the writer
    w2.push('!');
    println!("{w}");
}

3 · The law, applied: how it is broken

Every rejection the law causes has the same shape. Start with the example of Lesson 00, in a form that is easy to picture: a reader of the first message in an inbox, and a push that may need a new buffer.

fn main() {
    let mut inbox = vec![String::from("hello")];
    let first = &inbox[0];
    inbox.push(String::from("again"));
    println!("{first}");
}

If the compiler accepted this, push would find the buffer full, allocate a bigger one, move the strings across and free the old buffer, while first still held the old address: a use after free, in a program with no unsafe. The compiler says no:

Reading the error
error[E0502]: cannot borrow `inbox` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let first = &inbox[0];
  |                  ----- immutable borrow occurs here
4 |     inbox.push(String::from("again"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{first}");
  |                ----- immutable borrow later used here
Read the three labels in order. The first says where the loan was made, and of what kind. The second, marked with carets, is the act that the loan's kind excludes; push is a borrow in disguise, of inbox, exclusive. The third is the later use that keeps the loan needed, and therefore the reason the act is a conflict: delete the println! and the program compiles. Every borrow error in the next three lessons has this anatomy. The message cannot know what you meant, though. Finishing with first before the push, cloning the message, or restructuring so that no reference is needed are all legitimate repairs, and only the third label points at what to look at.

The acts that conflict with a loan are the effects of Lesson 01. A second name can read the place, write it, or end it, and the compiler's codes name how it does so:

Another name, while the loan is still needed…loan is shared (&a)loan is exclusive (&mut a)
takes a shared borrow: &a, or a.len()fineE0502
takes an exclusive borrow: &mut a, or a.push(..)E0502E0499
assigns to the place: a = …E0506E0506
ends it: moves it, or drops itE0505E0505

Only the top-left cell is a permitted overlap: reader with reader. The codes name the act, not the loan: E0502 covers both orders of a shared borrow meeting an exclusive one, E0499 is two writers, E0506 the owner overwriting, E0505 the owner ending the value. The methods in the first two rows are worth noticing: a.len() is a shared borrow of a for the length of the call, and a.push(..) is an exclusive one. Each cell has a program:

fn reader_then_writer() {
    let mut note = String::from("ada");
    let reader = &note;
    note.push('!');                  // a write while a reader is still needed
    println!("{reader}");
}

fn writer_then_reader() {
    let mut note = String::from("ada");
    let writer = &mut note;
    let n = note.len();              // a read while the writer is still needed
    writer.push('!');
}

fn two_writers() {
    let mut note = String::from("ada");
    let first = &mut note;
    let second = &mut note;          // two writers
    first.push('!');
}
fn overwrite() {
    let mut note = String::from("ada");
    let reader = &note;
    note = String::from("bob");      // the owner overwrites
    println!("{reader}");
}

fn end() {
    let note = String::from("ada");
    let reader = &note;
    drop(note);                      // the owner ends the value
    println!("{reader}");
}

One more code completes the picture. Reading a Copy value is not a borrow at all, and while an exclusive loan is needed it is still an act the loan excludes (E0503); while shared loans are around it is fine, for the same reason as the top-left cell:

fn exclusive() {
    let mut hits = 10;
    let counter = &mut hits;
    let copy = hits;                 // a plain read of a Copy value
    *counter += 1;
    println!("{copy}");
}

fn shared() {
    let hits = 10;
    let reader = &hits;
    let copy = hits;                 // fine: readers and reads coexist
    println!("{reader} {copy}");
}
Road not taken · check the law while the program runs
A counter per place, incremented by each borrow and tested by each write, would let the compiler accept anything and panic on the first conflict. That road exists in Rust, as RefCell (Lesson 15), and it costs a counter per value and moves the failure from compile time to run time. It is a tool for cases the compiler cannot prove, not the default: the promise is that a program which compiles cannot make this mistake at all.
The law machine
The program has one String, note, and a borrow a of it made on line 3. Choose the kind of a, choose what another name does to note next, and slide to decide how long a is needed: never, only before that act, or after it. The bars are loans; a red cross is a conflict. The table underneath runs the checker on all twelve programs with a needed after the act (the same checker that tools/rust_verify/borrowck_fuzz.js compares with rustc on random programs); the outlined cell is your choice.
borrows in the program
—
longest borrow
—
conflicts found
—
would rustc accept it?
—
Edit the program
Show the core JS
/* the law at one point: does this access conflict with this loan, which is in scope here?  → error code, or null.
   (`S` = the owner ends while the loan is live; the caller turns it into E0597 / E0716 / E0515.) */
function accessVerdict(F, a, L, nid) {
  var mutNow = L.mut && (!L.twoPhase || F.activeAt[L.id][nid]);
  switch (a.kind) {
    case 'copy': return mutNow ? 'E0503' : null;
    case 'sborrow': return mutNow ? 'E0502' : null;
    case 'reserve': return L.mut ? 'E0499' : null;                          // a reservation may coexist with shared loans, never with an exclusive one
    case 'mborrow': case 'activate': return L.mut ? 'E0499' : 'E0502';
    case 'write': case 'replace': return 'E0506';
    case 'move': return 'E0505';
    case 'drop': case 'sdead': return 'S';
  }
  return null;
}

What to try. The opening state is §3's flagship: a shared, another name calls note.push(..), a needed after the act. The bar for a runs from line 3 to line 5, the push adds its own one-line exclusive loan on line 4, a red cross sits where they meet, and the verdict is E0502, with the loan made on line 3 and needed again on line 5. Drag the slider left. With a needed only before the act its bar stops on line 4 and the same act is accepted; at never the bar is a single line. The verdict flips at exactly the point where the bar reaches the act, which is all that "still needed" has meant so far. Now set a is to exclusive and the act to note.len(): E0502, because a read is a shared borrow in disguise, whereas with a shared the same act is accepted, the one overlap the law permits. The acts assigns and drops give E0506 and E0505 under either kind. Finally open Edit the program, replace the source with §6's loop and press run edited source: the bar covers the loop's three lines and the conflict lands on the push.

4 · Two things called mutable

The law explains a distinction that trips up almost everyone who arrives with C++'s const in mind, because Rust spells two different ideas differently. mut on a binding (let mut n) says the owner may write to this place and may take an exclusive borrow of it. &mut T is a borrow: the holder is the only name that can reach the place, and so may write through it. Each can be missing on its own, and the compiler reports which:

struct Point { x: i32, y: i32 }

fn reset(p: &Point) {
    p.x = 0;                         // through a shared borrow
}

fn grow(v: &Vec<i32>) {
    v.push(1);                       // through a shared borrow
}

fn main() {
    let n = 5;
    let r = &mut n;                  // n was never declared mut
}

The first two are the second idea missing: a shared reference may have company, so writing through it is refused, as an assignment (E0594) or as taking an exclusive borrow (E0596). The third is the first idea missing: the owner never said n could be written. The repair for the first two is to say what the functions do in their signatures, &mut Point and &mut Vec<i32>; the repair for the third is let mut n. The reverse also holds, and surprises: a reference needs no mut of its own to write through it, because the binding is not what is being changed.

struct Point { x: i32, y: i32 }

fn reset(p: &mut Point) { p.x = 0; }
fn grow(v: &mut Vec<i32>) { v.push(1); }

fn main() {
    let mut n = 5;
    let r = &mut n;                  // r is not declared mut...
    *r += 1;                         // ...and still writes through it
    let mut p = Point { x: 3, y: 4 };
    reset(&mut p);
    let mut v = vec![0];
    grow(&mut v);
    println!("{n} {} {} {:?}", p.x, p.y, v);
}
6 0 4 [0, 1]
Shared does not literally mean read-only
The types Cell, RefCell and Mutex allow writes through a shared reference by keeping the law some other way: Cell never hands out a reference to its contents, RefCell counts its borrowers at run time, Mutex takes a lock (Lessons 15 and 17). This track says shared and exclusive for that reason. The error messages say "immutable" and "mutable" because that is what the common case looks like.

5 · Lending on: reborrows and receivers

An exclusive reference is not Copy, so passing one to a function ought to move it, and the function would use it up. It does not:

fn push_one(v: &mut Vec<i32>) {
    v.push(1);
}

fn main() {
    let mut list = vec![0];
    let r = &mut list;
    push_one(r);                     // lends r for the call: a reborrow
    push_one(r);                     // so r is still there to lend again
    r.push(3);
    println!("{list:?}");
}
[0, 1, 1, 3]

What the call receives is not r but a reborrow: a fresh exclusive loan on the place *r, nested inside r's own, that lasts as long as the callee needs it. The law applies to it like any loan, which is why the writer keeps its exclusivity down a chain of calls, and also why a reborrow that is still needed blocks its parent:

fn main() {
    let mut list = vec![0];
    let r = &mut list;
    let inner = &mut *r;             // a reborrow: a new loan on *r
    r.push(2);                       // r is used while inner is still needed
    inner.push(1);
}

Methods make the three signatures of §2 part of the call syntax. An impl block attaches functions to a type, and a function whose first parameter is self is called with a dot. That first parameter is one of the three ownership modes, written &self, &mut self or self:

struct Counter { hits: u32 }

impl Counter {
    fn get(&self) -> u32 { self.hits }         // reads
    fn bump(&mut self) { self.hits += 1; }     // changes
    fn finish(self) -> u32 { self.hits }       // consumes
}

fn main() {
    let mut c = Counter { hits: 0 };
    c.bump();
    c.bump();
    println!("{}", c.get());
    println!("{}", c.finish());
}
2
2
struct Counter { hits: u32 }

impl Counter {
    fn get(&self) -> u32 { self.hits }
    fn bump(&mut self) { self.hits += 1; }
    fn finish(self) -> u32 { self.hits }
}

fn main() {
    let c = Counter { hits: 0 };
    c.bump();                        // c was not declared mut
    let total = c.finish();
    println!("{}", c.get());         // c was consumed
}

The dot does the borrowing for you: c.bump() takes &mut c, which needs the binding to be mut (the first error), and c.finish() moves c (the second). Lesson 11 writes receivers into trait contracts; what matters here is that a receiver is not a new mechanism but the same three modes, chosen by the author of the method and visible to every caller.

Sidebar · auto-deref
In r.len() with r: &String, the compiler inserts the * for you when it looks up the method. That is a rewrite of syntax; the loan and the law are unchanged.
Sidebar · deref coercion
A &String is accepted where a &str is expected, because String says how to look through itself (the Deref trait, Lesson 15). It can weaken what a reference reaches, never strengthen it: nothing coerces a &T into a &mut T.
fn shout(text: &str) -> String {
    text.to_uppercase()
}

fn main() {
    let owned = String::from("ada");
    let r = &owned;                                   // &String
    println!("{} {}", r.len(), shout(r));             // r.len(): auto-deref;  shout(r): &String becomes &str
}
3 ADA

6 · The bug the law was written for

The most common way to break the law without noticing is to change a collection while a loop walks it. In C++ this compiles, and, as the C++ track's Lesson 13 explains, a push_back that reallocates invalidates every iterator into the vector, so using one afterwards is undefined behavior:

std::vector<int> scores = {3, 5};
for (int s : scores) {            // the loop's iterator points into scores' buffer
    scores.push_back(s);          // may reallocate: the iterator now points into freed memory
}                                 // -Wall -Wextra: no warning; -fsanitize=address: heap-use-after-free
std::cout << scores.size();       // without the sanitizer: prints 4 and looks fine

The comments record what Apple clang with libc++ did on the machine that built this lesson; the program looks fine without the sanitizer, which is the danger. The same program in Rust is the law applied to a name we do not usually see. A for loop over &scores creates an iterator, which is a shared borrow of the vector that lives for the whole loop, and the body's push is an exclusive borrow inside it:

fn main() {
    let mut scores = vec![3, 5];
    for s in &scores {               // the loop holds a shared loan on scores
        scores.push(*s);             // ...for as long as it runs
    }
}

The error names the two lines and the loop header as the later use, since the iterator is used again on the next turn. The family is the same one every time a container is changed under a walker: inserting into a map being iterated, removing from a vector being scanned. The repairs each break one of the three labels of §3. Index instead of iterating, reading a Copy value and letting go of the borrow before each write, breaks the loan; iterating a clone breaks the shared place; collecting the changes and applying them after the loop breaks the overlap in time.

fn main() {
    let mut scores = vec![3, 5];
    let n = scores.len();
    for i in 0..n {
        let s = scores[i];           // a Copy value, read and let go
        scores.push(s);
    }
    println!("{scores:?}");
}
[3, 5, 3, 5]

7 · The question the law cannot answer

Every example above leaned on a phrase the lesson never defined: while a loan is still needed. In each case the loan ended somewhere convenient, at the last println! that mentioned it, and moving that use up or down flipped the verdict; the widget's slider does the same. So "overlap in time" needs a definition of the time a borrow occupies, and the law cannot supply one, because the law is about names acting on places at moments and says nothing about how long a name matters. The one program in this lesson that shows the gap is the one where the owner is not another name at all, but a scope:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }                                // x ends here...
    println!("{r}");                 // ...and r is needed after that
}

The compiler rejects it, with a message that speaks of a value that "does not live long enough". The scope's closing brace is the owner ending x (Lesson 03), and the borrow is still needed on the next line: the law is broken. But that only says which case this is. It does not say how the compiler knew that r is needed on line 7, and the same question arises with more force where no scope helps. What if the reference is returned from a function, stored in a struct, or handed to another thread? The caller of a function that returns a reference must know how long that reference is good, and the signature is the only place that can say so.

What this lesson did not do
It did not define when a loan ends, and it did not say what happens when a reference travels: into a struct, out of a function, past the scope of what it points to. Every "still needed" above was read off the last use in the program text. Making that precise is Lesson 06's work, making it flexible enough to accept programs that are safe but look conflicting is Lesson 07's, and making it travel through function signatures is Lesson 08's.

Common mistakes / failure modes

"&mut means mutable and & means immutable"
&mut means exclusive: the only name that reaches the place. & means shared, and shared can still change a value that is built for it (Cell, Mutex; §4).
"mut on the variable is the same as &mut"
mut lets the owner write and lend exclusively; &mut is the lending. A &mut reference needs no mut binding to write through it (§4).
"once I borrow a value I can't touch it in this function"
Only while the loan is still needed: the law bites at the overlap, and moving the last use up or down changes the verdict (§3, the widget's slider).
"the borrow checker complains, so I should clone"
A clone breaks the sharing and may be the right repair, but the three labels also offer ending the loan earlier or restructuring so no reference is needed (§3, §6).
"passing my &mut to a function uses it up"
A call passes a reborrow, a nested loan that ends with the call; r can be lent again (§5).
"auto-deref makes references transparent, so &String and String are the same"
The compiler inserts * and & where a method lookup needs them; the ownership and the law are unchanged, and no coercion turns &T into &mut T (§5).

Checkpoint exercise

Try it
Before touching the widget, fill the twelve cells of §3's table for a borrow that is needed after the act, with the six acts of the widget's second selector. Then set the widget to needed: after the act and read the table. How many cells are accepted, and which? Then take one rejected cell and move the slider to before the act: what changes about the bar, and why does the verdict change with it? Answer: two cells are accepted, both in the shared row: a new shared borrow and note.len(). The ten rejections use four codes: E0502 four times (a shared loan meeting &mut or push, an exclusive loan meeting & or len), E0499 twice (exclusive meeting exclusive), and E0506 and E0505 twice each, because an assignment and a drop are refused under either kind of loan. Moving the slider to before the act shortens the bar so that it ends on the line before the act; the conflict count drops to 0 and the readout says Accepted, because the loan is no longer needed when the act happens. (The table does not move: it always shows the after case.) The verdict depends on the overlap, not on the two statements alone.

Where this points next

The law is enforced: many readers or one writer, never both, on every place, for as long as a loan is needed. That makes the owner and its borrowers safe together, at no cost at run time. But every "as long as" in this lesson was read off the program by eye, and the law says nothing about a borrow that outlives its referent or travels past the function that made it. To say how long a borrow may live, we need a name for the extent of a loan and a rule that ties it to what the loan points at. How long may a borrow live?

Takeaway
A borrow is a checked address: a name that reaches a place without owning it, at no run-time cost (8 bytes here). The hazard rule leaves room for exactly two kinds: shared (&T, Copy, many at once) and exclusive (&mut T, not Copy, alone). The compiler records each as a loan and enforces the law: while a loan is still needed, no other name — the owner included — may do to the place what its kind excludes: E0502/E0499 for a second borrow, E0506 for an assignment, E0505 for a move or drop, E0503 for a plain read. mut on a binding and &mut are different things (E0596, E0594); a call passes a reborrow, not the reference; receivers are the three ownership modes. Iterator invalidation is this law applied to a loop. What is left open is how long a loan is needed.

Interview prompts

Companion reads: C++ · 03 Pointers and references (the unchecked row of §1), C++ · 13 STL containers (iterator invalidation), C++ · 19 Undefined behavior, and Go · 11 Sharing memory (the same hazard, resolved by convention and a race detector).