Ownership: Moves, Copy vs Clone
Rust's boldest idea is that every value has exactly one owner — understanding what happens when that owner changes unlocks the rest of the language.
Every value you create in Rust needs somewhere to live, and eventually needs to stop living there — someone has to free that memory. Garbage-collected languages solve this by scanning for unreachable values at runtime; C and C++ solve it by trusting you to call free() at exactly the right moment, which is exactly the trust that produces use-after-free and double-free bugs. Rust's answer is ownership, and it's enforced entirely at compile time through three simple rules: every value has exactly one owner at any given moment; when that owner goes out of scope, the value is dropped; and ownership can move from one variable to another, but it can never be shared without your explicit permission.
Rule two is worth sitting with, because it's what replaces both the garbage collector and the manual free() call. When a variable goes out of scope — when its enclosing block, function, or {} ends — Rust automatically calls a special method named drop on its value, which releases whatever resources that value owns: heap memory, a file handle, a network socket. You never write this call yourself; the compiler inserts it deterministically, at a point it can prove is correct, which is why Rust code has no GC pauses and no need for a finally block just to clean up a resource.
Now consider what happens when you assign one variable to another. If s1 is a String — a heap-allocated, growable piece of text — and you write let s2 = s1;, Rust does not copy the heap buffer. It transfers ownership from s1 to s2, and then it deliberately treats s1 as no longer valid: any attempt to use s1 afterward is a compile error, not a runtime surprise. This is the crucial difference from a naive shallow copy in C, where both variables would end up pointing at the same heap memory — and when both eventually go out of scope, that memory gets freed twice, corrupting your program. Rust's move semantics make that scenario impossible to even compile.
So why does the exact same-looking code behave differently for i32? Because i32 implements a special marker trait called Copy. Types that are small, fixed-size, and live entirely on the stack — integers, floats, bool, char, and tuples of Copy types — opt into Copy, which means assignment duplicates the bits instead of moving ownership. Both the original and the new variable remain independently valid and usable, because duplicating a few stack bytes is essentially free and there's no shared heap resource that two owners could double-free. String, Vec<T>, and most types that own heap allocations cannot implement Copy, precisely because a cheap bitwise duplicate would leave two owners responsible for freeing the same memory.
When you genuinely want two independent, fully-owned copies of heap data — not just to read the same thing twice, which is what references in the next lesson are for — Rust makes you ask for it explicitly with .clone(). This is a deliberate design choice: deep-copying a large String or Vec is a real, sometimes expensive, runtime cost, and Rust never wants a cost like that to be invisible in your source code. If you see .clone() in someone's code, you immediately know a real, independent copy was made on purpose — you never have to wonder whether an innocuous-looking = secretly did the same.
Trying to use a value after it's been moved is one of the very first compiler errors every Rust learner meets, so it's worth seeing it laid out plainly. Given let s1 = String::from("hello"); let s2 = s1;, any later line that reads s1 fails with error[E0382]: borrow of moved value: s1, because ownership already transferred to s2 on the previous line. The fix depends on what you actually need: if you only ever meant to use one name for the value, just use s2 from then on; if you genuinely need both s1 and s2 to remain valid and independent, call s1.clone() instead of moving, accepting the cost of an explicit deep copy in exchange for two owners.
One more place ownership quietly moves is at a function call boundary: passing a String into a function by value moves it into that function's parameter, and the caller loses access to it afterward, exactly as if you'd written let param = my_string; yourself. That's often not what you want — most functions just need to read or briefly use a value, not take permanent ownership of it — and cloning everything just to avoid a move would be wasteful and would hide real allocation costs everywhere. That tension is exactly what the next lesson, references and borrowing, exists to resolve.
fn main() {{let s = String::from("hello");println!("{} is alive here", s);}println!("s is no longer accessible in this scope");}
The inner block gives s its own scope; the moment that block ends, Rust calls drop on s automatically and frees its heap buffer, with no garbage collector involved.
fn main() {let a = 5;let b = a;println!("a = {}, b = {}", a, b);let s1 = String::from("hello");let s2 = s1;println!("s2 = {}", s2);}
i32 implements Copy, so a and b are both independently valid after the assignment; String does not, so the second assignment moves ownership from s1 into s2 instead of copying it.
fn main() {let s1 = String::from("hello");let s2 = s1;// Using s1 here would fail to compile:// println!("{}, world!", s1);// error[E0382]: borrow of moved value: `s1`// s1's ownership moved to s2 on the line above, so s1 is no longer valid.println!("{}, world!", s2);let s3 = String::from("hello");let s4 = s3.clone();println!("s3 = {}, s4 = {}", s3, s4);}
The commented-out line shows the use-after-move error you would get from reading s1 after it moved into s2; the working fix below it uses .clone() to create two fully independent Strings on purpose.
🧠 Check your understanding
0/1 · 0/1 answered1. Why does assigning let s2 = s1; move ownership when s1 is a String, but assigning let b = a; simply copies the value when a is an i32?