Ownership and moves

Ownership is the rule that makes the rest of Rust possible. Every value has exactly one owner, the owner is a binding, and when that binding goes out of scope the value is released. There is no reference count and no garbage collector in the ordinary case: the compiler knows where the owner starts and ends, and it writes the release at that point.

Coming from C++? Ownership and moves puts a std::move and a Rust move next to each other and shows what each compiler does with the value that was left behind.

Three rules are enough to predict what the compiler accepts. Each value has one owner. There can be only one owner at a time. When the owner goes out of scope, the value is dropped. This lesson takes them one at a time, with the error that a broken rule produces.

A value has one owner

let greeting = String::from("hello"); makes the binding greeting the owner of a heap buffer. When the function below takes a String by value, the caller hands the buffer over: the function is the owner for the length of its body, and it releases the buffer when it returns.

The caller cannot use greeting after the call, and the compiler enforces that before the program runs. The size the function returns is the length of the string, not the capacity of the buffer, because len counts the bytes.

fn shout(text: String) -> usize { text.len() } fn main() { let greeting = String::from("hello"); let size = shout(greeting); println!("size: {size}"); }

rustc --edition 2024 take.rs
./take

size: 5

Nothing was copied: the three words of the String (a pointer, a length and a capacity) moved into the parameter, and the buffer stayed where it was. That is why a Rust move is cheap however large the string is, and why the next section calls it a move rather than a copy.

Assignment moves

Assignment is the same operation under another name. After let copy = greeting; the value belongs to copy, and greeting becomes an empty binding that may not be read. The compiler reports the read, not the assignment, because the assignment is legal; the use afterwards is not.

The message is one of the most useful in the language. It names the type, the trait that would have made a copy possible, the line where the move happened and the line where the old binding was used, and it offers clone() as a fix with its cost in the same sentence.

fn main() { let greeting = String::from("hello"); let copy = greeting; println!("{greeting}"); println!("{copy}"); }

rustc --edition 2024 move_err.rs

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

error: aborting due to 1 previous error

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

A clone would compile, and the next lesson is about exactly that choice. The important part here is that the error is at compile time and costs nothing at run time: there is no flag to check and no null pointer to test.

The scope of the owner

A value lives from the line that binds it to the closing brace of its scope. Inner scopes end first, so an inner String is released before the outer one; the lesson on Drop and scope makes that order visible with a destructor.

The program below prints the inner value before its scope ends and the outer value after. Both are owned by their own bindings, and neither borrows from the other, so the order of the two lines is the order of the statements.

fn main() { let outer = String::from("outer"); { let inner = String::from("inner"); println!("{inner}"); } println!("{outer}"); }

rustc --edition 2024 scopes.rs
./scopes

inner
outer

A binding can be shadowed, which starts a new owner while the old one stays alive until the end of the scope; the lesson on variables shows that with a name that changes type.

Returning ownership

A function that builds a value returns its ownership to the caller. The local binding inside the function ends at the closing brace, but the value has already moved into the return slot, so it is not dropped there. This is the ordinary way to build a String in a helper.

The program below does nothing else, which is the point: there is no std::move to write and no pointer to keep track of. The caller owns the result from the moment the call returns.

fn make_greeting() -> String { String::from("Hello, Rust") } fn main() { let greeting = make_greeting(); println!("{greeting}"); }

rustc --edition 2024 returns.rs
./returns

Hello, Rust

A function that returns a reference instead of a value has to say how long the reference lives, which is the subject of lifetimes. Returning the value itself avoids the question entirely.

A move inside a loop

The rule bites hardest in a loop. A value moved in the first pass is gone in the second, and the compiler says so with the same code, E0382, plus the note that the move happened in a previous iteration. The note also suggests the real fix: borrow the value if the function does not need to own it.

This is the first place where a C++ habit fails loudly. Passing a string to a function in a loop is free in C++ because the parameter is a reference, and it is a compile error in Rust until the parameter says &. That is the next part of the course.

fn consume(text: String) -> usize { text.len() } fn main() { let name = String::from("Rust"); let mut total = 0; for _ in 0..3 { total += consume(name); } println!("{total}"); }

rustc --edition 2024 loop_move_err.rs

error[E0382]: use of moved value: `name`
--> loop_move_err.rs:9:26
|
6 | let name = String::from("Rust");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
7 | let mut total = 0;
8 | for _ in 0..3 {
| ------------- inside of this loop
9 | total += consume(name);
| ^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in function `consume` to borrow instead if owning the value isn't necessary
--> loop_move_err.rs:1:18
|
1 | fn consume(text: String) -> usize {
| ------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
9 | total += consume(name.clone());
| ++++++++

error: aborting due to 1 previous error

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

The compiler offers a clone as a way out, and the clone would allocate three times. The better answer is to change the parameter to &String or &str, which the lessons on borrowing and slices teach.

Exercises

  1. Make the first program print the string after the call as well. What has to change, and what does the change cost?
  2. In the scope program, add a third binding in the inner block and predict the output before you run it.
  3. Fix the loop by changing the parameter type instead of cloning. Which two types would work, and which one accepts both a String and a literal?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is the owner of a value in Rust?

    Answer

    The binding that is responsible for releasing it

    The owner is the binding, and the value is released when that binding goes out of scope.

  2. What does let copy = greeting; do when greeting is a String?

    Answer

    It moves the value and leaves the old binding unusable

    A String does not implement Copy, so assignment transfers ownership instead of duplicating it.

  3. What does the compiler do when a moved value is used again?

    Answer

    It reports E0382 and refuses to build

    The borrow checker proves the value is gone and stops the build before anything runs.

  4. When is a value released?

    Answer

    At the end of the scope of its owner

    The compiler places the release at the end of the owner scope, which is why there is no collector.

  5. What does this program print? fn shout(text: String) -> usize { text.len() } fn main() { let greeting = String::from("hello"); let size = shout(greeting); println!("size: {size}"); }

    Answer

    size: 5

    The function takes ownership, returns the length of the five bytes, and the caller prints it.

  6. What does this program print? fn main() { let outer = String::from("outer"); { let inner = String::from("inner"); println!("{inner}"); } println!("{outer}"); }

    Answer

    inner / outer

    The inner binding is printed inside its block, then the outer one after the block ends.

  7. What is the first line of the compiler output for this program? fn main() { let greeting = String::from("hello"); let copy = greeting; println!("{greeting}"); }

    Answer

    error[E0382]: borrow of moved value: greeting

    The assignment moves the value, and the print borrows the old binding after the move.

  8. What is the first line of the compiler output for this program? fn consume(text: String) -> usize { text.len() } fn main() { let name = String::from("Rust"); let mut total = 0; for _ in 0..3 { total += consume(name); } println!("{total}"); }

    Answer

    error[E0382]: use of moved value: name

    The value moves in the first pass, and the error says it was moved in a previous iteration of the loop.

  9. What does this program print? fn make_greeting() -> String { String::from("Hello, Rust") } fn main() { let greeting = make_greeting(); println!("{greeting}"); }

    Answer

    Hello, Rust

    The value moves out of the function into the caller, so the local binding at the end of the function is not dropped.

  10. What does this program print? fn len(text: String) -> usize { text.len() } fn main() { let word = String::from("rust"); println!("{}", len(word)); }

    Answer

    4

    The word has four bytes, and moving the String into the function does not change its length.

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