Ownership · about 45 min

Ownership Rules and Moves

Why this matters

The previous lesson ended with a question: who frees each heap block, and when? Rust's answer is ownership, three short rules enforced by the compiler. They make memory management automatic and free of garbage collection, and they eliminate double frees and use-after-free bugs. They also produce the error every Rust learner meets first and most often, E0382 "use of moved value". After this lesson you will know exactly what that error protects you from.

The three rules

  1. Each value has an owner, a variable (or a field of something, or an element of a collection).
  2. There is exactly one owner at a time.
  3. When the owner goes out of scope, the value is dropped: its resources, such as a heap buffer, are freed.

Rule 3 is the automatic cleanup:

fn main() {
    {
        let s = String::from("temporary");
        println!("{s}");
    }   // s goes out of scope here: its heap buffer is freed
    println!("s is gone");
}
temporary
s is gone

At the closing brace, the compiler inserts a call that frees s's buffer. You never write free, and you cannot forget it. (Lesson 4 looks closely at this "drop".) The interesting consequences come from rule 2.

What assignment does to a String

Consider:

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

What does let s2 = s1; do? There are three possibilities, and it helps to see why Rust picks the one it does.

Option A: copy the handle only (a "shallow copy"). The 24 bytes on the stack are duplicated, so both point at the same heap buffer:

   s1 | ptr  ----------+
      | len 5          |       +---+---+---+---+---+
      | cap 5          +-----> | h | e | l | l | o |
                       |       +---+---+---+---+---+
   s2 | ptr  ----------+
      | len 5
      | cap 5

Now both s1 and s2 would free the buffer when they go out of scope: a double free. And if one were changed (grown, reallocated), the other would point at freed memory.

Option B: copy the heap data too (a "deep copy"). Safe, but potentially very expensive, and hidden in an innocent-looking =. Rust never does this implicitly. (You can ask for it explicitly with clone; next lesson.)

Option C, Rust's choice: copy the handle, then consider s1 invalid. This is a move:

   s1 | (moved: unusable)
      |
      |                        +---+---+---+---+---+
   s2 | ptr  ----------------> | h | e | l | l | o |
      | len 5                  +---+---+---+---+---+
      | cap 5

The 24 bytes are copied (cheap, like any small value), ownership of the buffer transfers to s2, and the compiler forbids any further use of s1. There is always exactly one owner, so the buffer is freed exactly once, when s2 goes out of scope. The move costs nothing at run time beyond copying 24 bytes; the invalidation exists only in the compiler's analysis.

E0382: use of moved value

Try to use s1 after the move:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s1}, world");
    println!("{s2}");
}
error[E0382]: borrow of moved value: `s1`
 --> main.rs:4:15
  |
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}, world");
  |               ^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

Read it carefully, because it tells the whole story:

Notice what this error is preventing. In C, the equivalent code compiles, and if both variables freed the buffer you would have a double free, or if one freed it you would have a use-after-free. Rust turns that into a compile-time error with arrows pointing at the cause.

Moves happen in more places than let

Ownership moves whenever a value is transferred:

The same rule applies everywhere: after the move, the source cannot be used.

fn main() {
    let mut names = Vec::new();
    let n = String::from("Ada");
    names.push(n);
    println!("{:?}", names);
}
["Ada"]

names.push(n) moved the String into the vector, which now owns it. Using n after that line would be E0382.

Moves in loops and branches

The compiler tracks moves along every path the program might take. A move inside a loop is a problem if the loop can come around and use the value again:

fn main() {
    let label = String::from("item");
    let mut all = Vec::new();
    for _ in 0..3 {
        all.push(label);
    }
}
error[E0382]: use of moved value: `label`
 --> main.rs:5:18
  |
2 |     let label = String::from("item");
  |         ----- move occurs because `label` has type `String`, which does not implement the `Copy` trait
3 |     let mut all = Vec::new();
4 |     for _ in 0..3 {
  |     ------------- inside of this loop
5 |         all.push(label);
  |                  ^^^^^ value moved here, in previous iteration of loop
  |
help: consider moving the expression out of the loop so it is only moved once
  |
4 ~     let mut value = all.push(label);
5 ~     for _ in 0..3 {
6 ~         value;
  |
help: consider cloning the value if the performance cost is acceptable
  |
5 |         all.push(label.clone());
  |                       ++++++++

The first pass moves label into the vector; the second pass would use a moved value. The message says "value moved here, in previous iteration of loop". The first help: is a good example of a suggestion to ignore: it would move the whole push out of the loop, which is not what the program means. The real fixes: create a new String inside the loop, or push a clone (the second suggestion).

Moves in branches are tracked too. If a value is moved in only one branch of an if, the compiler treats it as possibly moved afterwards and forbids using it:

fn main() {
    let s = String::from("maybe");
    let keep = std::env::args().count() > 5;
    if keep {
        let owner = s;
        println!("kept {owner}");
    }
    println!("{s}");
}
error[E0382]: borrow of moved value: `s`
 --> main.rs:8:15
  |
2 |     let s = String::from("maybe");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
...
5 |         let owner = s;
  |                     - value moved here
...
8 |     println!("{s}");
  |               ^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
5 |         let owner = s.clone();
  |                      ++++++++

The compiler cannot know whether keep will be true at run time, so it takes the safe view.

Giving a moved-from variable a new value

A moved-from variable is not dead forever. If it is mut, you can assign a fresh value, and it becomes valid again:

fn main() {
    let mut current = String::from("first");
    let old = current;
    current = String::from("second");
    println!("{old} then {current}");
}
first then second

Assigning to a variable that still holds a value drops the old value first, so current = ... on a valid variable frees its previous buffer automatically. No leaks.

Common mistakes

Using a value after passing it on. Typical in "debug print after the fact" code, as in the exercise: move the print before the move, or (after module 5) borrow.

Reaching for clone reflexively. It silences E0382 but copies the heap data, sometimes in a hot loop. Ask first: does the code really need two independent copies? Often reordering or borrowing is right.

Expecting integers to move. let y = x; with an i32 leaves x usable. That is not an exception to ownership but a different category of type, Copy, covered next.

Recap

Going further: run rustc --explain E0382 now and read it again; it should all make sense. Then try moving a value inside a match arm and using it after the match.

Exercises

Exercise 1 · quiz

Who owns it now?

After these lines run, which variable owns the heap buffer holding "data"?

let a = String::from("data");
let b = a;
let c = b;
Answer
  • c

Each let x = y; moves ownership: the 24-byte handle (pointer, length, capacity) is copied to the new variable and the old one becomes unusable. The heap buffer itself never moves or gets copied. After the second move, c is the only owner, and it alone will free the buffer when it goes out of scope. Using a or b afterwards is error E0382.

Exercise 2 · write code

Fix the use after move

This program fails with E0382 (borrow of moved value). Fix it without using clone, by reordering the code so that the original variable is used before it is moved. It must print:

original: report.txt
moved: report.txt

File: main.rs

Starting point

fn main() {
    let name = String::from("report.txt");
    let moved = name;
    println!("original: {name}");
    println!("moved: {moved}");
}

Example

expected output

original: report.txt
moved: report.txt
Hint 1

The error marks the line where the value was moved and the line where it was used afterwards.

Hint 2

Print name before the line let moved = name;.

Reference solution
fn main() {
    let name = String::from("report.txt");
    println!("original: {name}");
    let moved = name;
    println!("moved: {moved}");
}

Exercise 3 · quiz

Which compile?

Which of these main bodies compile? Select all that apply.

Answer
  • let x = 5; let y = x; println!("{x} {y}");

  • let s = String::from("hi"); let t = s; let s = String::from("new"); println!("{s} {t}");

  • let mut s = String::from("hi"); let t = s; s = String::from("new"); println!("{s} {t}");

(a) integers are Copy: assignment duplicates the value and both stay valid (next lesson). (b) is E0382: s was moved into t. (c) shadowing creates a brand-new s that has never been moved. (d) assigning a new value to a moved-from mut variable is allowed; it becomes valid again with the new String. Only reading a moved-from variable before reassigning it is an error.

Exercise 4 · write code

Moving inside a loop

This program reads lines and is meant to keep the longest line seen so far, then print it with its length. It fails to compile with E0382 because a value is moved inside the loop and then used again on the next pass. Fix it so that for the input

red
yellow
blue

it prints

longest: yellow (6)

(If several lines tie, keep the first.) Do not use clone: restructure the code so each line is moved at most once.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut longest = String::new();
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let candidate = line;
        if candidate.len() > longest.len() {
            longest = candidate;
        }
        println!("checked {}", candidate.len());
    }
    println!("longest: {longest} ({})", longest.len());
}

Example

input

red
yellow
blue

expected output

longest: yellow (6)

Example

input

abc
xyz
ab

expected output

longest: abc (3)
Hint 1

The println!("checked ...") line uses candidate after it may have been moved into longest. The expected output does not include those lines at all.

Hint 2

Remove the debugging print (and the needless candidate variable): move line into longest only when it is longer.

Reference solution
use std::io;

fn main() {
    let mut longest = String::new();
    for line in io::stdin().lines() {
        let line = line.unwrap();
        if line.len() > longest.len() {
            longest = line;
        }
    }
    println!("longest: {longest} ({})", longest.len());
}