Move Semantics
You have one rare baseball card. The Topps 1952 Mickey Mantle, the only one in the binder, the one that paid for the binder. You hand it to your friend so they can see it. The card is now in their hand. It is not in your binder. You cannot show it to a second friend while the first friend is still holding it, because there is only one card. This is exactly how Rust treats a value on the heap. The value lives in one place. The name on top of it is the binder slot. When you write let b = a, you are not making a copy — you are sliding the card out of slot a and into slot b. Slot a is now empty, and the compiler will refuse to let you reach for it.

The reason Rust works this way comes from a problem that ate C and C++ programmers alive for forty years. Two names pointing at the same heap allocation meant two pieces of code could free it, and the second free crashed the program — or worse, freed memory that something else had already taken over. Henry Baker wrote about linear types in 1992 as a way out, and a group at AT&T and Cornell built a language called Cyclone in 2002 that gave each heap value exactly one owner. When Graydon Hoare started Rust at Mozilla in 2006, he made the single-owner rule the default for every value bigger than a number. No garbage collector, no double-free, no debate — the binder slot rule was baked in from day one.
Here is the move in code. A String is the rare card — its letters live on the heap, and the variable on the stack is the slot label.
fn show_move() {
let a = String::from("Topps 1952 Mantle");
let b = a;
println!("--- move ---");
println!("before: a -> heap, b -> (nothing)");
println!("after: a -> (gone), b -> heap");
println!("b holds: {b}");
}After let b = a, the slot a is gone. If you try to print a on the next line, the compiler stops you with error[E0382]: borrow of moved value: a. It is not a runtime error and it is not a warning. The program does not compile. Rust is doing exactly what you would do if your friend already had the card — it tells you the slot is empty before you embarrass yourself reaching for it.
--- move ---
before: a -> heap, b -> (nothing)
after: a -> (gone), b -> heap
b holds: Topps 1952 Mantle
--- clone ---
before: a -> heap#1, b -> (nothing)
after: a -> heap#1, b -> heap#2
a holds: Topps 1952 Mantle
b holds: Topps 1952 Mantle
--- borrow ---
before: a -> heap, b -> (nothing)
after: a -> heap, b ----> a
a holds: Topps 1952 Mantle
b looks at: Topps 1952 Mantle
--- copy ---
before: a = 42 on stack, b = (nothing)
after: a = 42 on stack, b = 42 on stack
a holds: 42
b holds: 42Notice the binder language in the output. Before the move, slot a points at the heap; slot b points at nothing. After the move, slot a is gone and slot b points at the same heap allocation the card lived in. Nothing got copied — the pointer just slid from one slot to the other. The heap allocation was never touched.
Now the three ways out. The first one is the photocopy. If you actually want two cards, you tell Rust to make a real duplicate on the heap and put it under slot b. The method is called clone, and it costs you whatever the value costs to copy — for a short string that is fast, for a million-element vector that is slow. Rust makes you write .clone() so the cost is visible. You never get a hidden copy.
fn show_clone() {
let a = String::from("Topps 1952 Mantle");
let b = a.clone();
println!("--- clone ---");
println!("before: a -> heap#1, b -> (nothing)");
println!("after: a -> heap#1, b -> heap#2");
println!("a holds: {a}");
println!("b holds: {b}");
}The output shows two separate heap allocations now — heap#1 and heap#2. Both slots are valid. Both can be printed. You bought yourself a second card, and you paid for it in memory and time.
The second way is the borrow. Your friend wants to see the card. They do not want to keep it. You hand them the card with the understanding that they will give it back. In Rust, you write &a and it gives you a reference — a pointer that knows it does not own the thing it points at. The original slot a still owns the card. The borrowed slot b just looks at it.
fn show_borrow() {
let a = String::from("Topps 1952 Mantle");
let b = &a;
println!("--- borrow ---");
println!("before: a -> heap, b -> (nothing)");
println!("after: a -> heap, b ----> a");
println!("a holds: {a}");
println!("b looks at: {b}");
}The arrow in the output is the key. Slot a still points at the heap. Slot b points at slot a, which points at the heap. When the function ends, slot b disappears with no fuss, and slot a cleans up the heap allocation the way it always would have. References are how almost every real Rust function takes a value it does not need to own — it borrows, looks, and gives back.
The third way out is the one beginners trip over. Small things on the stack — integers, characters, booleans — do not get moved. They get copied. When you write let b = a for an i32, Rust copies the four bytes from one stack slot to another and both slots are valid. The compiler decides this for you based on whether the type implements a trait called Copy. Plain numbers are Copy. Anything that owns a heap allocation is not.
fn show_copy() {
let a: i32 = 42;
let b = a;
println!("--- copy ---");
println!("before: a = 42 on stack, b = (nothing)");
println!("after: a = 42 on stack, b = 42 on stack");
println!("a holds: {a}");
println!("b holds: {b}");
}The line between Copy and not-Copy is the line between "small and on the stack" and "big and on the heap." A four-byte integer is so cheap to duplicate that Rust does it silently. A String could be a megabyte, so Rust makes you ask for the duplicate by name. The rule is consistent — the compiler will never silently spend more than a few stack bytes on your behalf.

One question worth asking — why pick a default that surprises people who come from Python or Java? Because the alternative is what those languages do. Python and Java let you have ten names pointing at the same object and rely on a garbage collector to figure out when nobody is using it anymore. The garbage collector takes runtime cycles, freezes the program at unpredictable moments, and rules out using Rust on a microcontroller or inside a kernel. Hoare wanted a language that ran in the same places C ran, and the only way to get rid of the garbage collector was to make ownership obvious at compile time. The binder slot rule pays that bill.
Next lesson — what really happens when you write &a and how Rust stops two borrowers from fighting over the same card at once.