Ownership & Moves
One Owner at a Time
Section titled “One Owner at a Time”Every Rust value has exactly one owner. When you assign a value to another variable, or pass it into a function, what happens depends on whether the type implements Copy.
Non-Copy Types Move
Section titled “Non-Copy Types Move”Types that own heap memory — like String, Vec<i32>, or Box<i32> — do not implement Copy. When you assign one to another variable, the value is moved: the original binding becomes invalid.
// @expect-error E0382fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is moved into s2
println!("{}", s1); // ERROR: s1 was moved println!("{}", s2); // fine}Using s1 after the move is a compile-time error:
error[E0382]: borrow of moved value: `s1` --> src/main.rs:6:20 |3 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait4 | let s2 = s1; // s1 is moved into s2 | -- value moved here5 |6 | println!("{}", s1); // ERROR: s1 was moved | ^^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |4 | let s2 = s1.clone(); // s1 is moved into s2 | ++++++++The compiler’s own suggested fix is .clone() — and that is exactly the trap: reaching for .clone() every time the borrow checker complains is the single most common “smell” in AI-generated Rust. It silences the error, but it often means the code should have borrowed (&s1) instead of moving in the first place. Treat .clone() as a deliberate, explicit deep copy for when you truly need one — not as a reflex to make an error go away.
The same rule applies when you pass a value into a function:
fn consume(s: String) { println!("{}", s);} // s is dropped here
fn main() { let name = String::from("Rust"); consume(name); // name is moved // name is no longer valid here}What a Move Actually Does in Memory
Section titled “What a Move Actually Does in Memory”The error above raises an obvious question: if s2 = s1 is cheap, what actually happened to the bytes of "hello"? A String is not the text itself — it is a small, fixed-size header living on the stack: a pointer to a heap buffer, a length, and a capacity. Measured on this toolchain:
fn main() { println!("size_of::<String>() = {}", std::mem::size_of::<String>());}size_of::<String>() = 24Three usize fields (pointer, length, capacity) at 8 bytes each on a 64-bit target — 24 bytes total. The actual UTF-8 bytes (h, e, l, l, o) live separately, on the heap.
let s2 = s1; copies only that 24-byte header from s1’s stack slot into s2’s stack slot. It never touches the heap bytes at all — copying them would be wasteful, and it is exactly what .clone() is for when you actually want it.
flowchart TB
subgraph Stack
H1[s1 header - ptr len 5 cap 5 - marked moved, unusable]
H2[s2 header - ptr len 5 cap 5 - now the owner]
end
subgraph Heap
BUF[heap buffer - 5 bytes - h e l l o]
end
H1 -. compiler forbids reading this .-> BUF
H2 -->|owns it, will free it on drop| BUF This is also why the compiler must reject using s1 afterward, rather than just leaving it as a harmless duplicate pointer: both s1 and s2 would point at the very same heap buffer. If both were allowed to go out of scope, both destructors would try to free that same pointer — a double free. Instead, the compiler tracks s1 as “moved-from” and statically forbids every use of it, so only s2’s destructor ever runs.
Copy Types Are Duplicated
Section titled “Copy Types Are Duplicated”Simple scalar types stored entirely on the stack — integers, bool, char, floating-point numbers, and tuples of these — implement Copy. Assigning or passing them makes a bitwise copy; the original remains valid.
fn main() { let x: i32 = 42; let y = x; // x is COPIED, not moved println!("x={} y={}", x, y); // both valid}Common Copy types: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, char.
Copy vs Clone: Why You Cannot Always Derive Both
Section titled “Copy vs Clone: Why You Cannot Always Derive Both”Copy is a marker trait: it tells the compiler “assignment may duplicate this value bit-for-bit instead of moving it.” That is only sound when every field of a type is itself Copy — a type that owns a heap allocation, like String or Vec<T>, can never be Copy, because a bitwise duplicate would create two owners of the same heap buffer (the double-free problem from the previous section). Copy also requires Clone as a supertrait: every Copy type must also be Clone, since a bitwise copy is trivially a valid “clone.”
Trying to derive Copy on a struct that contains a non-Copy field is a compile error, not a warning:
// @expect-error E0204#[derive(Clone, Copy)]struct UserId { label: String, id: u32,}
fn main() { let u = UserId { label: String::from("alice"), id: 1 }; println!("{} {}", u.label, u.id);}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct UserId { | ^^^^^^3 | label: String, | ------------- this field does not implement `Copy`
error: aborting due to 1 previous errorThe fix is almost never “make String implement Copy” (it can’t, for the same double-free reason) — it is to drop the #[derive(Copy)] and keep only #[derive(Clone)], then call .clone() explicitly at the one or two call sites that genuinely need an independent copy.
Explicit Deep Copy with .clone()
Section titled “Explicit Deep Copy with .clone()”When you need a full independent copy of a heap-owning value, call .clone(). This is an explicit opt-in to a potentially expensive operation.
fn main() { let original = String::from("keep me"); let copy = original.clone(); // deep copy println!("original={} copy={}", original, copy);}Drop Order and RAII
Section titled “Drop Order and RAII”Every value with resources to release — heap memory, a file handle, a lock — can implement the Drop trait, and Rust calls drop automatically when the value’s owner goes out of scope. This pattern, where acquiring a resource and owning a value are the same act, is called RAII (Resource Acquisition Is Initialization): you never call a close() or free() method yourself, because the compiler already knows exactly where every value’s scope ends.
The order Drop runs in is a real, observable rule, not an implementation detail:
- Local variables drop in reverse declaration order (last declared, first dropped).
- A struct’s fields drop in declaration order (first field first).
- A temporary value with no name (like
Guard("...")used directly in an expression) drops at the end of the statement it appears in.
struct Guard(&'static str);
impl Drop for Guard { fn drop(&mut self) { println!("dropping {}", self.0); }}
struct Holder { first: Guard, second: Guard,}
fn main() { let _a = Guard("a (local, declared first)"); let _b = Guard("b (local, declared second)"); let h = Holder { first: Guard("h.first"), second: Guard("h.second") }; println!("holder fields: {} / {}", h.first.0, h.second.0); println!("temporary: {}", Guard("temporary (end of this statement)").0); println!("end of main");}holder fields: h.first / h.secondtemporary: temporary (end of this statement)dropping temporary (end of this statement)end of maindropping h.firstdropping h.seconddropping b (local, declared second)dropping a (local, declared first)The unnamed Guard drops immediately after the println! statement that created it — long before main ends. Then, at the true end of main, h’s fields drop first-to-second (declaration order), and only after that do the locals _a and _b drop, last-declared-first (_b before _a).
mem::take, mem::replace, and mem::swap
Section titled “mem::take, mem::replace, and mem::swap”Sometimes you need to move a value out of a place you do not own outright — a struct field, or a mutable reference — while leaving something valid behind (you cannot leave a struct field truly empty; every field must hold a value of its type at all times). The std::mem functions exist for exactly this:
use std::mem;
fn main() { // mem::take leaves the default value (empty String) behind let mut s1 = String::from("original"); let taken = mem::take(&mut s1); println!("taken={:?} s1={:?}", taken, s1);
// mem::replace swaps in a new value and gives back the old one let mut s2 = String::from("old value"); let old = mem::replace(&mut s2, String::from("new value")); println!("old={:?} s2={:?}", old, s2);
// mem::swap exchanges the contents of two bindings in place let mut a = String::from("a-value"); let mut b = String::from("b-value"); mem::swap(&mut a, &mut b); println!("a={:?} b={:?}", a, b);}taken="original" s1=""old="old value" s2="new value"a="b-value" b="a-value"mem::take(&mut place) moves place’s value out and leaves Default::default() behind — only works for types implementing Default, which String and Vec<T> do (empty string, empty vec). mem::replace(&mut place, new) is the general form: it leaves whatever new you supply, not just the type’s default. mem::swap(&mut a, &mut b) exchanges two values without either one ever being “moved-from” and invalid — useful for in-place algorithms that need to swap struct fields or vector elements.
Partial Moves
Section titled “Partial Moves”Moving one field out of a struct or tuple only invalidates that field — the rest of the value stays usable, though the struct as a whole can no longer be used or moved:
struct Pair { a: String, b: String,}
fn main() { let p = Pair { a: String::from("a"), b: String::from("b") }; let taken = p.a; // partial move: only p.a moves out println!("{} {}", taken, p.b); // p.b is still usable}The Structs lesson covers this in more depth, including how partial moves interact with struct update syntax (..base) — the short version here is enough to recognize the pattern when you see it.
Returning Ownership
Section titled “Returning Ownership”A function can give ownership back to the caller by returning the value:
fn take_and_give_back(s: String) -> String { s // ownership moves out of the function}
fn main() { let s1 = String::from("hello"); let s2 = take_and_give_back(s1); println!("{}", s2);}This is valid but verbose. In practice, you will almost always prefer borrowing (the next lesson) over moving-and-returning.
fn take_ownership(s: String) -> String { println!("inside take_ownership: {}", s); s // return ownership to caller}
fn main() { // String is NOT Copy — it moves let s1 = String::from("hello"); let s2 = take_ownership(s1); // s1 is moved println!("s2 now owns: {}", s2);
// i32 IS Copy — it is copied let x = 42; let y = x; println!("x={} y={}", x, y);
// .clone() makes an explicit deep copy let original = String::from("keep me"); let cloned = original.clone(); println!("original={} cloned={}", original, cloned);}Compiling…
Mental Model
Section titled “Mental Model”A move is a header transplant, not a data copy: for a heap-owning type, only the small fixed-size pointer/length/capacity record changes hands, and the compiler erases your ability to use the old name so there is never a moment where two names both claim the same heap buffer. Copy types skip this dance entirely because there is no heap buffer to double-own — a bitwise duplicate is always safe, so the compiler just does it silently instead of making you track which name is “current.”
Trade-offs
Section titled “Trade-offs”Moving by default is nearly free (copying a 24-byte header, not the underlying data) and rules out use-after-free and double-free at compile time — but it means passing a large heap value into a function by ownership when you only meant to read it forces an unnecessary design choice on every caller. .clone() buys back flexibility at a real, visible runtime cost (a full heap allocation and byte-for-byte copy), which is exactly why the language makes it an explicit method call instead of something that happens implicitly on assignment, the way it might in a language with reference semantics everywhere.
Common Misconceptions
Section titled “Common Misconceptions”- “
.clone()is the fix the compiler suggested, so it’s the right fix.” The suggestion is a fix that always works, not necessarily the best one. Most of the time a move error means the code should have borrowed (&s1) instead of taking ownership — reach for.clone()only when you genuinely need two independent, divergeable copies of the data. - “A struct with a
Stringfield just needs#[derive(Copy)]added and it’ll work like the small structs I’ve seen.”Copyrequires every field to beCopy, andString/Vec<T>/any heap-owning type never is —#[derive(Copy)]on such a struct iserror[E0204], not a warning you can ignore. - “
Droporder doesn’t matter as long as everything eventually gets cleaned up.” It matters whenever one value’s destructor depends on another still being valid — a struct holding a database connection and a struct holding a transaction on that connection need to drop in the right order, and Rust’s fixed rules (fields in declaration order, locals in reverse) are what make that order predictable instead of implementation-defined.
Reading AI-generated code
Section titled “Reading AI-generated code”The most common shape of the clone-to-silence smell is cloning an entire collection just to iterate over it read-only — the clone adds a full deep copy for no benefit, because iterating never needed ownership in the first place.
🐛 Spot the bug
fn total_length(names: &Vec<String>) -> usize { let cloned = names.clone(); // unnecessary — we only read it cloned.iter().map(|s| s.len()).sum()}
fn main() { let names = vec![String::from("Ada"), String::from("Grace")]; println!("{}", total_length(&names));}Show the answer
names is already a &Vec<String> — a read-only reference. Calling .clone() deep-copies every String in the vector (each one a separate heap allocation) just to call .iter() on the copy, when .iter() works identically on the original reference. The fix is to delete the .clone() line entirely and call names.iter() directly — nothing about the rest of the function needs to change.