Step 03 of 17

Ownership

Explain how Rust frees memory without a garbage collector: owners, moves, Copy types, Clone, and Drop at end of scope.

17 min 9 sections

Ownership is the idea that makes Rust Rust. It's how the language frees memory at exactly the right moment, without a garbage collector and without you calling free. Three short rules produce it, and they also explain most of the errors you'll see in your first weeks. This lesson covers the stack and the heap, the rules, what a move is, which types are copied instead, how to duplicate data with clone, and how Drop runs cleanup code deterministically.

The stack and the heap

Your program stores values in two places.

The stack holds values whose size is known at compile time: integers, floats, bool, char, fixed-size arrays, and structs made of them. Pushing and popping is just moving a pointer, so it's extremely fast, and a function's stack frame disappears when it returns.

The heap holds data whose size is only known at runtime or that must outlive the function that created it: a String that grows as you append to it, a Vec with a variable number of items. Allocating means asking the allocator for space and getting back a pointer. Someone has to give that space back exactly once, and never touch it afterwards.

A String is both. The value on the stack is three words: a pointer to a heap buffer, a length and a capacity. The characters live in the heap buffer.

text
 stack (s)                 heap
┌──────────┬───────┐      ┌───┬───┬───┬───┬───┐
│ ptr      │  ─────┼────► │ h │ e │ l │ l │ o │
│ len      │   5   │      └───┴───┴───┴───┴───┘
│ capacity │   5   │
└──────────┴───────┘

Every language has to decide who frees that buffer and when. C makes you do it, and bugs are freeing it twice, freeing it too early (use-after-free), or never (a leak). Garbage-collected languages track reachability at runtime and free it some time later. Rust decides at compile time, using ownership.

The three rules

  1. Each value has exactly one owner: a variable, a struct field, a collection slot.
  2. There can be only one owner at a time.
  3. When the owner goes out of scope, the value is dropped: its destructor runs and its memory is freed.
rust
fn main() {
    {
        let s = String::from("hello"); // s owns the heap buffer
        println!("{s}");
    } // s goes out of scope: the buffer is freed here, automatically
}

That closing brace is where Rust inserts the call to free the buffer. There's no runtime tracking and no pause. It's the same code you'd write by hand in C, except the compiler writes it and can't forget it.

Moves

What happens when you assign a String to another variable?

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s1}"); // error
}
text
error[E0382]: borrow of moved value: `s1`
 --> src/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}");
  |               ^^^^ value borrowed here after move

let s2 = s1 copies the three stack words (pointer, length, capacity) into s2. Now two variables point at the same heap buffer. If both were owners, both would free it at the end of scope: a double free. Rust's answer is that ownership moves to s2, and s1 becomes unusable. The compiler enforces it, so there's no runtime cost: no reference count, no flag.

Moves happen everywhere a value is handed over, not just in let:

rust
fn takes(s: String) {
    println!("got {s}");
} // s is dropped here

fn gives() -> String {
    String::from("yours") // ownership moves out to the caller
}

fn main() {
    let a = String::from("mine");
    takes(a);            // a moves into the function
    // println!("{a}");  // error: a was moved

    let b = gives();     // b now owns the returned String
    println!("{b}");
}

Passing a value to a function moves it. Returning a value moves it out. Pushing it into a Vec moves it into the vector. Once you see the pattern, the errors make sense: you gave it away; you can't use it anymore.

Most of the time you don't want to give a value away, just let a function look at it. That's a reference, and it's the next lesson, references and borrowing.

Copy types

Integers don't behave this way:

rust
fn main() {
    let x = 5;
    let y = x;
    println!("{x} {y}"); // fine
}

i32 implements the Copy trait. A Copy type lives entirely on the stack, so copying its bits produces a fully independent value and there's nothing to double-free. Assigning or passing it copies instead of moving.

Copy types include all integer and float types, bool, char, shared references &T, and tuples and arrays made only of Copy types. String, Vec<T>, Box<T> and anything else that owns heap memory or another resource can never be Copy.

You can make your own small types Copy:

rust
#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let a = Point { x: 1.0, y: 2.0 };
    let b = a;               // copied, not moved
    println!("{a:?} {b:?}");
}

Clone: an explicit deep copy

When you really need two independent owners of heap data, ask for a copy with .clone():

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // allocates a new buffer and copies the bytes
    println!("{s1} {s2}");
}

clone is explicit on purpose: it can be expensive (a Vec of a million items copies a million items), and Rust wants that cost visible in the code. Implicit copies are reserved for Copy types, which are always cheap.

Beginners often scatter .clone() to make the borrow checker go quiet. Sometimes that's fine; cloning a short string at startup costs nothing that matters. But inside a hot loop, or when cloning a large structure just to read it, it's a sign you wanted a reference. Profile before worrying, and prefer borrowing by default.

Drop and RAII

When a value's owner goes out of scope, Rust calls its destructor, defined by the Drop trait. For String and Vec, it frees the heap buffer. For a file, it closes the file descriptor. For a mutex guard, it unlocks the mutex. This pattern, tying a resource's lifetime to an owner's scope, is called RAII (Resource Acquisition Is Initialization), borrowed from C++.

You can implement Drop yourself to see exactly when it runs:

rust
struct Noisy(&'static str);

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

fn main() {
    let _a = Noisy("a");
    {
        let _b = Noisy("b");
        println!("inner scope ends");
    }
    let c = Noisy("c");
    drop(c); // drop early, explicitly
    println!("main ends");
}
text
inner scope ends
drop b
drop c
main ends
drop a

Values are dropped in reverse order of declaration at the end of their scope. std::mem::drop (in the prelude as drop) takes ownership of a value and lets it go immediately, which is useful to release a lock or close a file before the end of a long function.

Because drop is deterministic, you don't need defer (Go), try/finally (Java) or using (C#). Files close, locks release and connections return to their pool when their owner's scope ends, even on early returns and panics.

Ownership and structs

Ownership composes. A struct owns its fields, a Vec owns its elements, and dropping the outer value drops everything inside it, recursively.

rust
struct User {
    name: String,
    tags: Vec<String>,
}

fn main() {
    let u = User { name: String::from("ada"), tags: vec![String::from("admin")] };
    let name = u.name;        // moves just the name field out of u
    println!("{name}");
    println!("{:?}", u.tags); // fine: tags wasn't moved
    // println!("{}", u.name); // error: u.name was moved
}

This is a partial move: you can move individual fields out of a struct you own, and still use the fields you didn't move. You can't use u as a whole afterwards.

Practice

Predict, then check with cargo check.

  1. Write a function fn shout(s: String) -> String that returns s in upper case (s.to_uppercase()). In main, create a String, call shout, and print both the original and the result. Fix the error without using clone by changing what main does with the return value.
  2. Create let v = vec![String::from("a"), String::from("b")]; and loop over it with for s in v { ... }. After the loop, try to print v.len(). Read the error: what did the for loop do to v?
  3. Implement Drop for a struct Guard(String) that prints "releasing <name>". Create three guards in main, drop the second one explicitly halfway through, and predict the output order before running it.
Show solution
  1. Calling shout(s) moves s into the function, so printing s afterwards fails. Since shout already returns a String, take the result back instead of keeping the original:
rust
fn shout(s: String) -> String {
    s.to_uppercase()
}

fn main() {
    let s = String::from("hello");
    let s = shout(s); // move in, get a new String back
    println!("{s}");  // HELLO
}

A version that keeps the original and the result would take &str instead. That's next lesson.

  1. for s in v calls v.into_iter(), which consumes the vector: each String moves out into s. After the loop, v has been moved:
text
error[E0382]: borrow of moved value: `v`
help: consider iterating over a slice of the `Vec<String>`'s content to avoid moving into the `for` loop
  |
  |     for s in &v {
  |              +

for s in &v borrows each element instead.

rust
struct Guard(String);

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

fn main() {
    let _one = Guard("one".into());
    let two = Guard("two".into());
    let _three = Guard("three".into());
    drop(two);
    println!("end of main");
}
text
releasing two
end of main
releasing three
releasing one

two is dropped where you call drop. The rest are dropped in reverse declaration order when main ends.

Note the leading underscore in _one: it tells the compiler the variable is intentionally unused, but the value still lives until the end of scope. A bare _ (as in let _ = Guard(...)) is different: it doesn't bind the value at all, so it's dropped immediately.

Key takeaways

  • Every value has one owner; when the owner goes out of scope, the value is dropped and its memory freed, at compile-time-determined points.
  • Assigning, passing or returning a non-Copy value moves it; the old binding can't be used afterwards.
  • Copy types (integers, floats, bool, char, &T, small plain-data structs) are copied bit-for-bit instead of moved.
  • .clone() is an explicit, possibly expensive, deep copy. Prefer borrowing; clone when you truly need a second owner.
  • Drop runs deterministically at end of scope, in reverse declaration order, which is how files, locks and connections clean themselves up.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.Why does Rust move a String on assignment instead of copying it?

  2. 2.Which of these types is Copy?

  3. 3.fn f(v: Vec<i32>) {} … let v = vec![1, 2]; f(v); println!("{:?}", v); — what happens?

  4. 4.Three values a, b, c are declared in that order in one scope, and none is moved. In which order are they dropped?

Concepts in this lesson

One-page notes on each idea, for review or a quick lookup later.

Finished reading?

Mark it complete to fill in your roadmap.