Shared references
In Rust a reference is a borrow. The owner keeps the value, the borrower gets a way to read it, and the compiler checks that the borrow never outlives what it points at. That last sentence is the whole difference from a C++ reference, which has no such check and no such promise.
Coming from C++? References and borrowing writes the same two-parameter function in both languages and then returns a reference to a local, which one compiler warns about and the other refuses outright.
This lesson covers shared references only: &T may be read, not written, and any number of them may exist at once. The mutable kind, &mut T, has the opposite rule and gets the next lesson.
A borrow, not an owner
&greeting creates a reference with the type &String. The function below takes that reference, reads its length and returns a number; the caller still owns the string and can print it afterwards. Nothing was moved and nothing was copied.
The reference is valid only while the owner is alive and not moved. The compiler tracks that with the same analysis that tracks ownership, and it refuses a program where the borrow could outlive the value.
fn length(text: &String) -> usize {
text.len()
}
fn main() {
let greeting = String::from("hello");
let size = length(&greeting);
println!("{greeting} has {size} bytes");
}
rustc --edition 2024 borrow_param.rs
./borrow_param
hello has 5 bytes
A function that takes &String cannot store the reference anywhere that outlives the call unless its signature says so, which is what lifetimes are for. For a read inside the call, no annotation is needed.
References as parameters
Any number of shared references to one value may exist at the same time. The program below creates two, passes both to a function, and prints the owner afterwards. There is no conflict because nobody is writing.
This is what makes a read-only API pleasant in Rust: callers pass references, the function cannot change anything, and the compiler proves it. A function that takes &String and tries to mutate would fail to compile inside the function, not at every call site.
fn starts_with_hello(text: &String) -> bool {
text.starts_with("hello")
}
fn main() {
let greeting = String::from("hello there");
let a = &greeting;
let b = &greeting;
println!("{} {}", starts_with_hello(a), starts_with_hello(b));
println!("{greeting}");
}
rustc --edition 2024 two_borrows.rs
./two_borrows
true true
hello there
The two calls return the same answer because they read the same value. If the value changed between them, the second call would see the change, which is the behaviour a shared borrow promises.
Deref and auto-ref
*reference follows the reference and gives the value. Most of the time you do not write it: a method call on a reference finds the method of the value, and a comparison between a reference and a value works through the same deref. The program below uses both forms.
reference == &7 compares two references, which is the same as comparing the values they point at. That is why the second field prints true without an explicit dereference.
fn main() {
let value = 7;
let reference = &value;
println!("{} {}", *reference, reference == &7);
}
rustc --edition 2024 deref.rs
./deref
7 true
The automatic part is called auto-deref, and it is why text.len() works on a &String as well as on a String. The lesson on methods uses the same rule for &self.
What a borrow promises
A shared borrow promises that the value will not be moved or mutated while the borrow is alive. In exchange the borrower may read it as often as it likes, and several borrowers may read at once. The compiler checks the promise at compile time and inserts no run-time check.
The program below compares two strings by length through references and then prints both owners. The borrow ends at the last use, so the owners are free again by the time they are printed.
fn longer(a: &String, b: &String) -> bool {
a.len() > b.len()
}
fn main() {
let a = String::from("longer");
let b = String::from("short");
println!("{}", longer(&a, &b));
println!("{a} {b}");
}
rustc --edition 2024 longer.rs
./longer
true
longer short
There is no null reference in Rust: a reference is always valid for the length of its borrow, so a function that takes &String cannot be handed a null pointer. A value that may be absent is an Option, which the course covers in Option and its combinators.
Moving a borrowed value
The promise has a second half: while a borrow is alive, the owner may not be moved either, because the reference would point at a value that no longer exists where it did. The compiler reports this as E0505, and the message shows the borrow, the move and the later use of the reference.
The program below takes a reference, then drops the owner, then prints through the reference. Each line of the message is a line of the program, which makes this one of the easiest borrow errors to read.
fn main() {
let text = String::from("hello");
let reference = &text;
drop(text);
println!("{reference}");
}
rustc --edition 2024 move_borrowed_err.rs
error[E0505]: cannot move out of `text` because it is borrowed
--> move_borrowed_err.rs:4:10
|
2 | let text = String::from("hello");
| ---- binding `text` declared here
3 | let reference = &text;
| ----- borrow of `text` occurs here
4 | drop(text);
| ^^^^ move out of `text` occurs here
5 | println!("{reference}");
| --------- borrow later used here
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let reference = &text.clone();
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0505`.
The fix is to end the borrow before the move, by using the reference first and dropping it after, or to clone when the value really has to live in two places. The next lesson shows how the borrow ends at its last use, which is what makes that reordering work.
Exercises
- Change length to take &str instead of &String and call it with both a string and a literal. Does either call need a change?
- Add a third shared borrow to the second program and print through all three. What does the compiler have to prove?
- Move the println! that uses the reference before the drop in the last program. Does it compile, and why?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a shared reference in Rust?
Answer
A borrow that allows reading and forbids writing
A shared borrow reads the value while the owner keeps it, and it is checked at compile time.
-
How many shared references to one value may exist at the same time?
Answer
Any number
Reading does not conflict with reading, so the compiler allows any number of shared borrows.
-
What does a shared borrow prevent while it is alive?
Answer
Moving or mutating the owner
The borrow must stay valid, so the owner may not be moved or changed until the borrow ends.
-
What does
*referencedo?Answer
It follows the reference and gives the value
Deref reads the value behind the reference, and method calls apply it automatically.
-
What does this program print?
fn length(text: &String) -> usize { text.len() } fn main() { let greeting = String::from("hello"); let size = length(&greeting); println!("{greeting} has {size} bytes"); }Answer
hello has 5 bytes
The borrow ends with the call, and the owner prints its five bytes afterwards.
-
What does this program print?
fn starts_with_hello(text: &String) -> bool { text.starts_with("hello") } fn main() { let greeting = String::from("hello there"); let a = &greeting; let b = &greeting; println!("{} {}", starts_with_hello(a), starts_with_hello(b)); }Answer
true true
Both references read the same string, so both calls return true.
-
What does this program print?
fn main() { let value = 7; let reference = &value; println!("{} {}", *reference, reference == &7); }Answer
7 true
The deref prints the number, and comparing two references to seven is true.
-
What does this program print?
fn longer(a: &String, b: &String) -> bool { a.len() > b.len() } fn main() { let a = String::from("longer"); let b = String::from("short"); println!("{}", longer(&a, &b)); println!("{a} {b}"); }Answer
true / longer short
The comparison reads both strings through references, and both owners print afterwards.
-
What is the first line of the compiler output for this program?
fn main() { let text = String::from("hello"); let reference = &text; drop(text); println!("{reference}"); }Answer
error[E0505]: cannot move out of
textbecause it is borrowedThe move happens while a borrow is alive, and the message names the borrow, the move and the later use.
-
What does this program print?
fn first_len(text: &String) -> usize { text.len() - 4 } fn main() { let word = String::from("rustacean"); let size = first_len(&word); println!("{word} {size}"); }Answer
rustacean 5
The string has nine bytes, five are left after subtracting four, and the owner still prints.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev