RustOwnership

Ownership in Rust

Ownership is Rust's most distinctive feature — and its most powerful. It is what lets Rust guarantee memory safety without a garbage collector and without requiring you to manually call malloc and free. If you truly understand ownership, you understand why Rust works the way it does.

The Problem: Memory Management is Hard

Every program that runs needs to use memory. The difficult part is not allocating memory — it's knowing when to free it. Get it wrong and you get one of three classic bugs:

  • Memory leak — you forget to free memory. The program consumes more and more RAM until it crashes or is killed.

  • Dangling pointer — you free memory and then try to use it. The memory now holds garbage, or belongs to a different object. Reading it produces nonsense; writing to it corrupts other data.

  • Double free — you free the same memory twice. The allocator's internal bookkeeping gets corrupted, causing crashes or security vulnerabilities.

Languages take two broad approaches to this problem:

  • Garbage collection (Java, Python, Go) — a runtime periodically scans for memory that is no longer reachable and frees it automatically. Safe, but adds runtime overhead and unpredictable pauses.

  • Manual management (C, C++) — you call malloc/free or new/delete yourself. Fast, but error-prone. The bugs above are common and often security-critical.

Rust takes a third path: ownership. Memory is managed by a set of compile-time rules that the compiler checks before your program ever runs. No garbage collector. No manual free. No runtime overhead.

The Three Rules of Ownership

The entire ownership system rests on three rules:

  1. Each value in Rust has exactly one owner — a variable that is responsible for the value.

  2. There can only be one owner at a time — you cannot have two variables both owning the same value.

  3. When the owner goes out of scope, the value is dropped — its memory is freed automatically.

Note
These rules are enforced entirely at compile time. If your code violates any of them, the compiler rejects it with a clear error message. There is no runtime check and no overhead.
Rule 3 in Action: Automatic drop at End of Scope

When a variable's scope ends — the closing } — Rust automatically calls a special function called drop on it. drop frees any heap memory the value owns. You never have to do this yourself:

RUST
{
    let s = String::from("hello"); // s comes into scope
                                   // s is the owner of the heap buffer "hello"

    // do stuff with s...
    println!("{}", s);

} // s goes out of scope here
  // Rust calls drop(s) automatically
  // the heap memory for "hello" is freed
  // no malloc/free, no garbage collector
Success
Because `drop` is called deterministically at the end of scope, there are no memory leaks. The compiler guarantees that every allocation has exactly one corresponding `drop` call.
Stack vs Heap

To understand ownership, you need to understand where data lives:

Property

Stack

Heap

Size at compile time

Must be known

Can grow at runtime

Allocation speed

Instant — just move the stack pointer

Slower — the allocator must find free space

Example types

i32, bool, f64, char, tuples, arrays

String, Vec<T>, Box<T>, HashMap

Who frees it

Automatic when function returns

The owner, via drop

Simple types like i32 live entirely on the stack. Their size is fixed and known at compile time, so allocating and freeing them is trivial. Complex types like String store a small header on the stack (a pointer, a length, and a capacity) but the actual string data lives on the heap, where it can grow.

RUST
// lives entirely on the stack — 4 bytes, fixed size
let x: i32 = 5;

// stack header (pointer + length + capacity) points to heap data
let s = String::from("hello");
//      ^--- heap: h e l l o
Move Semantics: One Owner at a Time

When you assign one String to another, Rust does not copy the heap data. Instead, it moves the ownership from the original variable to the new one — and invalidates the original:

RUST
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED into s2

println!("{}", s2); // OK — s2 is the new owner
println!("{}", s1); // COMPILE ERROR — s1 was moved

Only the stack header is copied — the heap buffer stays in place and s2's header now points to it. s1's header is no longer valid. This might look like a shallow copy, but because s1 is invalidated, Rust calls it a move.

Why Move Prevents Double-Free Bugs

Imagine if both s1 and s2 were valid and both pointed to the same heap buffer. When s1 goes out of scope, Rust would call drop on it, freeing the buffer. When s2 later goes out of scope, Rust would call drop again — freeing already-freed memory. That is a double-free bug.

By invalidating s1 the moment the move happens, Rust ensures only s2 will call drop on the buffer. One owner, one drop, one free. The compiler enforces this at zero runtime cost.

Warning
This is also why types that own heap data (like `String` and `Vec`) do NOT implement the `Copy` trait. A bitwise copy would produce two owners of the same heap buffer, making double-free inevitable. Only types that are entirely stack-based — where duplication is always safe — get `Copy`.
Ownership and Functions

Passing a value to a function moves it into the function, exactly as if you had assigned it to a variable. The caller can no longer use the value after the call:

RUST
fn takes_ownership(s: String) {
    println!("{}", s);
} // s goes out of scope and drop is called — heap memory freed

fn main() {
    let my_string = String::from("world");

    takes_ownership(my_string); // my_string is moved into the function

    // println!("{}", my_string); // COMPILE ERROR — moved!
}

Compare that with an i32, which implements Copy. The function receives a copy of the integer, and the original variable in the caller remains valid:

RUST
fn makes_copy(n: i32) {
    println!("{}", n);
} // n is dropped here, but this doesn't affect the caller's copy

fn main() {
    let x = 5;
    makes_copy(x);           // x is COPIED into the function
    println!("x = {}", x);  // x is still valid — no move happened
}
Returning Ownership from Functions

A function can transfer ownership back to the caller by returning a value. The caller becomes the new owner:

RUST
fn gives_ownership() -> String {
    let s = String::from("from function");
    s // ownership moves out to the caller
}

fn main() {
    let received = gives_ownership(); // ownership transferred here
    println!("{}", received);
} // received is dropped here

You can also take ownership of a value, transform it, and return the (possibly modified) ownership back to the caller:

RUST
fn add_world(mut s: String) -> String {
    s.push_str(", world");
    s // move ownership back to caller
}

fn main() {
    let s1 = String::from("hello");
    let s2 = add_world(s1); // s1 moved in, result moved to s2
    println!("{}", s2);
}
The Ergonomics Problem

Moving ownership into a function and returning it back is correct — but verbose and cumbersome. Imagine needing to do this for every function call:

RUST
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length) // return the String AND the length so the caller gets ownership back
}

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

This works, but returning a tuple just to give ownership back is ugly. What if you had five parameters? The tuple approach becomes completely impractical.

Note
This is exactly the problem that **borrowing** was designed to solve. Instead of moving a value into a function and returning it back, you lend the function a temporary reference — the function can read (or write) the data without ever taking ownership. The next page covers borrowing and references in full detail.
A Mental Model for Ownership

Think of ownership like a physical object. There is only ever one person who owns an object at a time. When you give the object to someone else (let s2 = s1), you no longer have it. When the owner leaves the room (goes out of scope), they take the object with them and it is gone. Nobody else can use it. No two people can own the same object simultaneously.

The compiler is the room monitor — it tracks who owns what at every point in the program and rejects any code that would violate these rules. You never have to track it yourself at runtime.

Summary of Ownership Rules
  • Each value has exactly one owner at any point in time.

  • Assigning a heap value to another variable moves it — the old variable is invalidated.

  • Passing a heap value to a function moves it — the caller loses access.

  • When an owner goes out of scope, drop is called automatically and memory is freed.

  • Stack-based types (i32, bool, etc.) implement Copy and are duplicated rather than moved.

  • Returning a value from a function transfers ownership to the caller.

Tip
Ownership errors from the Rust compiler are precise and informative — they tell you exactly where a value was moved and where you tried to use it after the move. Read them carefully. They are not cryptic warnings; they are the compiler pointing at the exact lines involved. Over time, reading these messages trains you to think in ownership terms before you even write the code.