TechLead
Lesson 3 of 28
5 min read
Rust

Ownership: Rust's Core Concept

Understand Rust ownership rules, move semantics, the Copy trait, stack vs heap allocation, and how ownership prevents memory bugs.

Why Ownership Matters

Ownership is Rust's most distinctive feature and the foundation of its memory safety guarantees. Instead of using a garbage collector (like Java/Go) or manual memory management (like C/C++), Rust uses a system of ownership with rules that the compiler checks at compile time. No runtime cost is incurred.

The Three Ownership Rules

  1. Each value has exactly one owner — the variable that holds it
  2. There can only be one owner at a time — when ownership transfers, the old owner is invalidated
  3. When the owner goes out of scope, the value is dropped — memory is freed automatically

Stack vs Heap

Understanding where data lives in memory is essential to understanding ownership:

Stack Heap
Fixed-size data (i32, f64, bool, etc.)Dynamic-size data (String, Vec, etc.)
Very fast (push/pop)Slower (allocation/deallocation)
Automatically cleaned upNeeds ownership tracking
Data is copied (cheap)Data is moved (pointer transferred)

Move Semantics

When you assign a heap-allocated value to another variable, Rust moves ownership. The original variable becomes invalid. This prevents double-free bugs.

fn main() {
    // Heap-allocated String
    let s1 = String::from("hello");
    let s2 = s1; // s1 is MOVED to s2

    // println!("{s1}"); // ERROR: value borrowed after move
    println!("{s2}"); // OK — s2 owns the data

    // What happens in memory:
    // s1 → [ptr, len, cap] → "hello" on heap
    // After move:
    // s1 → (INVALID)
    // s2 → [ptr, len, cap] → "hello" on heap

    // Moves happen on assignment, function calls, and returns
    let s3 = String::from("world");
    takes_ownership(s3);
    // println!("{s3}"); // ERROR: s3 was moved into the function
}

fn takes_ownership(s: String) {
    println!("Got: {s}");
} // s is dropped here, memory freed

Common Pitfall: Accidental Moves

New Rust developers often get "value used after move" errors. The fix is usually to clone the data, borrow it with a reference, or restructure the code so the original owner isn't needed after the transfer.

The Copy Trait

Simple stack-only types implement the Copy trait. When copied, the original remains valid because copying is cheap (just a bitwise copy on the stack).

fn main() {
    // Integers implement Copy
    let x = 5;
    let y = x; // x is COPIED, not moved
    println!("x={x}, y={y}"); // Both valid!

    // Types that implement Copy:
    // - All integer types (i32, u64, etc.)
    // - bool
    // - char
    // - f32, f64
    // - Tuples of Copy types: (i32, bool) ✅
    // - Tuples with non-Copy: (i32, String) ❌

    // Types that do NOT implement Copy:
    // - String (heap-allocated)
    // - Vec (heap-allocated)
    // - Any type with a Drop implementation
}

// Stack-only types are copied into functions
fn makes_copy(n: i32) {
    println!("Got: {n}");
}

fn main_copy_demo() {
    let num = 42;
    makes_copy(num);
    println!("Still valid: {num}"); // OK — i32 is Copy
}

Clone: Explicit Deep Copy

fn main() {
    let s1 = String::from("hello");

    // Clone creates a deep copy (new heap allocation)
    let s2 = s1.clone();

    // Both are valid because s2 has its own heap data
    println!("s1={s1}, s2={s2}");

    // Clone can be expensive for large data!
    let big_vec = vec![0; 1_000_000];
    let big_copy = big_vec.clone(); // Copies 1M elements
    println!("Lengths: {} {}", big_vec.len(), big_copy.len());
}

Ownership and Functions

fn main() {
    let s = String::from("hello");

    // Passing to a function moves ownership
    let len = calculate_length_takes(s);
    // s is no longer valid here

    // Better: return ownership back
    let s2 = String::from("world");
    let (s2, len2) = calculate_and_return(s2);
    println!("{s2} has length {len2}");

    // Best: use references (next topic!)
}

fn calculate_length_takes(s: String) -> usize {
    s.len()
} // s dropped here

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

Key Takeaways

  • ✅ Each value in Rust has exactly one owner — when the owner goes out of scope, memory is freed
  • ✅ Assignment of heap data moves ownership, invalidating the original variable
  • ✅ Stack-only types (integers, bool, char) implement Copy and are cheaply duplicated
  • ✅ Use .clone() for explicit deep copies when you need both values
  • ✅ Ownership eliminates entire classes of memory bugs at compile time with zero runtime cost

Continue Learning