Ownership in Rust
Understand Rust's ownership model — the rules, move semantics, the Copy trait, and Drop.
The Three Ownership Rules
Ownership is the feature that makes Rust unique. It is the mechanism through which Rust achieves memory safety without a garbage collector — no reference counting, no GC pauses, no runtime overhead. The entire system is enforced at compile time through three rules:
- Each value in Rust has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (freed).
These three rules, taken together, eliminate memory leaks, use-after-free bugs, and double-frees — the most common sources of security vulnerabilities in systems software.
Scope and Drop
The most immediate consequence of ownership is deterministic memory management. When a variable goes out of scope, its value is automatically freed. There is no free() to forget, no garbage collector to wait for — the compiler inserts the cleanup code exactly where it is needed.
fn main() {
{
let s = String::from("hello"); // s owns the String — heap memory is allocated
println!("{}", s);
} // s goes out of scope here — drop(s) is called automatically
// the heap memory is freed at this exact point
// println!("{}", s); // ERROR: s is no longer in scope
}
drop is called automatically by the compiler at the closing brace of the owner’s scope. This is deterministic — you always know exactly when memory is freed.
Stack vs Heap
Understanding the stack/heap distinction is essential to understanding ownership, because the two memory regions have fundamentally different characteristics that drive Rust’s design decisions.
- Stack: Fixed-size data known at compile time. Push/pop is extremely fast. Integers, floats, booleans, chars, fixed-size arrays all live here.
- Heap: Data whose size is unknown at compile time or may change at runtime. Allocated through an allocator, must be explicitly freed.
String,Vec,Boxall store their data on the heap.
fn main() {
let x = 5; // stored entirely on the stack
let s = String::from("hello"); // s (a 3-word struct) is on the stack,
// but the actual string bytes are on the heap
}
Move Semantics
When you assign a heap-allocated value to another variable, ownership moves — the original binding is invalidated and the new variable becomes the sole owner. This prevents double-free errors: because there is always exactly one owner, there is always exactly one drop. Rust does not silently copy heap data — the assignment is cheap (just copies the stack-stored pointer/length/capacity) and the original becomes unusable.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2 — s1 is no longer valid
// println!("{}", s1); // ERROR: value moved to s2
println!("{}", s2); // OK — s2 is the owner now
}
The same move happens when you pass a value to a function — the function becomes the new owner:
fn take_ownership(s: String) {
println!("got: {}", s);
} // s is the owner here, so it is dropped when this function returns
fn main() {
let s = String::from("world");
take_ownership(s); // s is moved into the function
// println!("{}", s); // ERROR: s was moved — it no longer exists here
}
And when returning from a function, ownership moves back to the caller:
fn give_ownership() -> String {
let s = String::from("hello");
s // move ownership to the caller — the String is not dropped here
}
fn take_and_give_back(s: String) -> String {
s // move the received value back to the caller
}
fn main() {
let s1 = give_ownership(); // s1 becomes owner
let s2 = String::from("world");
let s3 = take_and_give_back(s2); // s2 is moved in, s3 becomes owner of the result
println!("{} {}", s1, s3);
}
This is why Rust introduces borrowing — passing ownership around for every function call is tedious. But first, understand the Copy trait.
The Copy Trait
Types that are entirely stack-allocated and trivially copyable implement the Copy trait. For these types, assignment copies the value bitwise instead of moving ownership — both the original and the new binding remain valid. This is why you can use x after let y = x when x is an integer, even though you cannot do the same with a String.
fn main() {
let x = 5;
let y = x; // x is COPIED (not moved) because i32 implements Copy
println!("{} {}", x, y); // both still valid: 5 5
}
Types that implement Copy:
- All integer types:
i8,i16,i32,i64,i128,u8,u16,u32,u64,u128,isize,usize - Floating-point:
f32,f64 bool,char- Tuples and arrays only if all their elements are
Copy - Shared references
&T
Types that do not implement Copy because they manage heap resources:
StringVec<T>Box<T>HashMap,HashSet- Any type containing a non-
Copyfield
Cloning
When you genuinely need an independent copy of a non-Copy type — rather than just a reference — use .clone(). Clone is explicit by design: it signals at the call site that a potentially expensive heap allocation is happening, so readers immediately know this is not a cheap operation.
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy — allocates new heap memory for s2's data
println!("{} {}", s1, s2); // both valid — s1 and s2 are independent owners
}
Drop and RAII
Rust implements the RAII (Resource Acquisition Is Initialization) pattern. When an owner goes out of scope, Drop::drop is called on it. This is not just about memory — it applies to any resource. File handles are closed, network sockets are released, mutex locks are unlocked, all automatically when the owning variable leaves scope.
use std::fs::File;
fn main() {
{
let f = File::create("temp.txt").unwrap(); // file is opened (resource acquired)
// write to f...
} // f is dropped here — file is closed automatically, no close() needed
}
You can implement Drop for your own types to run custom cleanup logic:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
// This runs automatically when the Resource goes out of scope
println!("Dropping resource: {}", self.name);
}
}
fn main() {
let r1 = Resource { name: String::from("A") };
{
let r2 = Resource { name: String::from("B") };
println!("Inside inner block");
} // prints: Dropping resource: B
println!("Back in outer block");
} // prints: Dropping resource: A
Resources are dropped in reverse order of declaration — like a stack unwinding.
Forcing Early Drop
The standard library’s drop(value) function lets you drop a value before the end of its scope. This is useful when you need to release a resource — like a mutex lock — before the scope ends so that other code can acquire it.
fn main() {
let s = String::from("hello");
println!("created: {}", s);
drop(s); // explicitly drop s before the end of the block
// println!("{}", s); // ERROR: s has already been dropped
println!("s has been dropped early");
}
Ownership and Functions: Summary
fn main() {
// Copy type — assignment copies, both bindings remain valid
let n = 42;
let m = n;
println!("{} {}", n, m); // both valid
// Non-Copy type — assignment moves, original is invalidated
let v = vec![1, 2, 3];
let w = v;
// println!("{:?}", v); // ERROR: v was moved
// Non-Copy type — clone for an explicit independent copy
let a = String::from("hello");
let b = a.clone();
println!("{} {}", a, b); // both valid — independent heap allocations
// Passing a non-Copy value to a function moves it
let s = String::from("world");
let len = calculate_length(s); // s is moved into calculate_length
// s is no longer accessible here
// Use references (borrowing) to avoid moving — see next tutorial
}
fn calculate_length(s: String) -> usize {
s.len()
} // s is dropped here — the String is freed
The next tutorial covers borrowing and references, which let you use values without transferring ownership.