Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5. Ownership

Every value in a running program occupies memory, and something has to decide when that memory is released. C leaves the decision to you and punishes mistakes with corruption. Python, JavaScript, and Lux hand the decision to a garbage collector, which costs a runtime, some memory, and a pause at an unpredictable moment. Rust decides at compile time, from rules about who owns what, and emits the frees itself. No collector, no malloc and free in your code, and the whole apparatus disappears when the program is built.

That is what you are buying. The price is this chapter. Read it slowly; it is the only part of the book that asks you to.

5.1 Where Values Live

Two places. The stack holds a frame for each function call, and everything in a frame has a size known at compile time and disappears when the function returns. The heap holds anything whose size is decided while the program runs or whose lifetime outlasts the call that made it.

A String is both at once:

#![allow(unused)]
fn main() {
let s = String::from("hello");
}

The variable s is three machine words on the stack: a pointer to the characters, the number of bytes in use, and the number of bytes allocated. The characters themselves are on the heap. You can see the shape:

i32      4
&str     16
String   24
Vec<i32> 24
&String  8

Eight bytes per word on a 64-bit machine, so String and Vec are three words and a plain reference is one. &str is two, a pointer and a length, and is therefore not the same kind of thing as a C pointer at all.

The distinction that matters is not stack against heap but owning against borrowing. A String owns its bytes and will free them. A &str refers to bytes that somebody else owns, and frees nothing.

5.2 Move

Assignment of an owning value transfers ownership:

let s1 = String::from("hello");
let s2 = s1;
println!("{s1} {s2}");

The three words are copied into s2, and s1 is marked dead. The heap bytes are not copied and are not touched. Then the print, which uses s1, does not compile:

error[E0382]: borrow of moved value: `s1`
 --> moved_value.rs:4:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1} {s2}");
  |                ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

This is the error you will meet most often. The message names where the value came from, where it went, and where you tried to use it after.

The reason for the rule is the free. Exactly one variable is responsible for the heap bytes, and that variable frees them when it goes out of scope. If both s1 and s2 were live, both would free, and a double free is one of the classic ways a C program becomes a security advisory.

Passing to a function is the same transfer:

fn consume(s: String) { }

let s = String::from("hello");
consume(s);
println!("{s}");        // error: value moved into consume

Returning transfers it back out. A function that takes a value and hands it back modified is an ordinary shape in Rust and costs nothing at runtime, since only the three words move.

5.3 Copy

Small values behave the way you expect instead:

#![allow(unused)]
fn main() {
let a = 5;
let b = a;
println!("{a} {b}");
}

Types that are nothing but bits, with no heap and no cleanup, are Copy: assignment duplicates them and both remain usable. All the integer and floating types, bool, char, shared references, and tuples and arrays made entirely of Copy types. String, Vec, and anything owning a resource are not, and neither is &mut T, because two usable mutable references to the same thing is the situation these rules exist to prevent.

When you want a duplicate of something that is not Copy, ask:

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1} {s2}");
}

clone really does copy the heap bytes, and it is visible in the source because it is the expensive one. Rust’s position is that a deep copy is fine as long as nobody is surprised by it.

5.4 Drop

When a value goes out of scope, its memory is released. If the type has cleanup to do, the Drop implementation runs first:

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

fn main() {
    let _a = Noisy("a");
    {
        let _b = Noisy("b");
        println!("inner block ends");
    }
    let _c = Noisy("c");
    println!("main ends");
}
inner block ends
dropping b
main ends
dropping c
dropping a

Values are dropped at the end of their scope, in reverse order of declaration. A moved-from value is not dropped, because it is no longer the owner.

This is how files close, locks release, and sockets shut down: not because you remembered, but because the owner went out of scope. There is no finally and no with statement in Rust, and no need for either.

5.5 Borrowing

Handing ownership to every function that wants a look at a value would be tedious. A reference lends it instead:

// Borrowing: the caller keeps the text and gets a slice of it back.
fn first_word(text: &str) -> &str {
    match text.find(' ') {
        Some(i) => &text[..i],
        None => text,
    }
}

fn shout(text: &mut String) {
    text.make_ascii_uppercase();
    text.push('!');
}

fn main() {
    let mut line = String::from("hello there world");
    println!("first word: {}", first_word(&line));

    shout(&mut line);
    println!("{line}");
    println!("still owned here, {} bytes", line.len());
}
first word: hello
HELLO THERE WORLD!
still owned here, 18 bytes

&line is a shared borrow: the function may read and must not modify. &mut line is a mutable borrow: the function may modify, and the variable being lent has to be mut itself. In both cases main still owns the string and is still responsible for freeing it.

Note that no * appears in the bodies. Rust dereferences automatically when you call a method or reach a field, so text.len() works whether text is a String or a &String. You need * to read through a reference to something simple, as in *count += 1 or comparing *value == target.

The rules

At any given moment, for any given value, you may have either any number of shared references or exactly one mutable reference, and never both. A reference may never outlive the thing it points at.

That is the system. Everything the compiler tells you about borrows follows from those two sentences.

The payoff is worth naming, because it goes well beyond memory safety. If you hold a &mut, nothing else in the program can be looking at that value, so nobody can observe it half-modified. If you hold a &, nothing can be changing it underneath you. The iterator-invalidation bug, where a collection is modified while something walks it, becomes a compile error:

error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
 --> borrow_conflict.rs:4:5
  |
3 |     let first = &names[0];
  |                  ----- immutable borrow occurs here
4 |     names.push(String::from("grace"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{first}");
  |                ----- immutable borrow later used here

That program is not merely inelegant. push may reallocate, and first would then point into freed memory. In C++ it compiles and mostly works and occasionally does not. Here it is rejected with the three relevant lines underlined.

The same rules catch data races when threads are involved, so Rust’s concurrency story is short: the compiler already refuses shared mutable access, and threads add no new rule.

Borrows end at their last use

A borrow lasts from where it is taken to where it is last used, not to the end of the block:

fn main() {
    let mut v = vec![1, 2, 3];

    let first = &v[0];
    println!("first is {first}");
    // The borrow above is over: its last use was the line before.

    v.push(4);
    println!("{v:?}");
}

This compiles. Move the println! that uses first below the push and it does not. When an error talks about a borrow “later used here”, that line is what is keeping the borrow alive, and moving it is often the fix.

5.6 Slices

A slice is a borrowed view of part of a sequence: a pointer and a length, no allocation, no copy.

#![allow(unused)]
fn main() {
let text = String::from("hello there world");
let hello = &text[0..5];
let world = &text[12..];
println!("{hello} {world}");

let numbers = [1, 2, 3, 4, 5];
let middle = &numbers[1..4];
println!("{middle:?} sums to {}", middle.iter().sum::<i32>());
}

&str is a slice of a string and &[T] is a slice of an array or vector. Since a slice borrows, the compiler will not let the underlying value be modified or dropped while the slice is alive. That is what makes returning a slice from a function safe.

Write function parameters as slices rather than owned collections wherever you only need to read: &str instead of &String, &[i32] instead of &Vec<i32>. The slice version accepts more kinds of argument and asks for less.

String slice indices are byte positions, and slicing through the middle of a multi-byte character stops the program. Chapter 7 has the details.

5.7 Lifetimes

A reference is only good while the thing it refers to is alive. The compiler tracks that with lifetimes, and most of the time it works them out silently. Occasionally it cannot, and then it asks:

error[E0106]: missing lifetime specifier
 --> missing_lifetime.rs:1:33
  |
1 | fn longest(a: &str, b: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

The function returns a reference into one of its arguments, and the compiler needs to know which so it can check the caller. 'a is a lifetime parameter, a name for “some region of the program”, and the annotated signature says: the result is good for as long as both arguments are. Take the compiler’s suggestion and the function compiles:

#![allow(unused)]
fn main() {
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
}

Lifetime annotations never change what the program does. They are a claim the compiler checks, not an instruction it follows.

You will write few of them, because three elision rules cover ordinary signatures. Each elided input lifetime gets its own. If there is exactly one input lifetime, it is given to every output. And if one of the parameters is &self, its lifetime is given to every output. That last rule is why methods almost never need annotations, and the second is why first_word above did not.

The other common message is a value not living long enough, which means a reference has outlasted its referent. Trying to return a reference to a local:

error[E0106]: missing lifetime specifier
 --> dangling.rs:1:14
  |
1 | fn make() -> &String {
  |              ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: instead, you are more likely to want to return an owned value

The help is right. Return the String, not a reference to it. This is the dangling pointer, and in Rust it is a compile error with a suggestion attached.

One lifetime has a name of its own. 'static means “lives for the whole program”, and every string literal has it, because literals are baked into the executable. That is why a function can return &'static str without borrowing from anything.

5.8 Getting Unstuck

Some practical advice for the first weeks, when the borrow checker feels like an adversary rather than a colleague.

When you cannot see the fix, clone(). A clone is a real cost but it is a correct program, and you can remove it later once you understand why it was needed. Everyone does this and nobody should feel clever about avoiding it on the first draft.

When a function needs two pieces of a structure at once and the compiler objects to the second borrow, take both at the start, or take the whole structure mutably once, or split the work into a function that returns what you need. Fighting for a second borrow usually means the design wants restructuring.

When a loop is modifying the collection it is walking, collect the changes into a separate vector and apply them after the loop. This is better code in any language; Rust just insists on it.

When ownership genuinely needs to be shared, the standard library has Rc for sharing within a thread and Arc across threads, and RefCell for moving the borrow check to runtime. They are the right answer for graphs and for trees with parent pointers, and the wrong answer for the first problem you reach for them on. Use them when you have understood why the plain approach cannot work.

And read the error before changing anything. Rust’s diagnostics name the variable, mark the three relevant lines, and frequently write the correction. People who find Rust painful are usually people who skim.

Exercise 5-1. Write a function that takes a String, appends an exclamation mark, and returns it. Write it again taking &mut String. When is each the better interface?

Exercise 5-2. Write fn longest_word(text: &str) -> &str. Why does this one need no lifetime annotation?

Exercise 5-3. Make the borrow checker produce E0499, two mutable borrows at once, then fix it in two different ways.

Exercise 5-4. Noisy prints when dropped. Add a function that takes a Noisy by value and does nothing with it, call it, and predict from the output where the drop happens.

Exercise 5-5. This does not compile. Explain why in one sentence, then fix it without using clone:

fn main() {
    let mut words = vec![String::from("one"), String::from("two")];
    for w in &words {
        if w.len() == 3 {
            words.push(w.to_uppercase());
        }
    }
}