Phase 2Ownership & borrowing

#5 Ownership

move, copy, drop

What is ownership?

Ownership is Rust's most distinctive feature. It is a set of rules that the compiler checks at compile time to manage memory. No garbage collector is needed, and no manual allocation and deallocation is required.

Ownership is what makes Rust unique: it guarantees memory safety without any runtime cost.

The three rules

The ownership system is built on three simple rules:

Ownership rules
// 1. Each value in Rust has one (and only one) owner.
// 2. When the owner goes out of scope, the value is dropped.
// 3. There can only be one owner at a time.

{
    let s = String::from("hello"); // s is the owner
    // s is valid here
}   // s goes out of scope — memory is freed automatically

When a variable goes out of scope (exits the {} block), Rust automatically calls drop to free its memory. This is deterministic — you always know when memory is freed.

Move semantics

When you assign a heap-allocated value to another variable, the ownership moves. The original variable becomes invalid:

Move
let s1 = String::from("hello");
let s2 = s1;  // s1 is MOVED to s2

println!("{s2}");  // OK
// println!("{s1}");  // ERROR: s1 was moved

This prevents double free errors. Only one variable owns the data, so only one will free it when it goes out of scope.

If you need a deep copy, use clone():

Clone
let s1 = String::from("hello");
let s2 = s1.clone();  // Deep copy

println!("s1 = {s1}");  // OK — both are valid
println!("s2 = {s2}");  // OK

The Copy trait

Simple types stored entirely on the stack implement the Copy trait. For these types, assignment creates a copy instead of a move:

Copy types
let x = 5;
let y = x;  // Copy, not move!

println!("x = {x}, y = {y}");  // Both are valid!

// Types that implement Copy:
// - Integers (i32, u64, etc.)
// - Floating-point (f32, f64)
// - Booleans (bool)
// - Characters (char)
// - Tuples of Copy types: (i32, f64)

Ownership and functions

Passing a value to a function follows the same rules. Heap values are moved, stack values are copied:

Functions and ownership
fn greet(name: String) {
    println!("Hello, {name}!");
}   // name is dropped here

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)  // Return ownership back
}

fn main() {
    let name = String::from("Alice");
    greet(name);
    // println!("{name}");  // ERROR: name was moved into greet

    let s = String::from("hello");
    let (s, len) = calculate_length(s);
    println!("Length of \"{s}\" is {len}");
}

Returning values transfers ownership back to the caller. But passing ownership back and forth is cumbersome — that's why Rust has references, which we'll learn about in the next lesson.

Your turn

Try running the ownership demo with cargo run and cargo check in the terminal below:

terminal — cargo
user@stemlegacy:~/ownership-demo$