Ownership and borrowing
Ownership is Rust’s distinguishing feature and the foundation of its memory-safety guarantees. Each value has a single owner — a binding that determines the value’s lifetime and is responsible for releasing the value’s resources. Borrowing admits temporary access through references (&T for shared, &mut T for exclusive); the borrow checker statically verifies that references never outlive their referents and that simultaneous shared and exclusive references do not coexist. The combination produces a language that admits manual memory management with the safety guarantees of garbage-collected languages — and the compile-time errors that programmers learn to read carefully.
The model rejects substantial classes of bugs at compile time: data races, use-after-free, double-free, dangling pointers, and the iterator-invalidation hazards of C++. The cost is that some patterns common in other languages (graphs with cycles, certain tree shapes, observer patterns) require additional machinery (interior mutability, reference counting, manual lifetime management). The conventional Rust style is to embrace ownership and find ownership-friendly designs; the patterns that require workarounds are documented but uncommon in idiomatic code.
This page covers the ownership rules, moves vs copies, references, the borrow rules, and the conventions for writing ownership-correct code. Lifetimes are treated separately in Lifetimes; smart pointers (which admit shared ownership and interior mutability) are in Smart pointers.
The ownership rules
The three rules:
- Each value has a single owner — the binding currently designating it.
- There is exactly one owner at a time.
- When the owner goes out of scope, the value is dropped (its
Drop::dropruns, releasing resources).
fn main() {
let s = String::from("hello"); // s owns the String
{
let s2 = s; // ownership moves to s2
// s is no longer valid; s2 is the owner
println!("{}", s2); // OK
} // s2 goes out of scope; the String is dropped
// println!("{}", s); // ERROR: s is invalid (moved)
}
The String allocates heap memory; when s2 goes out of scope, the Drop implementation frees the memory. The single-owner rule guarantees that the memory is freed exactly once.
Moves
By default, assignment moves the value:
let s1 = String::from("hello");
let s2 = s1; // moves; s1 is no longer valid
println!("{}", s1); // ERROR: borrow of moved value
After the move, s1 is invalidated — the compiler refuses to let you use it. The original allocation is owned by s2; if both s1 and s2 were valid, the language would have to choose whose Drop runs (a double-free).
Moves apply to function arguments and return values:
fn take(s: String) {
println!("{}", s);
} // s is dropped here
let s = String::from("hello");
take(s); // s is moved into the function
// println!("{}", s); // ERROR: s was moved
For functions that need to return the value:
fn process(s: String) -> String {
println!("{}", s);
s // return s; ownership moves to the caller
}
let s = String::from("hello");
let s = process(s); // re-bind to the same name
The pattern of “move into, mutate, return” is common but cumbersome. The conventional alternative is borrowing.
Copy types
For types whose values are trivially copyable (no resource ownership), Rust marks them Copy. Copy types are copied on assignment, not moved:
let x = 5;
let y = x; // copy; x and y are independent
println!("{}", x); // OK
println!("{}", y); // OK
The Copy types include:
- All primitive types (
i32,f64,bool,char, etc.). (), the unit type.- Arrays and tuples of
Copytypes. - Shared references (
&T). - Function pointers and bare function items.
Conventionally, types implement Copy only when:
- They are small (typically a few words at most).
- They have no resource ownership (no
Drop). - Copying produces a useful value (no unique identity).
A type may implement Copy (as a marker trait) by deriving it; the prerequisite is that all fields are Copy:
#[derive(Copy, Clone)]
struct Point {
x: f64,
y: f64,
}
let p = Point { x: 3.0, y: 4.0 };
let q = p; // copy
println!("{:?}", p); // OK; p is still valid
Copy requires Clone (a stricter trait that admits explicit copying). Most Copy types also derive Clone.
Clone and explicit copying
For types that don’t implement Copy, the conventional way to duplicate is clone():
let s1 = String::from("hello");
let s2 = s1.clone(); // explicit deep copy
println!("{}", s1); // OK
println!("{}", s2); // OK
The clone() method is the conventional Rust mechanism for explicit copying. Programs typically use clones sparingly — moves and borrows are conventionally preferred for their performance and simplicity.
References (&T and &mut T)
A reference is a borrowed pointer that admits access without taking ownership:
let s = String::from("hello");
let r = &s; // r is a reference to s
println!("{}", r); // dereference is implicit
println!("{}", s); // OK: s is still owned
The &s produces a reference; s retains ownership.
Two reference forms:
- Shared reference
&T— read-only access; multiple shared references may coexist. - Exclusive reference
&mut T— read-write access; only one at a time, and no shared references during.
let s = String::from("hello");
let r1 = &s; // shared
let r2 = &s; // also shared; OK
let r3 = &s; // also shared; OK
let mut s = String::from("hello");
let r1 = &mut s; // exclusive
// let r2 = &mut s; // ERROR: only one mutable at a time
// let r2 = &s; // ERROR: shared during exclusive
The borrow checker enforces these rules at compile time. Violations produce errors with messages identifying the conflicting borrows.
Method calls and references
Methods take self, &self, or &mut self to indicate their ownership semantics:
impl String {
fn into_bytes(self) -> Vec<u8> { /* takes ownership */ }
fn len(&self) -> usize { /* shared borrow */ }
fn push_str(&mut self, s: &str) { /* exclusive borrow */ }
}
The conventional choice:
&selffor read-only access (the most common).&mut selffor methods that modify.selffor methods that consume the receiver (return ownership transformation, etc.).
The dot operator handles dereferencing automatically:
let s = String::from("hello");
s.len(); // automatically borrows
The compiler inserts the appropriate borrow ((&s).len() becomes s.len() syntactically).
Borrow rules
The two principal borrow-checker rules:
- Aliasing XOR mutability: at any given moment, either one mutable reference or any number of shared references — never both.
- References must not outlive their referents: if a reference is in scope, the referent must still be alive.
The first rule prevents data races and certain mutation hazards (iterator invalidation, etc.). The second prevents dangling references.
let mut v = vec![1, 2, 3];
let first = &v[0]; // shared borrow of v[0]
v.push(4); // ERROR: requires mutable borrow of v
println!("{}", first); // would access invalid memory if push reallocated
The borrow checker rejects the program at compile time. The fix:
let mut v = vec![1, 2, 3];
let first = v[0]; // copy out
v.push(4); // OK
println!("{}", first); // OK; first is independent of v
For substantial cases, the conventional patterns are: split borrows (borrowing different fields of a struct), shorter borrow scopes (let the shared borrow end before the mutable starts), and cloning when the cost is acceptable.
Slices
A slice is a reference to a contiguous region of a sequence:
let v = vec![1, 2, 3, 4, 5];
let slice: &[i32] = &v[1..4]; // slice of v from 1 to 4 (exclusive)
let s = String::from("hello");
let part: &str = &s[1..4]; // string slice
Slices are references; they don’t own their data. Slices admit substantial code reuse — functions that take &[T] or &str work for arrays, vectors, slices, and strings.
The slice’s lifetime is tied to its source; the borrow checker prevents the source from being dropped while the slice is in scope.
Lifetimes (briefly)
Every reference has a lifetime — the period during which the reference is valid. The borrow checker uses lifetimes to verify that references don’t outlive their referents.
In most cases, lifetimes are elided — the compiler infers them automatically:
fn first_word(s: &str) -> &str { // lifetimes elided
s.split_whitespace().next().unwrap_or("")
}
For more elaborate cases, explicit lifetime annotations are required:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
The full treatment is in Lifetimes.
Move vs borrow choice
The principal design question for any non-trivial function: take ownership or borrow?
| Need | Choose |
|---|---|
| Read but not modify | &T |
| Modify | &mut T |
| Take ownership (the function will hold or transform) | T |
| Read and the function may not need the original | &T (caller keeps ownership) |
| Need a clone | T (and the caller clone()s) |
The conventional Rust pattern is:
- Take
&Tfor read-only operations. - Take
&mut Tfor modifications. - Take
T(move) for consuming operations or constructors.
fn read(s: &str) -> usize { s.len() }
fn modify(v: &mut Vec<i32>) { v.push(10); }
fn consume(s: String) -> Vec<u8> { s.into_bytes() }
The pattern admits the substantial majority of Rust code; functions are reusable, callers retain ownership, and unnecessary clones are avoided.
Drop
The Drop trait admits running cleanup code when a value goes out of scope:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn main() {
let r = Resource { name: String::from("a") };
{
let r2 = Resource { name: String::from("b") };
// ...
} // drops "b"
// ...
} // drops "a"
The drop() method is called automatically; the explicit std::mem::drop(value) admits dropping early. Manually calling value.drop() is forbidden — the compiler enforces.
The drop order is the reverse of declaration order — the most-recently-declared value drops first. Within a struct, the fields drop in declaration order.
Common patterns
Take a slice, not an owned collection
fn longest(words: &[String]) -> &String { /* ... */ } // takes a slice
The function accepts &[String], &Vec<String>, or any contiguous sequence; the caller retains ownership.
Return owned strings
fn full_name(first: &str, last: &str) -> String {
format!("{} {}", first, last) // produces an owned String
}
The return type String admits the caller to take ownership; the function does the allocation.
Construct in place
fn build_vec() -> Vec<i32> {
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
v // moved out as the return value
}
The function constructs the value and returns it; ownership transfers to the caller. The conventional Rust pattern; no explicit return needed.
Take ownership for transformation
fn into_uppercase(s: String) -> String {
s.to_uppercase() // returns a new String; original dropped
}
let s = String::from("hello");
let upper = into_uppercase(s); // s moves in; upper owns the new value
// s is no longer valid; upper is "HELLO"
For single-use transformations, the move-and-return pattern is conventional. For reusable transformations, take &T and return T:
fn to_uppercase(s: &str) -> String {
s.to_uppercase()
}
clone() when ownership is needed
let s = String::from("hello");
let copy = s.clone(); // explicit deep copy
let other = process(copy); // process takes ownership
println!("{}", s); // OK: s still owns the original
println!("{}", other); // OK
The clone() is explicit; the conventional Rust style avoids unnecessary clones but uses them when the alternative would complicate the code substantially.
Split borrows on a struct
struct Pair {
a: Vec<i32>,
b: Vec<i32>,
}
let mut p = Pair { a: vec![1], b: vec![2] };
let ra = &p.a; // shared borrow of a
let rb = &mut p.b; // mutable borrow of b
// OK: different fields; no aliasing
The borrow checker admits split borrows — separate borrows of different fields of a struct. The mechanism admits substantial code that would not work with whole-struct borrows.
A note on unsafe
The unsafe keyword admits a small set of operations that the borrow checker does not verify:
- Dereferencing raw pointers (
*const T,*mut T). - Calling unsafe functions.
- Implementing unsafe traits.
- Accessing mutable static variables.
- Creating mutable references to packed-struct fields.
let raw: *const i32 = &x;
unsafe {
println!("{}", *raw); // dereferencing a raw pointer
}
The unsafe block is the conventional escape hatch for FFI, low-level operations, and advanced patterns that the borrow checker cannot verify. The discipline:
- Use
unsafesparingly and document why. - Encapsulate
unsafein a safe abstraction; expose a safe API. - Run
cargo miri(an undefined-behaviour detector) onunsafecode.
The standard library uses unsafe extensively; user code rarely needs it.
A note on the discipline
The contemporary Rust ownership advice:
- Default to borrowing — pass
&Tand&mut Trather thanT. - Move ownership when the function will store or consume the value.
- Use
clone()when the alternative is fighting the borrow checker — but only when the cost is acceptable. - Use smart pointers (
Rc,Arc,RefCell) only when shared ownership or interior mutability is genuinely needed. - Read borrow-checker errors carefully — they are often more illuminating than they look.
The combination — explicit ownership, borrowing rules, lifetime tracking — is the substance of Rust’s memory model. The discipline is substantial but produces a language that admits low-level performance with the safety of garbage-collected alternatives.