Ownership

A high-level introduction to Rust's ownership system, covering the stack and heap, moves, copies, borrowing, and slices, and how they guarantee memory safety without a garbage collector.

Rust’s promise of memory safety without a garbage collector rests on a single idea: every value has exactly one owner at any moment, and that owner is responsible for cleaning it up. This page gives you the big picture before you dive into the detailed mechanics in the subsections that follow. You will see what ownership is, why it exists, how it works on the stack and the heap, and how the concepts of moving, copying, borrowing, and slicing fit together.

The Memory Management Problem

Every running program needs memory. In languages with a garbage collector (Java, Go, Python) the runtime periodically scans memory to find and free data that is no longer used. In languages like C and C++, the programmer must manually allocate and free memory, which opens the door to use-after-free bugs, double frees, and memory leaks. Rust takes a third path: it enforces a set of ownership rules at compile time, so memory is freed deterministically the moment it is no longer needed, with no runtime overhead and no manual calls to free.

No Garbage Collector, No Manual Free:

Rust's ownership system allows it to eliminate an entire class of memory bugs without paying for a garbage collector at runtime. The compiler knows, without any annotations, exactly when each piece of memory is no longer reachable and injects the cleanup code automatically.

If you have used a language that handles memory for you, you may not have thought about when memory is actually released. In Rust, that release moment is always visible in the structure of the code: it happens when the owning variable goes out of scope.

The Core Ownership Rules

There are three rules that the Rust compiler enforces for every value, every time. They are not guidelines or best practices — they are hard constraints.

The Three Ownership Rules:

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

These rules are simple, but they interact with every part of the language. The rest of this section unpacks what they mean when you assign variables, pass values to functions, return values, and work with references.

Stack vs. Heap: The Foundation of Ownership

Ownership decisions depend on where data lives — on the stack or on the heap. Knowing the distinction makes the “move” and “copy” behavior predictable.

  • Stack: Fast, fixed‑size data is placed here. The stack works like a pile of plates: the last thing you put on is the first thing you take off. Because the size is known at compile time, the compiler can automatically push and pop values as scopes open and close. Primitive types like integers, booleans, and fixed‑size arrays live on the stack.
  • Heap: Data whose size is not known at compile time (or that can grow) goes on the heap. The allocator finds a spot in the heap, writes the data there, and returns a pointer. That pointer (which has a fixed size) is then stored on the stack. Types like String and Vec<T> keep their actual content on the heap and their metadata (pointer, length, capacity) on the stack.

Mental Model:

Think of the stack like a scratchpad where you jot down small, fixed notes that you can erase quickly. The heap is like a warehouse where you store larger, variable‑size items and keep a slip of paper with the warehouse shelf number (the pointer) in your scratchpad. Ownership is about who holds that slip of paper and who is responsible for telling the warehouse to free the shelf when the slip is thrown away.

Because stack‑only types are cheap to duplicate (just copy the bits), Rust allows them to be copied implicitly. Heap‑backed types, however, are moved — only the pointer on the stack is copied, and the original variable is invalidated so that only one “slip” controls the warehouse shelf. This prevents accidental double‑free errors.

Moves: Transferring Ownership

When you assign a heap‑backed value from one variable to another, the ownership moves. After the move, the original variable can no longer be used. This is the compiler preventing you from having two owners who might both try to free the same memory.

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

// println!("{}", s1);   // Compiler error: value borrowed after move
println!("{}", s2);      // Works fine

The assignment let s2 = s1; does not make a deep copy of the string. It copies only the stack‑resident metadata (pointer, length, capacity) to s2 and invalidates s1. From that point onward, only s2 owns the heap‑allocated "hello". When s2 goes out of scope, the String’s drop function frees the heap memory. If s1 were still considered valid, both variables would try to drop the same memory at the end of their scopes — a double‑free bug. Rust’s move semantics make that scenario impossible at compile time.

Moved Values Are Unusable:

Attempting to use a variable after its value has been moved will result in a compile error like error[E0382]: borrow of moved value. This is one of the most frequent errors new Rust programmers encounter. The fix is either to clone the value or to restructure the code so that the original variable is no longer needed.

The full details of move semantics — including how moves interact with structs, enums, and control flow — are covered in the Moves page.

Copies: The Exception to Moves

Not every assignment is a move. Types that implement the Copy trait — all primitive integer and floating‑point types, booleans, characters, and tuples or arrays made entirely of Copy types — are duplicated bit‑for‑bit when assigned. The original variable remains valid because copying a stack‑only value is cheap and has no risk of a double‑free.

let x = 5;
let y = x;          // x is copied, not moved
println!("x = {}, y = {}", x, y); // both are usable

Because the entire value lives on the stack, making an independent copy is a trivial memcpy operation. The compiler applies Copy automatically for types that consist only of other Copy types.

`Copy` Is Not the Default for Custom Types:

If you define your own struct, it will not be Copy by default, even if all its fields are Copy. You must derive or implement the Copy trait explicitly. Forgetting this is a common source of surprise when you expect assignment to produce an independent duplicate but instead get a move.

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1;                // p1 is moved, not copied
// println!("{:?}", p1);    // error: value borrowed after move

Adding #[derive(Copy, Clone)] to Point would make let p2 = p1; a copy instead.

See the Copy Types: The Exception to Moves page for a full list of Copy types and the rules that govern when a type can be made Copy.

Ownership and Functions

Passing a value to a function moves or copies it just like assignment. The same rule applies: heap‑owned types are moved, Copy types are copied. A function that receives a moved value becomes its new owner and is responsible for dropping it (unless it moves the value further).

fn take_ownership(s: String) { // s enters scope and owns the String
    println!("{}", s);
}                              // s goes out of scope; memory freed

fn make_copy(n: i32) {        // n comes into scope as a copy
    println!("{}", n);
}                              // n goes out of scope; nothing special happens

fn main() {
    let s = String::from("hello");
    take_ownership(s);        // s moved into the function
    // s is no longer valid here

    let x = 5;
    make_copy(x);             // x copied, still usable afterward
    println!("{}", x);        // works
}

Returning a value from a function also transfers ownership to the caller. This pattern can be used to pass a value into a function, do some work, and give it back — but writing code that constantly moves values in and out quickly becomes cumbersome. That is where references and borrowing come in. The full interaction between ownership and function boundaries is explored in Ownership and Functions.

References and Borrowing: Lending Values Without Giving Them Away

Instead of transferring ownership, you can let a function (or a block of code) temporarily access a value through a reference. This act is called borrowing. The original variable remains the owner and will still free the memory when it goes out of scope.

fn calculate_length(s: &String) -> usize { // borrows s immutably
    s.len()
}                                          // s is not dropped here

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);       // pass a reference
    println!("The length of '{}' is {}.", s, len); // s is still valid
}

References are denoted with &. A reference is a pointer to the value, but the ownership system guarantees that the referenced data remains valid for the entire lifetime of the reference — the compiler will reject code that tries to modify or drop a value while references to it are still live. Rust also enforces a rule that, at any given time, you can have either one mutable reference or any number of immutable references, but not both simultaneously. This prevents data races at compile time.

Seeing Borrowing Work:

If your program compiles after you replace a direct value pass (s) with a reference (&s), and the original variable is still usable after the function call, you have successfully borrowed. The compiler has verified that no ownership was transferred and that all reference lifetimes are valid.

The borrowing model and its rules — mutable versus immutable references, the scope of borrows, and non‑lexical lifetimes — are detailed in References and Borrowing.

The Slice Type: References into Collections

A slice is a reference to a contiguous range of elements inside a collection, like a String or a Vec<T>. Slices let you work with a subset of data without taking ownership or copying the underlying storage. They are written as &str for string slices and &[T] for general slices.

let s = String::from("hello world");
let hello: &str = &s[0..5];
let world: &str = &s[6..11];

A slice is always a reference; it borrows from the original collection. The borrow checker ensures the original collection cannot be mutated or dropped while a slice is live. This makes slices both safe and zero‑cost abstractions. The Slice Type page explains the syntax, the relationship between String and &str, and how slices work with arrays and vectors.

A Tour of the Ownership Section

Now that you have the conceptual map, the following pages walk through each part in depth. The order matters: start with “What Is Ownership?” to ground yourself in the rules, then proceed to moves and copies to understand assignment behavior, then ownership and functions to see the rules applied at function boundaries, and finally borrowing and slices for the tools that make ownership ergonomic.

  • What Is Ownership? — the rules in detail, scope, and the drop mechanism.
  • Moves — how heap data transfers, partial moves, and moves in pattern matching.
  • Copy Types: The Exception to Moves — which types are Copy, when to implement it, and the distinction from Clone.
  • Ownership and Functions — passing and returning values, the ownership dance.
  • References and Borrowing — immutable and mutable references, borrowing rules, and how they prevent dangling pointers and data races.
  • The Slice Type — string slices, array slices, range indexing, and the relationship between owned and borrowed collections. Each page is designed to be read in sequence, but you can jump to any topic and find a self‑contained explanation.