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.
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?
&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 gets | Cost | What goes wrong |
|---|---|---|
a copy of the value (clone) | an allocation and a copy per call | a hidden cost, and edits to the copy vanish (Lesson 04) |
the address, unchecked (C's T*, C++'s T&) | nothing | dangling, 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 value | the budget of Lesson 00 |
| the address, checked | nothing at run time; a proof at compile time | the 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:
| Signature | The call | What 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 = ¬e; // many readers at once
let r2 = ¬e;
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:
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() | fine | E0502 |
takes an exclusive borrow: &mut a, or a.push(..) | E0502 | E0499 |
assigns to the place: a = … | E0506 | E0506 |
| ends it: moves it, or drops it | E0505 | E0505 |
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 = ¬e;
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 = ¬e;
note = String::from("bob"); // the owner overwrites
println!("{reader}");
}
fn end() {
let note = String::from("ada");
let reader = ¬e;
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}");
}
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.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]
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.
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.&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.
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).clone"&mut to a function uses it up"r can be lent again (§5).&String and String are the same"* 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
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?
&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
- What is the difference between
&Tand&mut T, and why is&mut TnotCopy? (§1–§2 — shared may coexist with any number of readers; exclusive must be the only name, so copying it would make a second writer.) - State the law and name the error codes for how it is broken. (§1, §3 — any number of readers or exactly one name that writes or ends, never both;
E0499two exclusive,E0502shared meets exclusive,E0506assignment,E0505move or drop,E0503plain read of a Copy value.) - Why is pushing to a vector while holding a reference to its first element rejected, and what does C++ do with the same code? (§3, §6 — push may reallocate and leave the reference dangling; C++ accepts it and the program is undefined, Rust refuses at compile time with three labels: loan, act, later use.)
- What is the difference between
let mut xand&mut x? (§4 —mutpermits the owner to write and lend exclusively;&mutis the exclusive borrow itself; each can be missing alone,E0596/E0594.) - If
&mutis notCopy, why can you pass the samer: &mut Vec<i32>to two calls in a row? (§5 — each call receives a reborrow, a nested exclusive loan on*rthat ends with the call.) - What do
&self,&mut selfandselfmean? (§5 — the same three ownership modes as parameters: read, change, consume; the dot borrows or moves for you.) - Does the borrow checker cost anything at run time? (§2 — no: a reference is an address, 8 bytes for a
String; the law is checked at compile time and erased.) - Why does the compiler reject
for s in &v { v.push(*s); }, and what are your options? (§6 — the iterator holds a shared loan for the whole loop and the push is an exclusive borrow inside it; index and copy, iterate a clone, or collect changes and apply them after.)
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).