Table of Contents
- Introduction to Ownership
- The Stack and The Heap: A Quick Refresher
- Ownership Rules
- Transferring Ownership
- The
CopyandCloneTraits: Exceptions to the Rule - References and Borrowing
- Mutable References: Rules and Use Cases
- Common Pitfalls with Mutable References
- Conclusion
- References
Introduction to Ownership
Ownership is Rust’s unique approach to managing memory. Unlike languages with garbage collectors (e.g., Java) or manual memory management (e.g., C/C++), Rust enforces ownership rules at compile time, ensuring memory safety without runtime overhead.
At its core, ownership answers a simple question: Who is responsible for cleaning up a piece of memory when it’s no longer needed? This responsibility is tied to “owners”—variables that control the lifetime of a value.
The Stack and The Heap: A Quick Refresher
To understand ownership, we first need to distinguish between the stack and heap—two regions of memory used by your program:
-
Stack: A last-in, first-out (LIFO) data structure for storing values with a known, fixed size (e.g., integers, booleans, tuples with fixed-size elements). Stack allocation is fast because the compiler knows exactly how much space to reserve, and accessing data is O(1).
-
Heap: Used for values with dynamic or unknown size (e.g., strings, vectors). When you allocate on the heap, the operating system finds an empty spot, marks it as used, and returns a pointer to that location. Heap allocation is slower than the stack because it requires searching for free space, and accessing data involves following a pointer (O(1) but with higher constant time).
Ownership rules are most relevant for heap-allocated data, as stack data is often copied or dropped automatically without issues.
Ownership Rules
Rust’s ownership system is governed by three key rules:
- Each value in Rust has exactly one variable that is its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (deallocated).
Let’s unpack these with examples.
Rule 1 & 3: Scope and Drop
A variable’s scope is the range of code where it is valid. For example:
{
let s = String::from("hello"); // s is valid from here
// ... use s ...
} // s goes out of scope here; Rust calls `drop` to deallocate the String
Here, s is the owner of the String “hello”. When s goes out of scope (at the closing }), Rust automatically calls a special function called drop to free the heap memory used by the String. This prevents memory leaks!
Rule 2: One Owner at a Time
What happens when we assign a value to another variable? Let’s use a String (heap-allocated) and an integer (stack-allocated) to contrast behavior.
Transferring Ownership
Example 1: Heap-Allocated Values (e.g., String)
Consider this code:
let s1 = String::from("hello"); // s1 is the owner of "hello"
let s2 = s1; // What happens here?
You might expect s2 to be a copy of s1, but in Rust, this is a move operation. Here’s why:
A String has three parts on the stack: a pointer to the heap data, a length (number of bytes used), and a capacity (total bytes allocated). The actual string data (“hello”) lives on the heap:
s1: [ptr] -> heap: "hello"
[len] = 5
[cap] = 5
When we assign s1 to s2, Rust moves ownership of the heap data from s1 to s2. After the move:
s2now owns the heap data (ptr, len, cap are copied tos2’s stack space).s1is marked as invalid (no longer an owner).
If we try to use s1 after the move, Rust throws a compile error:
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // ❌ Compile error: use of moved value: `s1`
This prevents a double-free error (where both s1 and s2 would try to deallocate the same heap data when they go out of scope).
Example 2: Stack-Allocated Values (e.g., i32)
For stack-allocated values with fixed sizes (like i32, bool, or f64), Rust copies the value instead of moving it. This is safe and efficient because the data is small and lives on the stack.
let x = 5; // x owns the integer 5 (stack-allocated)
let y = x; // y is a copy of x (stack data is copied)
println!("x = {}, y = {}", x, y); // ✅ Works! x is still valid.
Why the difference? Stack data is cheap to copy, so Rust avoids the overhead of moving ownership. Heap data is expensive to copy, so Rust moves ownership instead.
The Copy and Clone Traits: Exceptions to the Rule
Rust uses traits to determine whether a type is copied or moved.
The Copy Trait
Types that implement the Copy trait are copied instead of moved when assigned to another variable. By default, Rust implements Copy for:
- All integer types (
i32,u64, etc.). - Boolean type (
bool). - Floating-point types (
f32,f64). - Character type (
char). - Tuples, if all their elements implement
Copy(e.g.,(i32, bool)isCopy, but(i32, String)is not).
You can check if a type is Copy with std::marker::Copy.
The Clone Trait
For types that don’t implement Copy (like String), use the Clone trait to explicitly create a deep copy of the heap data. This is slower but necessary when you need independent copies.
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicitly clone heap data
println!("s1 = {}, s2 = {}", s1, s2); // ✅ Both are valid!
Clone is a manual operation—Rust won’t do it automatically to avoid accidental performance hits.
References and Borrowing
Moving ownership every time you want to use a value is cumbersome (e.g., passing a String to a function would move ownership, making it unavailable afterward). References solve this by allowing you to access a value without taking ownership.
A reference is a pointer to a value, denoted by &T (for immutable references). Using a reference is called borrowing.
Immutable References
Here’s how to create an immutable reference to a String:
let s1 = String::from("hello");
let s1_ref = &s1; // s1_ref is an immutable reference to s1
println!("s1_ref = {}", s1_ref); // ✅ Access s1 via reference
Key properties of immutable references:
- They do not take ownership of the value.
- The original owner (
s1) retains ownership and remains valid. - You can have multiple immutable references to the same value (since reading doesn’t cause data races).
Example: Borrowing in Functions
References shine when passing values to functions. Instead of moving ownership, we pass a reference:
fn print_string(s: &String) { // s is an immutable reference
println!("{}", s);
}
let s = String::from("hello");
print_string(&s); // Borrow s (no ownership transfer)
println!("s is still valid: {}", s); // ✅ s is still owned by us!
No Dangling References
Rust ensures references are always valid (no dangling pointers). For example, this code is invalid:
fn dangle() -> &String {
let s = String::from("hello"); // s is owned by dangle()
&s // ❌ Returning a reference to s, which is dropped when dangle() ends
} // s goes out of scope here; reference is now dangling!
The compiler catches this with an error: “missing lifetime specifier” (we’ll cover lifetimes in a future blog, but the core issue is the reference outlives the value).
Mutable References: Rules and Use Cases
Immutable references are great for reading data, but what if you need to modify a value? Enter mutable references (&mut T), which allow you to change the borrowed value.
Basic Mutable Reference Example
let mut s = String::from("hello"); // s is mutable
let s_mut_ref = &mut s; // s_mut_ref is a mutable reference to s
s_mut_ref.push_str(" world"); // Modify s via the mutable reference
println!("s = {}", s); // ✅ s is now "hello world"
Strict Rules for Mutable References
To prevent data races (simultaneous access to the same data with at least one write), Rust enforces two critical rules for mutable references:
Rule 1: Only One Mutable Reference at a Time
You cannot have multiple mutable references to the same value:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ❌ Compile error: cannot borrow `s` as mutable more than once at a time
println!("r1 = {}, r2 = {}", r1, r2);
This prevents race conditions where two parts of code try to modify the same data simultaneously.
Rule 2: No Immutable References with a Mutable Reference
You cannot have immutable references if a mutable reference exists:
let mut s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &mut s; // ❌ Compile error: cannot borrow `s` as mutable because it is also borrowed as immutable
println!("r1 = {}", r1);
Immutable references guarantee read-only access, so allowing a mutable reference would break that guarantee.
Scoping Mutable References
These rules are enforced per scope, so you can reuse mutable references if they’re in different scopes:
let mut s = String::from("hello");
{
let r1 = &mut s; // r1 is valid in this inner scope
r1.push_str(" world");
} // r1 goes out of scope here
let r2 = &mut s; // ✅ Now we can create a new mutable reference
r2.push_str("!");
println!("s = {}", s); // Output: "hello world!"
Common Pitfalls with Mutable References
Accidental Multiple Mutable References
Even subtle code can violate the “one mutable reference” rule. For example:
let mut v = vec![1, 2, 3];
let first = &mut v[0]; // Mutable reference to first element
v.push(4); // ❌ Error: cannot borrow `v` as mutable (for push) while borrow is active (first)
*first = 5;
Why? v.push(4) may reallocate the vector’s heap data (if capacity is exceeded), invalidating first (which points to the old heap location). Rust catches this!
Mixing Mutable and Immutable References in Loops
Loops can accidentally hold references across iterations. For example:
let mut s = String::from("hello");
for _ in 0..2 {
let r = &s; // Immutable reference
println!("r = {}", r);
let r_mut = &mut s; // ❌ Error: cannot borrow as mutable while immutable reference exists
r_mut.push_str("a");
}
The immutable reference r lives until the end of the loop iteration, blocking the mutable reference r_mut. Fix: Limit the scope of r with braces.
Conclusion
Ownership and mutable references are foundational to Rust’s memory safety guarantees. By enforcing strict rules at compile time, Rust eliminates common bugs like use-after-free, double frees, and data races—all without runtime overhead.
Key takeaways:
- Ownership ensures each value has one responsible owner, dropped when out of scope.
- References (borrowing) let you access values without taking ownership.
- Mutable references allow modification but restrict you to one at a time (no concurrent mutation).
Mastering these concepts unlocks Rust’s full potential for writing safe, efficient systems code.