Skip to main content

Ownership Rules

Ownership is Rust’s way of managing memory without a garbage collector and without runtime overhead. The compiler enforces a set of rules at compile time to guarantee:

  • No use-after-free
  • No double-free
  • No data races
  • No dangling pointers

All of that, before your program ever runs.

The Three Ownership Rules

Rule 1: Each value in Rust has a single owner

Every value has exactly one variable that owns it.

let s = String::from("hello");

Here:

  • The String value "hello" is owned by s
  • s is responsible for freeing the memory when it goes out of scope

Rule 2: When the owner goes out of scope, the value is dropped

When a variable goes out of scope, Rust automatically calls drop.

{
let s = String::from("hello");
} // s goes out of scope → memory is freed

No free(), no GC. Rust does this deterministically.

Rule 3: A value can only have one owner at a time

This is the rule that surprises most people.

let s1 = String::from("hello");
let s2 = s1; // ownership moves

After this:

  • s2 owns the string
  • s1 is invalid
println!("{}", s1); // ❌ compile-time error

Why?
Because if both s1 and s2 could free the same memory, you’d get a double free bug.

Rust prevents that by moving ownership instead of copying.

Move vs Copy

Types that are moved

Heap-allocated types: String, Vec<T>, Box<T>

let v1 = vec![1, 2, 3];
let v2 = v1; // move
// v1 is no longer usable

Types that are copied

Simple stack-only types implement the Copy trait:

  • i32, f64
  • bool
  • char
  • Tuples of Copy types
let x = 5;
let y = x; // copy
println!("{}", x); // ✅ OK

These are cheap to duplicate and don’t manage heap memory.

Ownership and Functions

Passing ownership to a function

fn take_ownership(s: String) {
println!("{}", s);
} // s is dropped here

let s = String::from("hello");
take_ownership(s);
println!("{}", s); // ❌ error

Ownership moves into the function.

Returning ownership

fn give_back(s: String) -> String {
s
}

let s1 = String::from("hello");
let s2 = give_back(s1);
// s1 is invalid, s2 owns the string

This works, but passing ownership back and forth gets annoying.

That’s where borrowing comes in.

Borrowing (References)

Instead of transferring ownership, you can borrow a value.

fn print_length(s: &String) {
println!("{}", s.len());
}

let s = String::from("hello");
print_length(&s);
println!("{}", s); // ✅ still valid
  • &String is an immutable reference
  • The function can read but not modify
  • Ownership never changes

Mutable Borrowing

You can also borrow mutably—but carefully.

fn add_world(s: &mut String) {
s.push_str(" world");
}

let mut s = String::from("hello");
add_world(&mut s);
println!("{}", s); // "hello world"

The Borrowing Rules (Very Important)

At any given time, either:

  1. Any number of immutable references
let r1 = &s;
let r2 = &s;

OR

  1. Exactly one mutable reference
let r = &mut s;

❌ You cannot mix them.

let r1 = &s;
let r2 = &mut s; // ❌ compile-time error

Why?
Because mixing mutable and immutable access could lead to data races.

Rust enforces this at compile time.

Dangling References (Prevented by Ownership)

Rust won’t let references outlive the data they point to.

fn dangling() -> &String {
let s = String::from("hello");
&s // ❌ s is dropped at end of function
}

The compiler stops this entirely.

A Complete Example: Ownership in Action

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

let len = calculate_length(&s);
println!("Length: {}", len);

modify(&mut s);
println!("Modified: {}", s);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

fn modify(s: &mut String) {
s.push_str(" is awesome");
}
  1. s owns the String
  2. calculate_length borrows immutably
  3. Ownership stays in main
  4. modify borrows mutably
  5. Only one mutable borrow exists at that time
  6. Memory is freed automatically when main ends

Why Ownership Matters

Ownership lets Rust guarantee:

  • No null pointers
  • No use-after-free
  • No data races
  • No memory leaks (unless you explicitly opt in)

All with zero runtime cost.