Ownership: The Core Concept
Learn Rust's ownership rules - each value has one owner, values drop when owners go out of scope, and ownership transfers on assignment. Understand move semantics, copy semantics, and the Drop trait.
You've learned that Rust prevents memory bugs at compile time without garbage collection. Ownership is the mechanism that makes this possible. Every value in Rust has exactly one owner—a variable responsible for that value's lifetime. When the owner goes out of scope, Rust automatically frees the value's memory. The compiler tracks ownership throughout your program and rejects code where ownership rules are violated.
This eliminates use-after-free bugs, double-free bugs, and memory leaks caused by forgotten deallocations. The cost is learning to think in terms of ownership transfers and borrowing. The compiler will reject valid algorithms from other languages. You must structure code to satisfy ownership rules. Once internalized, ownership becomes automatic—and the bugs it prevents simply disappear from your programs.
The Three Rules of Ownership
Ownership operates on three fundamental rules that the compiler enforces:
First, each value has exactly one owner. Only one variable owns any particular value at any given time.
Second, when the owner goes out of scope, Rust automatically drops the value, freeing its memory immediately.
Third, when you assign a value from one variable to another or pass it to a function, ownership transfers to the new variable. The original variable becomes invalid and can't be used again.
These rules apply universally. Every value follows them. The compiler checks ownership at compile time, so there's zero runtime cost for these guarantees.
Ownership in Practice
Consider a simple example with stack-allocated data:
{
let x = 5; // x owns the value 5
} // x goes out of scope, value 5 is dropped
When x goes out of scope at the closing brace, Rust drops the value. For stack-allocated integers, dropping is trivial—just moving the stack pointer. Nothing allocates on the heap, so nothing needs freeing.
Now consider heap-allocated data:
{
let s = String::from("hello"); // s owns the heap-allocated string
} // s goes out of scope, heap memory is freed
String::from allocates memory on the heap to store "hello". The variable s owns this allocation. When s goes out of scope, Rust calls the drop function for String, which frees the heap memory. You don't call free or delete manually. The compiler inserts the cleanup automatically based on scope.
Move Semantics
When you assign one variable to another, ownership can transfer. For types that allocate heap memory, assignment moves ownership:
let s1 = String::from("hello");
let s2 = s1; // Ownership moves from s1 to s2
println!("{}", s1); // Error: value borrowed after move
After let s2 = s1, only s2 is valid. The variable s1 becomes invalid. Rust prevents using it. This prevents double-free bugs. If both variables were valid and both tried to free the same heap memory when going out of scope, you'd have undefined behavior. By moving ownership and invalidating s1, Rust ensures only one variable will free the memory.
Under the hood, String contains a pointer to heap memory, the length of the string, and the capacity of the allocated buffer. When you assign s1 to s2, Rust copies these three values from s1 to s2. Both variables would point to the same heap memory. To prevent double-free, Rust invalidates s1.
Compare this to shallow copying in other languages, where both variables remain valid and point to the same data. Shallow copying without garbage collection creates aliasing bugs—modifying through one variable affects the other, and freeing one variable leaves the other with a dangling pointer. Rust's move semantics prevent these issues.
Copy Semantics
Not all types move ownership on assignment. Types that implement the Copy trait have copy semantics—assignment copies the value instead of moving ownership. Both variables remain valid:
let x = 5;
let y = x; // Copy the value 5
println!("{} {}", x, y); // Both x and y are valid
The integer 5 lives entirely on the stack. Copying it is cheap—just copying bytes. There's no heap allocation to manage, so there's no risk of double-free. Both x and y have their own copy of 5.
All primitive types implement Copy: integers, floating-point numbers, booleans, characters. Tuples and arrays implement Copy if all their elements implement Copy. A tuple (i32, i32) implements Copy, but a tuple (i32, String) doesn't because String doesn't implement Copy.
You can't implement Copy for types that implement Drop. If a type needs custom cleanup when going out of scope, it can't be copied implicitly. This prevents subtle bugs where copying a value would require duplicating resources like heap memory or file handles.
Ownership and Functions
Passing a value to a function transfers ownership to the function parameter:
fn take_ownership(s: String) {
println!("{}", s);
} // s goes out of scope and is dropped
let s = String::from("hello");
take_ownership(s); // Ownership moves to the function
println!("{}", s); // Error: value used after move
After calling take_ownership(s), the variable s is invalid. The function owns the string now. When the function returns, s goes out of scope and the heap memory is freed.
Returning a value from a function transfers ownership to the caller:
fn create_string() -> String {
let s = String::from("hello");
s // Ownership moves to caller
}
let s = create_string(); // s owns the returned string
The local variable s inside create_string would normally be dropped when the function returns. By returning it, ownership transfers to the caller. The heap memory stays allocated because the caller now owns it.
This pattern works but becomes cumbersome. If you pass a value to a function and want to keep using it afterward, the function must return it:
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // Return ownership along with result
}
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
You have to pass s1 in, get back s2 (the same string), and also get the length. This works but feels awkward. Rust provides a better solution: references, which let you borrow values without taking ownership.
The Drop Trait and RAII
When a value goes out of scope, Rust calls its drop method if the type implements the Drop trait. This is Rust's approach to Resource Acquisition Is Initialization (RAII)—a pattern from C++ where resource lifetimes tie to object lifetimes.
The Drop trait defines cleanup behavior:
struct FileHandle {
path: String,
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("Closing file: {}", self.path);
// Actual file closing logic would go here
}
}
{
let file = FileHandle {
path: String::from("/tmp/data.txt"),
};
// Use the file...
} // file.drop() called automatically here
You don't call drop manually. The compiler inserts calls to drop at the end of scopes. This guarantees cleanup happens deterministically when the owner goes out of scope, not at some unpredictable time when a garbage collector runs.
This pattern extends beyond memory. Files close when file handles drop. Network connections close when connection objects drop. Locks release when lock guards drop. Rust ensures cleanup happens automatically and at predictable times.
You can't call drop directly because Rust would still call it again when the value goes out of scope, causing double-free. If you need to free a value early, use std::mem::drop:
let s = String::from("hello");
drop(s); // Explicitly drop s early
// s is invalid here
This is the same drop that Rust calls automatically—it just takes ownership of the value and drops it immediately.
Clone: Explicit Deep Copying
When you need to actually duplicate heap data, use the clone method:
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy: allocates new heap memory
println!("{} {}", s1, s2); // Both are valid
Calling clone performs a deep copy. It allocates new heap memory, copies the string data, and returns a new String that owns the copied data. Both s1 and s2 are valid and own separate heap allocations. When they go out of scope, each frees its own memory.
Cloning is explicit. When you see clone() in code, you know heap allocation is happening. This makes expensive operations visible. Languages that implicitly deep copy values hide performance costs. Rust makes them explicit.
Not all types implement Clone. Types that can't be meaningfully copied don't provide clone. This prevents bugs where you accidentally duplicate resources that shouldn't be duplicated.
Stack vs Heap Allocation Revisited
Understanding ownership requires understanding where data lives. Stack-allocated data has known size at compile time and disappears automatically when the stack frame pops. Heap-allocated data has variable size or outlives its creating function and requires explicit management.
Ownership is crucial for heap-allocated data. Stack data just disappears when the frame pops—no tracking needed. Heap data must be freed exactly once, at the right time. Ownership ensures this happens.
Consider a Vec<i32>:
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
The variable v lives on the stack. It contains a pointer to heap memory, the vector's length, and its capacity. The actual integers [1, 2, 3] live on the heap in dynamically allocated memory. When v goes out of scope, its drop implementation frees the heap memory.
If you move v, ownership of the heap allocation transfers:
let v1 = Vec::from([1, 2, 3]);
let v2 = v1; // Move ownership
// v1 is invalid, v2 owns the heap memory
The stack data (pointer, length, capacity) copies to v2, but ownership of the heap data transfers. Only v2 will free it.
Why Ownership Prevents Memory Bugs
Use-after-free bugs occur when you free memory but keep a pointer to it. Later code dereferences the freed pointer, accessing invalid memory. Ownership prevents this—after ownership moves, the original variable is invalid. The compiler rejects any attempt to use it.
Double-free bugs occur when you free the same memory twice. This corrupts the allocator's bookkeeping and causes undefined behavior. Ownership prevents this—only one variable owns any value, so only one drop call happens.
Memory leaks occur when you allocate memory but never free it. Ownership prevents most leaks—when the owner goes out of scope, Rust frees the memory automatically. Leaks can still happen if you create reference cycles with reference-counted pointers, but basic ownership eliminates the common case of forgetting to free.
Data races occur when multiple threads access shared data without synchronization. Ownership combined with Rust's borrowing rules (covered next) prevents data races at compile time. The type system enforces that you can't have mutable access to data that's also accessible immutably elsewhere.
Ownership Patterns
Common patterns emerge around ownership. Functions that consume ownership of inputs and return new owned values:
fn process(input: String) -> String {
// Transform input...
input.to_uppercase()
}
Functions that take ownership temporarily and return it:
fn analyze(data: Vec<i32>) -> (Vec<i32>, f64) {
let sum: i32 = data.iter().sum();
let avg = sum as f64 / data.len() as f64;
(data, avg) // Return ownership along with result
}
These patterns work but feel cumbersome when you need to use values after passing them to functions. The next chapter covers borrowing—how to give functions access to data without transferring ownership. Borrowing eliminates the awkwardness while preserving the safety guarantees ownership provides.
Ownership is Rust's fundamental safety mechanism. Master it and you master the language. The restrictions feel unfamiliar initially, especially if you're coming from languages with garbage collection or manual memory management. The compiler will reject code that would work in other languages. Persist through the learning curve. Ownership prevents entire categories of bugs at compile time. When your program compiles, you have strong guarantees about memory safety that no amount of testing in other languages can provide.
