Ownership, Borrowing, and Lifetimes
What is ownership in Rust, and what are its three rules?
Answer
Ownership is the set of rules that lets Rust manage memory safely without a garbage collector and without manual malloc/free.
The three rules of ownership:
- Every value in Rust has an owner.
- A value has exactly one owner at a time. When ownership moves to another binding, the old binding can no longer be used.
- When the owner goes out of scope, the value is dropped.
Semantically, a move is a bitwise copy of the value's inline (stack) representation, after which the compiler statically marks the source binding as uninitialized. The optimizer often eliminates the copy entirely.
For a String, a move copies three machine words (pointer, length, capacity). The heap buffer is not copied and no allocation happens.
fn take(s: String) { println!("{s}"); }
fn main() {
let a = String::from("hi");
take(a);
// println!("{a}"); // E0382: borrow of moved value: `a`
}Move vs. Copy
Answer
At the machine level, a move and a copy are the same operation: a memcpy of the value's inline representation. The difference is purely in what the compiler allows afterwards:
- After a move, the source binding is invalidated. Using it is a compile-time error (
E0382). - After a copy, the source stays valid and can be used again.
Copy can't be customized: it's always a plain bitwise copy. Anything that needs extra work (for example, allocating a new heap buffer) must go through Clone, which is always called explicitly.
Which types implement Copy?
Answer
Copy is implemented by:
- all integer and floating-point types,
bool,char, and() - shared references
&T(but not&mut T) - raw pointers
*const Tand*mut T - function pointers and function items
- tuples and arrays whose elements are all
Copy Option<T>whenT: Copy, andResult<T, E>when bothTandEareCopy
A type can only be Copy if all of its fields are Copy. A type that implements Drop can't be Copy at all (E0184); otherwise every copy would run the destructor and the same resource would be freed several times. That's why String, Vec, Box, and File are move-only.
#[derive(Copy, Clone)]
struct Point {
x: f32,
y: f32,
}
fn main() {
let p = Point { x: 1.0, y: 2.0 };
let q = p; // copy
let r = p; // another copy, `p` is still valid
println!("{} {} {}", p.x, q.x, r.x);
}&T vs. &mut T, and the borrowing rules
Answer
&T is a shared reference and &mut T is an exclusive (unique) reference. They are distinct types with different guarantees.
The core borrowing rule is "aliasing XOR mutability": at any point in the program, a value can have either any number of &T references or exactly one &mut T, never both. On top of that, a reference must never outlive the value it points to.
fn main() {
let mut v = vec![1, 2, 3];
let r1 = &v;
let r2 = &v; // multiple shared references are fine
println!("{r1:?} {r2:?}");
let m = &mut v; // OK: r1 and r2 are no longer used (NLL)
m.push(4);
}These guarantees are what enable optimizations. When compiling to LLVM IR, rustc marks &mut T parameters with the noalias attribute (and &T too, as long as T has no UnsafeCell inside), telling LLVM that no other pointer can observe or modify that memory. As a result, creating aliasing &mut references through unsafe is undefined behavior, even if the code "looks correct" and appears to work.
What are lifetimes, and why are explicit annotations needed?
Answer
A lifetime is the region of code during which a reference is guaranteed to be valid. Every reference has a lifetime, either inferred by the compiler or written explicitly as 'a. Lifetimes exist only at compile time, during borrow checking; they leave no trace at runtime.
Explicit annotations are needed when the compiler can't unambiguously tell how the lifetimes of the inputs relate to the lifetime of the output, i.e. when the elision rules don't apply.
The most common case: a function takes several references and returns one.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}Here 'a says: "the returned reference is valid for no longer than both a and b are."
Important: annotations don't extend how long data lives; they only describe relationships that already exist. If you claim 'static for a value that lives shorter than that, the compiler will reject the code.
Structs that hold references also need lifetime parameters:
struct Parser<'src> {
input: &'src str,
pos: usize,
}'src is required because the struct stores a reference, and the compiler must ensure that a Parser never outlives the input it borrows from.
Lifetime elision rules
Answer
Lifetime elision is a set of deterministic rules the compiler uses to fill in lifetimes you've left out. It applies to function and method signatures (and to impl headers via '_). Inside function bodies, lifetimes are inferred rather than elided, and struct definitions always require explicit lifetimes.
The rules:
- Each elided lifetime in the parameters becomes a distinct lifetime parameter.
- If there is exactly one input lifetime, it is assigned to all elided output lifetimes.
- If there are multiple input lifetimes but one of them is
&selfor&mut self, the lifetime ofselfis assigned to all elided output lifetimes.
If none of these rules determines the output lifetime, you get a compile error and have to annotate it yourself.
// Rules 1 and 2: fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
struct Cache;
impl Cache {
// Rules 1 and 3: fn get<'a, 'b>(&'a self, key: &'b str) -> &'a str
fn get(&self, key: &str) -> &str { todo!() }
}What does 'static mean?
Answer
'static shows up in two different contexts, and it's important not to mix them up.
1. The 'static lifetime on a reference. &'static T points to data that stays valid for the entire run of the program. String literals have type &'static str because they're embedded in the binary's read-only data section (.rodata). References to static items are 'static too, and so are references produced by Box::leak.
let s: &'static str = "hello";
let leaked: &'static mut String = Box::leak(Box::new(String::from("dyn")));2. The T: 'static bound. This means "T contains no references shorter than 'static." It does not mean the value lives forever: it can be dropped at any moment. It just means the value doesn't borrow anything that could go away underneath it.
// Same bounds as std::thread::spawn
fn spawn<F: FnOnce() + Send + 'static>(f: F) { /* ... */ }Here 'static doesn't mean "the closure lives forever"; it means "the closure doesn't capture any short-lived references." A String satisfies T: 'static because it owns its data.
A common mistake: declaring fn make() -> &'static str and trying to return a string built at runtime. The fix is to return a String, or to use Box::leak if you're fine with that memory never being freed.
fn make(value: i32) -> &'static str {
&format!("{}", value) // E0515: cannot return reference to temporary value
}What is NLL, and how does the borrow checker work today?
Answer
NLL (Non-Lexical Lifetimes) shipped with the Rust 2018 edition (Rust 1.31) and was effectively a rewrite of the borrow checker. It was later enabled for the 2015 edition as well, and the old checker was removed entirely in Rust 1.63.
Before NLL, a borrow lasted until the end of its lexical scope: a reference was considered alive until the closing brace, even if it was never used again. This caused many false rejections of perfectly valid code.
let mut v = vec![1, 2, 3];
let r = &v[0];
println!("{r}");
v.push(4); // error before NLL; fine with NLL, since `r` is no longer usedToday the borrow checker runs on MIR (the compiler's mid-level intermediate representation). It performs a dataflow analysis over the control-flow graph: a borrow is live from where it's created to its last use, and the checker verifies that no conflicting borrows are live at the same point.
The next step is Polonius, a new borrow-checking algorithm (originally prototyped in Datalog). It accepts some patterns that NLL still rejects, most notably conditionally returning a borrow out of a function (the famous "NLL problem case #3"). At the time of writing, Polonius is still unstable and available only behind -Zpolonius on nightly.
If code looks correct but NLL rejects it, the usual workarounds are introducing an explicit { ... } block or moving part of the logic into a separate function so the borrow ends earlier.
What does Box do?
Answer
Box<T> is an owning pointer to a heap allocation. For a sized T, it's a single machine word: a pointer to memory obtained from the global allocator. For unsized types (Box<[T]>, Box<dyn Trait>), it's a fat pointer of two words.
When a Box is dropped, it runs T's destructor and then returns the memory to the allocator. For zero-sized types, Box doesn't allocate at all.
When do you need Box?
Answer
Recursive types. enum List { Cons(i32, Box<List>), Nil }. Without Box, the type would have infinite size and the compiler couldn't compute its layout.
Large values. Moving a Box<[u8; 1_000_000]> copies a single pointer, not a megabyte. Beware, though: Box::new([0u8; 1_000_000]) may build the array on the stack first and then move it to the heap, which can overflow the stack in debug builds. vec![0u8; N].into_boxed_slice() avoids that.
Trait objects (dynamic dispatch). Box<dyn Trait> is a fat pointer (data pointer + vtable pointer), which lets you store different implementations in the same collection.
Returning different concrete types behind one type. If a function returns one of several types implementing a trait, impl Trait won't work because it requires a single concrete type. Box<dyn Trait> solves this. The same applies to recursive async fns, which need Box::pin because a future can't contain itself.
trait Shape { fn area(&self) -> f64; }
struct Circle(f64);
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
}
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0))];What Box does not give you: shared ownership (use Rc/Arc), mutation through a shared reference (use Cell/RefCell), or synchronization between threads (use Arc with Mutex/RwLock). Box<T> is Send/Sync only if T is.
Rc vs. Arc: what's the difference, and when should you use each?
Answer
Rc<T> and Arc<T> both provide shared ownership through reference counting. The difference is how the counter is updated.
Rc uses plain, non-atomic integer operations. This makes cloning and dropping cheaper, but it isn't safe to share across threads. Rc<T> is neither Send nor Sync, so passing it to thread::spawn is a compile-time error.
Arc ("atomically reference counted") uses atomic operations on its counters (in the current std implementation, clone uses Ordering::Relaxed and drop uses Ordering::Release). Arc<T> is Send and Sync when T: Send + Sync. Both increments and decrements are atomic, so they cost more than Rc's, and the overhead grows under contention when many threads touch the same counter (cache-line bouncing).
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let d2 = Arc::clone(&data);
thread::spawn(move || println!("{d2:?}")).join().unwrap();Both types keep two counters internally: strong and weak. The value is dropped when the strong count reaches zero; the allocation itself is freed when the weak count reaches zero too.
Weak references exist to break cycles. Rc::new_cyclic and Arc::new_cyclic let you construct a value that holds a Weak reference to itself.
When to use which:
- Single-threaded code:
Rcby default. - Data shared across threads:
Arc. - Inside
tokio::spawn, the future must beSend + 'static, so shared data goes in anArc. - Immutable, read-heavy data:
Arcworks great. - Shared data that needs mutation:
Arc<Mutex<T>>orArc<RwLock<T>>(orRc<RefCell<T>>in single-threaded code).
Pitfall: a cycle of Rcs or Arcs leaks memory. If A holds an Rc<B> and B holds an Rc<A>, neither strong count ever reaches zero. A tree whose children point back to their parents is the classic example; the fix is to use Weak for the parent links.
What is interior mutability, and which types provide it?
Answer
Interior mutability is a pattern where a value is mutated through a shared reference &T. It appears to break the aliasing XOR mutability rule at the API level, but safety is preserved by other means: runtime checks, synchronization primitives, or an API that never hands out references to the inner value.
Cell<T> wraps an UnsafeCell and exposes get/set/replace/take without ever handing out references to the contents. No runtime checks are needed, since without references there's nothing to alias. get requires T: Copy, but set, replace, and swap work with any T. Cell is !Sync.
RefCell<T> works with any type. It hands out Ref<T> and RefMut<T> guards via borrow and borrow_mut and tracks borrows at runtime. Violating the XOR rule (for example, calling borrow_mut while another borrow is active) panics; try_borrow/try_borrow_mut return a Result instead. RefCell is !Sync, so it's single-threaded only.
Mutex<T> and RwLock<T> are the thread-safe counterparts. They block the calling thread while waiting for the lock and implement Sync.
OnceCell<T> / LazyCell<T> (single-threaded) and OnceLock<T> / LazyLock<T> (thread-safe) handle one-time initialization.
Atomic types (AtomicBool, AtomicUsize, AtomicPtr, etc.) provide lock-free mutation of integers, booleans, and pointers.
use std::cell::RefCell;
use std::collections::HashMap;
struct Cache {
map: RefCell<HashMap<String, String>>,
}
impl Cache {
fn get_or_insert(&self, k: &str, v: String) -> String {
let mut m = self.map.borrow_mut();
// We return a clone: a reference can't outlive the `RefMut` guard
m.entry(k.into()).or_insert(v).clone()
}
}Where it's useful: types that look immutable from the outside but cache something internally (memoization, lazy initialization), and graphs or trees that need local updates through shared pointers (Rc<RefCell<Node>>).
All of these types are built on top of UnsafeCell<T>, the only primitive in the language that allows mutating data behind a shared reference. UnsafeCell::get returns a *mut T, and using it directly requires unsafe code that upholds the aliasing rules by hand.
What is Cow?
Answer
Cow<'a, B> (clone-on-write) is an enum with two variants: Borrowed(&'a B) and Owned(<B as ToOwned>::Owned). The idea: as long as you don't need to modify the data, you hold a borrowed reference; once you do, to_mut clones the data into the Owned variant (only if it isn't owned already). into_owned extracts an owned value, cloning only when necessary.
Where it's used in practice:
Parsing and normalization. If the input is already valid, return Cow::Borrowed(input) with no allocation. If it needs fixing, switch to Owned.
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<'_, str> {
if s.contains('\r') {
Cow::Owned(s.replace('\r', ""))
} else {
Cow::Borrowed(s)
}
}APIs that may return either borrowed or owned data. String::from_utf8_lossy and Path::to_string_lossy return Cow<str>: Borrowed if the input is valid UTF-8, otherwise a newly allocated string with invalid sequences replaced by U+FFFD.
Zero-copy deserialization. serde can deserialize into Cow<'a, str>, borrowing from the input buffer when the string has no escape sequences and allocating only when it does. Note that this requires the #[serde(borrow)] attribute on the field; without it, Cow always deserializes as Owned.
Where it doesn't help: if the hot path almost always needs to modify the data, Cow just adds a branch. When you know you'll need a String, just use a String.
What is Drop, and can you call it manually?
Answer
Drop is a trait with a single method, fn drop(&mut self). The compiler calls it automatically when a value goes out of scope (unless the value was moved out earlier). Destructors run on normal exit and during unwinding (with panic = "unwind"), but not after mem::forget, on process abort (including panic = "abort"), on std::process::exit, or for values leaked through Rc/Arc cycles. Running destructors is therefore not a safety guarantee: leaking memory is considered safe in Rust.
You can't call the trait method x.drop() directly: that's error E0040. If you could, the compiler would still insert its own call at the end of scope, and the destructor would run twice. To drop a value early, use the free function std::mem::drop(x) (it's in the prelude). Its implementation is literally an empty function: it takes the value by move, and the value is dropped when the function returns.
struct Guard;
impl Drop for Guard {
fn drop(&mut self) { println!("bye"); }
}
fn main() {
let g = Guard;
drop(g); // the destructor runs here
println!("after");
}Drop order:
- Struct fields are dropped in declaration order, after the struct's own
Drop::dropruns. - Local variables are dropped in reverse order of declaration.
This matters for RAII. A guard created after the resource it borrows is dropped before that resource, which is exactly what you want (and the borrow checker enforces it). For struct fields, the order is up to you: if one field must be dropped before another (for example, a handle before the context it depends on), declare it first, or use ManuallyDrop to control the order explicitly.
Avoid panicking in Drop::drop. If a destructor panics while the thread is already unwinding from another panic, the process aborts. That's why destructors shouldn't call .unwrap() or perform fallible operations; types like BufWriter ignore errors in drop and offer an explicit method (flush) for callers who care.
Interaction with moves: after drop(x), the binding x is moved-from and can't be used anymore. And, as mentioned above, a type that implements Drop can't be Copy.
Moves in closures and Fn, FnMut, FnOnce
Answer
A closure is an anonymous type that captures variables from its environment and implements one or more of the Fn* traits. Which traits it implements depends on what the body does with the captured values:
FnOnce: can be called at most once. Every closure implements it. A closure that moves a captured value out (for example, drops it or returns it) implements onlyFnOnce.FnMut: can be called multiple times and may mutate captured state. Calling it requires&mutaccess to the closure.Fn: can be called multiple times through a shared reference&, and doesn't mutate captured state.
The traits form a hierarchy: Fn: FnMut and FnMut: FnOnce. A closure that implements Fn can be passed anywhere an FnMut or FnOnce is expected.
let s = String::from("hi");
let f1 = || println!("{s}"); // Fn: captures `s` by &
let mut v = vec![1];
let mut f2 = || v.push(2); // FnMut: captures `v` by &mut
let f3 = move || drop(s); // FnOnce only: consumes `s`By default, the compiler captures each variable in the least restrictive way possible: by &, then by &mut, then by value. The move keyword forces all captures to be by value. This is needed for thread::spawn and tokio::spawn, because the closure (or future) must be 'static and can't hold references into the spawning thread's stack. If you need to borrow local data from threads, use std::thread::scope instead.
Pitfall: move does not make a closure FnOnce. A move closure that only reads its captured values is still Fn. The traits a closure implements are determined by what its body does with the captures; move only changes how they're captured.
What is PhantomData, and when should you use it?
Answer
PhantomData<T> is a zero-sized marker type that tells the compiler: "treat this struct as if it contained a T," even though no T is actually stored. It has no runtime cost, but it affects variance, auto traits (Send/Sync), and drop checking.
The compiler rejects unused type and lifetime parameters on a struct (E0392), so PhantomData is how you "use" them.
Tying a lifetime to a struct that holds a raw pointer. Raw pointers carry no lifetime, so without PhantomData<&'a T> the struct would have no connection to the data it points to, and the borrow checker couldn't stop it from outliving that data.
use std::marker::PhantomData;
struct Slice<'a, T> {
ptr: *const T,
len: usize,
_marker: PhantomData<&'a T>,
}Type-state and typed IDs. You can make Id<User> and Id<Order> distinct types even though both are just a u64 inside. Mixing them up becomes a compile error, at zero runtime cost.
use std::marker::PhantomData;
struct Id<T> {
value: u64,
_t: PhantomData<T>,
}
struct User;
struct Order;Controlling variance and auto traits. The choice of PhantomData parameter matters:
| Marker | Variance in T | Send/Sync |
|---|---|---|
PhantomData<T> | covariant | inherited from T |
PhantomData<&'a T> | covariant | inherited from &T |
PhantomData<*const T> | covariant | neither Send nor Sync |
PhantomData<fn() -> T> | covariant | always Send + Sync |
PhantomData<fn(T)> | contravariant | always Send + Sync |
PhantomData<*mut T> | invariant | neither Send nor Sync |
These are tools for authors of unsafe code who know exactly which variance and thread-safety properties they want. For example, PhantomData<fn() -> T> is handy for typed IDs: the type is covariant, doesn't claim to own a T, and stays Send + Sync regardless of T.
What it doesn't do: PhantomData<T> never runs T's destructor, because there's no T to destroy. If your type logically owns T values through a raw pointer (like Vec<T> does), your Drop impl has to drop them manually (e.g. with ptr::drop_in_place). PhantomData<T> then tells the drop checker that dropping your type may drop T values.
Borrow vs. AsRef vs. Deref
Answer
All three let you get a reference to something, but they solve different problems and come with different contracts.
Deref is transparent dereferencing. By implementing Deref<Target = T>, you declare that your type is a smart pointer to T. The compiler then applies deref coercion: &Box<T> implicitly becomes &T, and &String becomes &str. Method resolution also follows the deref chain. Deref is meant for smart pointers only; implementing it on arbitrary wrappers to "inherit" methods is a well-known anti-pattern.
let s = String::from("hi");
let r: &str = &s; // deref coercionAsRef<T> is a cheap reference-to-reference conversion. It carries no semantic guarantees beyond that, and a type can implement it for several targets: String implements AsRef<str>, AsRef<[u8]>, AsRef<OsStr>, and AsRef<Path>. It's used in generic APIs: fn read<P: AsRef<Path>>(path: P) accepts &str, String, PathBuf, or anything else that can be viewed as a &Path.
Borrow<T> is similar to AsRef, but with a stronger contract: Eq, Ord, and Hash must behave identically on the owned value and on its borrowed form. That's what makes HashMap::get work with a borrowed key: a HashMap<String, V> can be queried with a &str, because a String and its &str form are guaranteed to hash and compare the same way.
use std::collections::HashMap;
let mut m: HashMap<String, i32> = HashMap::new();
m.insert("a".into(), 1);
m.get("a"); // lookup by &str works because String: Borrow<str>Rule of thumb:
- Writing a smart pointer:
Deref. - Accepting anything string-like or path-like in an API:
AsRef<str>orAsRef<Path>. - Collection keys that need lookup by a borrowed form:
Borrow.