Ownership & Moves

Rust · Lesson 8 of 31 7 min read Updated

Ownership is the idea that makes Rust different from every language you have used before. It is how Rust frees memory without a garbage collector and without asking you to call free. Once ownership clicks, most compiler errors start to make sense.

In this lesson you will learn the three ownership rules, see what happens in memory when you assign a String to another variable, and find out why some values are copied while others are moved.

The stack and the heap

A running program keeps data in two places:

  • The stack holds values whose size is known at compile time, such as an i32 or a bool. Adding and removing stack values is very fast, and they are removed automatically when a function ends.
  • The heap holds data whose size can change or is only known at run time, such as the text in a String that grows as a user types. The program asks for heap memory, gets back an address (a pointer), and must give the memory back when finished.

Giving heap memory back exactly once, at the right time, is the hard part. Free it too early and other code reads garbage. Free it twice and the program can crash. Never free it and memory leaks. Ownership solves this.

The three ownership rules

  1. Each value in Rust has a variable that is its owner.
  2. A value has exactly one owner at a time.
  3. When the owner goes out of scope, the value is dropped (its memory is freed).

A scope is the region where a variable is valid, usually from its let to the closing } of its block. Rust calls a special cleanup function named drop at that point. To see it happen, the example below uses a small custom type that prints a message when it is dropped (you will learn struct and impl in Structs & Methods; for now just watch the messages):

Rust
struct Parcel {
    label: String,
}

impl Drop for Parcel {
    fn drop(&mut self) {
        println!("Parcel {} dropped", self.label);
    }
}

fn main() {
    let first = Parcel { label: String::from("A-101") };
    {
        let second = Parcel { label: String::from("B-202") };
        println!("Inside the block with {}", second.label);
    }
    let moved = first;
    println!("End of main, holding {}", moved.label);
}
Output
Inside the block with B-202
Parcel B-202 dropped
End of main, holding A-101
Parcel A-101 dropped

second is dropped at the end of the inner block. first was handed to moved, so only moved is dropped at the end of main, and the parcel is freed exactly once.

Moves

A String is made of three parts stored on the stack (a pointer to the text on the heap, a length and a capacity) plus the text itself on the heap:

Text
   stack: city                     heap
+----------+-------+        +---+---+---+---+---+
| pointer  |   ----+------> | K | o | c | h | i |
| length   |   5   |        +---+---+---+---+---+
| capacity |   5   |
+----------+-------+

When you write let destination = city;, Rust copies only the three stack parts. Now two variables would point at the same heap text, and both would try to free it at the end of main: a double free. Rust prevents this by treating the assignment as a move: ownership passes to destination, and city can no longer be used.

Rust
fn main() {
    let city = String::from("Kochi");
    let destination = city;
    println!("Going to {city}");
}
Output
error[E0382]: borrow of moved value: `city`
 --> src/main.rs:4:25
  |
2 |     let city = String::from("Kochi");
  |         ---- move occurs because `city` has type `String`, which does not implement the `Copy` trait
3 |     let destination = city;
  |                       ---- value moved here
4 |     println!("Going to {city}");
  |                         ^^^^ value borrowed here after move

The message tells the whole story: where the move happened, where you used the old name, and why (a String is not Copy). The compiler also suggests city.clone() as one possible fix. A move costs almost nothing: no text is copied, only three numbers.

Copy types and clone

Simple values that live entirely on the stack are cheap to copy, so Rust copies them instead of moving them. These types implement the Copy trait: all integers, floats, bool, char, and tuples or arrays made only of Copy types. Shared references (&T, next lesson) are Copy too.

When you really want a second, independent copy of heap data, ask for it with .clone(). A clone allocates new heap memory and copies the content, so it is slower, and the code shows it clearly.

Rust
fn main() {
    let overs = 20;
    let overs_copy = overs;
    println!("{overs} and {overs_copy}");

    let team = String::from("Lucknow Lions");
    let backup = team.clone();
    println!("{team} and {backup}");

    let scores = [45, 61, 38];
    let scores_copy = scores;
    println!("{scores:?} {scores_copy:?}");
}
Output
20 and 20
Lucknow Lions and Lucknow Lions
[45, 61, 38] [45, 61, 38]

overs and scores are Copy, so both variables stay usable. team is a String, so we cloned it on purpose; without .clone(), team would have been moved.

Ownership and functions

Passing a value to a function works exactly like assignment: a String argument is moved into the function, and a Copy value is copied. Returning a value moves ownership out to the caller.

Rust
fn shout(message: String) -> String {
    message.to_uppercase()
}   // `message` is dropped here

fn count_letters(word: String) -> (String, usize) {
    let n = word.len();
    (word, n)          // give the String back along with the count
}

fn main() {
    let order = String::from("two masala dosa");
    let loud = shout(order);
    println!("{loud}");
    // println!("{order}");   // error: `order` was moved into `shout`

    let name = String::from("Ananya");
    let (name, letters) = count_letters(name);
    println!("{name} has {letters} letters");
}
Output
TWO MASALA DOSA
Ananya has 6 letters

After shout(order), order belongs to the function and is dropped when the function ends; to_uppercase built a brand-new String, which is moved out to loud. count_letters shows how clumsy it would be to hand ownership back every time you only want to look at a value. Rust has a much better tool for that, borrowing, which is the subject of References & Borrowing.

Common mistakes

  • Using a variable after moving it. If you still need the value, pass a reference (&name) instead, or clone it if you truly need a separate copy.
  • Cloning everything to silence errors. .clone() makes the error go away but copies data every time. Ask first whether a borrow would do.
  • Thinking a move copies the data. A move copies only the small stack part; the heap data stays where it is and gets a new owner.
  • Expecting integers to move. Integers, bool, char and f64 are Copy, so they are copied and the original stays usable.

Key points

  • Every value has exactly one owner, and it is dropped when the owner goes out of scope.
  • Assigning or passing a heap-owning value such as String moves it; the old variable can no longer be used.
  • Moves stop double frees and cost almost nothing, because only the pointer, length and capacity are copied.
  • Copy types (integers, floats, bool, char, and tuples or arrays of them) are copied instead of moved.
  • .clone() makes an explicit deep copy; returning a value moves ownership to the caller.

Exercise

Predict which lines compile, then write a version that does compile. Create let a = String::from("Rohtak");, assign let b = a;, and print a. Also create let x = 7; let y = x; and print both. Fix the String part in two different ways: once with clone, once by printing only b.

Show solution
Rust
fn main() {
    // Fix 1: clone, so both variables own their own String
    let a = String::from("Rohtak");
    let b = a.clone();
    println!("{a} {b}");

    // Fix 2: use only the new owner
    let c = String::from("Rohtak");
    let d = c;
    println!("{d}");

    // Integers are Copy, so this was always fine
    let x = 7;
    let y = x;
    println!("{x} {y}");
}
Output
Rohtak Rohtak
Rohtak
7 7

The original println!("{a}") after let b = a; fails with E0382 because the String moved. The integer lines always compile, because i32 is Copy.

Quick Quiz

1. When is a value's memory freed in Rust?

2. After let s2 = s1; where s1 is a String, what is true?

3. Which type is Copy?

4. What happens to a String passed by value into a function that does not return it?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call