Ownership and moves in Rust
Rust's ownership rules explained: one owner per value, moves on assignment and function calls, drop at end of scope, and how to fix use after move.
Ownership is how Rust manages memory without a garbage collector and without you calling free. The compiler checks three rules:
- Every value has exactly one owner, the variable (or struct field, or collection) responsible for it.
- There is only one owner at a time.
- When the owner goes out of scope, the value is dropped: its memory (and any file, socket or lock it holds) is released.
Rule 3 means cleanup always happens, exactly once, at a place you can see in the code. Rules 1 and 2 are what make "exactly once" possible.
Moves
A String is a small handle on the stack (pointer, length, capacity) pointing at text on the heap. If assigning one String to another simply copied the handle, two variables would point at the same text and both would try to free it: a double free. Rust prevents that by moving instead:
fn main() {
let s1 = String::from("crab");
let s2 = s1;
println!("{s1} {s2}");
}
error[E0382]: borrow of moved value: `s1`
After let s2 = s1;, s2 owns the text and s1 is no longer usable. Nothing on the heap was copied; the move is as cheap as copying three numbers. The compiler simply stops you from using the old name.
The same happens when you pass a value to a function: the parameter becomes the owner, and the value is dropped when the function ends, unless the function gives it back by returning it.
fn shout(mut msg: String) -> String {
msg.push('!');
msg
}
fn main() {
let msg = String::from("hello");
let msg = shout(msg);
println!("{msg}");
}
hello!
What moves and what copies
Simple values that live entirely on the stack (i32, f64, bool, char, and tuples or arrays of them) are Copy: assigning them duplicates the bits, and both variables stay usable. Types that own heap memory or other resources (String, Vec<T>, Box<T>, HashMap, files) move.
Dropping
When a value's owner goes out of scope, Rust calls its drop code automatically. Values are dropped in the reverse order they were created. You can run it early with drop(value). This pattern (resource tied to an owner's scope) is called RAII, and it's why Rust code doesn't need finally blocks or manual close() calls.
How to fix "use after move"
- Borrow instead of moving. If a function only reads the value, make it take
&String(better,&str) or&Vec<T>(better,&[T]), and call it with&value. This is the usual answer. - Clone when you really need two independent copies:
let backup = report.clone();. It copies the heap data, so it costs time and memory. - Return the value from the function (as
shoutdoes above), or restructure so the last use is the move.
Best practices at work
- Take ownership (
String,Vec<T>) in a function or constructor when you're going to store the value; borrow when you only need to look at it. - Don't sprinkle
.clone()until it compiles. Each clone is an allocation, and code reviewers will ask why. Borrowing is usually the right fix. - Moving is cheap. Returning a big
Vecfrom a function doesn't copy its elements.
Example
struct Ticket {
id: u32,
title: String,
}
impl Drop for Ticket {
fn drop(&mut self) {
println!("closing ticket {}", self.id);
}
}
fn summarize(title: &str) -> usize {
title.split_whitespace().count()
}
fn archive(t: Ticket) {
println!("archiving #{}: {}", t.id, t.title);
} // t is dropped here
fn main() {
let first = Ticket { id: 1, title: String::from("Login page is slow") };
let second = Ticket { id: 2, title: String::from("Typo in footer") };
println!("{} words", summarize(&first.title)); // borrowed: first is still ours
archive(first); // moved: archive owns it now and drops it
let moved = second; // ownership moves to `moved`
println!("still open: #{}", moved.id);
let a = 5;
let b = a; // i32 is Copy: both stay usable
println!("{a} {b}");
println!("end of main");
} // `moved` is dropped here
Output:
4 words
archiving #1: Login page is slow
closing ticket 1
still open: #2
5 5
end of main
closing ticket 2