Ownership is the most important idea in Rust. It’s the main thing that makes Rust different from Python, and almost every other note in this guide builds on it: &self, moves, Copy, .clone().

If a Rust compiler error has ever said “value used after move” or “cannot borrow as mutable”, this note explains why. I build it up one step at a time.

1. The problem that ownership solves

A program stores data in memory. When the program no longer needs the data, that memory must be freed. Languages handle this in three ways:

Language Who frees the memory? Problem
C You call free() by hand Forget it and the program leaks memory. Free twice, or use memory after freeing it, and it crashes or silently corrupts data.
Python A garbage collector frees it while the program runs It costs extra time and memory, and you don’t control when it happens
Rust The compiler works out where to free it, using ownership rules You must follow the rules, and the compiler enforces them

Python mainly counts how many names point to each object (reference counting). When the count drops to 0, Python frees the object, and a cycle collector cleans up what counting misses. All of this happens while the program runs.

Rust decides where memory is freed while the code compiles. At runtime nothing is counted or collected. That’s how Rust is fast and memory-safe at the same time.

2. Stack and heap: the two memory areas

You need these two terms first.

The stack is fast and simple. It holds values whose size is fixed and known in advance, such as f64, usize and [[f64; 2]; 2]. A function puts its values on the stack when it starts, and they’re removed automatically when it ends. Think of a stack of plates: you only add or remove at the top.

The heap holds data whose size can change while the program runs, such as Vec<f64> and String. The program has to ask the system for heap memory (an allocation), and that memory has to be freed later. Think of a warehouse: you rent space, and someone must give it back.

The Vector type from the demo (src/math/matrix.rs) uses both:

STACK (24 bytes)              HEAP
┌──────────────┐             ┌─────┬─────┬─────┐
│ pointer   ───┼────────────►│ 1.0 │ 2.0 │ 3.0 │
│ length = 3   │             └─────┴─────┴─────┘
│ capacity = 3 │
└──────────────┘

I checked the stack part with std::mem::size_of::<Vector>(): 24 bytes (a pointer, a length and a capacity, 8 bytes each on a 64-bit machine). The numbers themselves live on the heap.

The stack part cleans itself up. The real question is who frees the heap part. Ownership answers it.

3. The three ownership rules

  1. Every value has exactly one owner. Usually the owner is a variable. It can also be a struct field or a collection holding the value.
  2. There can be only one owner at a time.
  3. When the owner goes out of scope, Rust frees the value. This is called drop.

A scope is the region between { and }.

{
    let v = Vector::new(vec![1.0, 2.0, 3.0]); // v owns the vector
    // use v here
} // v goes out of scope here, and Rust frees the heap memory

You never write free(). The compiler inserts the cleanup at the }.

See drop happen

You can watch this by giving a type a Drop implementation that prints a message when it’s freed:

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 scope ends");
    }
    let c = Noisy("c");
    let _d = c; // move: only _d owns it now
    println!("main ends");
}
inner scope ends
dropping b
main ends
dropping c
dropping a

Three things to notice:

  • b is dropped exactly at the end of its inner { }.
  • At the end of main, values are dropped in reverse order of creation.
  • c was moved into _d (see the next section), so it’s dropped once, not twice.

4. Move: ownership is transferred

When you assign a heap-owning value to another variable, ownership moves:

let a = Vector::new(vec![1.0, 2.0]);
let b = a;              // ownership moves from a to b
println!("{:?}", a);    // ERROR: a was moved
error[E0382]: borrow of moved value: `a`

Why does Rust do this? Imagine that a and b both owned the heap data. At the }, both would free the same memory: a double free, a classic C crash. With a move, only b owns the data, so it’s freed exactly once.

A move is cheap. Only the 24-byte stack part is copied. The heap numbers stay where they are.

Compare this with Python:

a = [1.0, 2.0]
b = a          # a and b point to the SAME list
b.append(3.0)
print(a)       # [1.0, 2.0, 3.0]: a changed too!

Python lets two names share one object. Rust doesn’t allow two owners. (When you genuinely need shared ownership, Rust has explicit tools for it, such as Rc, but that’s a later topic.)

Passing a value to a function also moves it

fn consume(v: Vector) { /* v is dropped at the end of this function */ }

let a = Vector::new(vec![1.0]);
consume(a);           // a moves into the function
println!("{:?}", a);  // ERROR: a was moved

The compiler points at the exact cause: this parameter takes ownership of the value.

5. Copy and Clone: two ways to duplicate

Copy: small, stack-only values are duplicated automatically. They never move.

let x: f64 = 1.5;
let y = x;           // a copy, not a move
println!("{}", x);   // OK

f64, usize, bool, Matrix2 and DimensionMismatch are all Copy. Note 06 shows how a struct becomes Copy.

Clone: an explicit, possibly expensive duplicate.

let a = Vector::new(vec![1.0, 2.0]);
let b = a.clone();   // new heap allocation, and the numbers are copied
println!("{:?}", a); // OK: a and b own two separate copies

6. Borrowing: use a value without owning it

Moving and cloning often aren’t what you want. Usually you want to look at a value and then give it back. That’s borrowing. A borrow is a reference, written with &.

Analogy: you own a book. A friend borrows it, reads it and returns it. You still own the book, and the friend isn’t allowed to throw it away.

fn print_dimension(v: &Vector) {   // borrows: doesn't take ownership
    println!("dimension = {}", v.dimension());
}

let a = Vector::new(vec![1.0, 2.0]);
print_dimension(&a);  // lend a
print_dimension(&a);  // lend it again
println!("{:?}", a);  // a is still the owner

There are two kinds of borrow:

Syntax Name Allows Analogy
&T shared borrow read only many people can read the book at the same time
&mut T mutable borrow read and change one person writes in the book, and nobody else touches it

A reference is just a pointer: size_of::<&Vector>() is 8 bytes.

The borrowing rule

At any one time you can have either:

  • any number of &T (readers), or
  • exactly one &mut T (one writer).

Never both at the same time.

let mut v = vec![1.0, 2.0];
let first = &v[0];     // a shared borrow (a reader)
v.push(3.0);           // ERROR: push needs &mut v, but a reader still exists
println!("{}", first);
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

Why is this an error? push may need more room and move the numbers to a new, larger heap location. first would then point at freed memory. In C, this bug silently corrupts memory. Rust stops it before the program even exists.

A detail that confused me: a borrow lasts only until its last use, not until the }. If you read first before the push, the code compiles:

let mut v = vec![1.0, 2.0];
let first = &v[0];
println!("{}", first); // last use of the reader...
v.push(3.0);           // ...so the writer is allowed now

A borrow cannot outlive its owner

let r;
{
    let x = 5.0;
    r = &x;
}                   // x is dropped here
println!("{}", r);  // ERROR: r would point at freed memory
error[E0597]: `x` does not live long enough

This check is what lifetimes are about: the compiler proves that every borrow ends before its owner is dropped.

mut: changing needs permission twice

Variables are read-only by default. To change a value, or to lend it out as &mut, the variable itself must be declared let mut:

let v = Vector::new(vec![1.0]);
v.scale(2.0);   // ERROR: v is not declared as mutable
error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable

7. Back to self, &self and &mut self

Methods take their value in one of the same three ways. The demo’s Vector has one method of each kind:

impl Vector {
    /// Borrow to read.
    pub fn dimension(&self) -> usize {
        self.components.len()
    }

    /// Borrow to change.
    pub fn scale(&mut self, k: f64) {
        for x in self.components.iter_mut() {
            *x *= k;
        }
    }

    /// Take ownership: the Vector is consumed.
    pub fn into_components(self) -> Vec<f64> {
        self.components
    }
}
let mut v = Vector::new(vec![1.0, 2.0]);
v.dimension();                 // v is still usable
v.scale(2.0);                  // v is still usable, and now holds [2.0, 4.0]
let raw = v.into_components(); // v moved into the method; v is gone now
Form Meaning After the call
self take ownership (the value moves into the method) the caller can’t use it any more
&self borrow to read unchanged, still usable
&mut self borrow to change changed, still usable

Display::fmt (the method behind println!("{}", ...), see Note 02) takes &self because printing only reads. That’s why you can still use a value after printing it.

8. All the ownership concepts together

Concept What happens Cost
Owner One variable (or field) is responsible for a value none
Drop The owner leaves its scope, and Rust frees the value one free, at a known place
Move Ownership transfers, and the old name becomes unusable copies only the stack part (24 bytes for Vector)
Copy A small stack value is duplicated automatically copies a few bytes
Clone An explicit full duplicate can allocate heap memory
&T A shared, read-only borrow; many can exist at once a pointer, 8 bytes
&mut T One exclusive borrow that can change the value a pointer, 8 bytes
Lifetime The compiler proves a borrow ends before the owner is dropped none at runtime

Summary: Rust ownership and borrowing

  • Ownership is how Rust frees memory without a garbage collector.
  • Every value has one owner. When the owner leaves its scope, Rust drops the value, exactly once.
  • Assigning or passing a heap value moves it. Copy types are copied instead.
  • .clone() makes an explicit, possibly expensive duplicate.
  • & borrows to read. &mut borrows to change. You can have many readers or one writer, never both at once, and a borrow ends at its last use.
  • The compiler checks all these rules before the program runs, so they cost nothing at runtime.