Why this matters
If let s2 = s1; moves a String, why does let y = x; with an integer leave x perfectly usable? And when you really do need two independent copies of a String, how do you get them? The answers are two traits, Copy and Clone. Understanding the difference tells you which assignments are free, which are expensive, and why Rust insists on making the expensive ones visible.
Integers do not move
fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
let p = (1.5, 'z', true);
let q = p;
println!("{:?} {:?}", p, q);
}
x = 5, y = 5
(1.5, 'z', true) (1.5, 'z', true)
No E0382. An i32 is four bytes on the stack with no heap memory attached. Copying those four bytes produces a complete, independent value; there is nothing that could be freed twice. So for such types, Rust copies on assignment and leaves the original valid. These types implement the Copy trait. (A trait is a named capability a type can have; module 10 covers them in depth. For now, "implements Copy" means "duplicates itself on assignment".)
let x = 5; let y = x; let s1 = String::from("hi"); let s2 = s1;
x | 5 | y | 5 | s1 | (moved) | s2 | ptr -----> [h|i]
| len 2 |
both valid, fully independent | cap 2 |
one owner of the heap buffer
Both cases do the same thing at the machine level: copy the bytes of the stack value. The only difference is whether the compiler lets you use the source afterwards, and that depends on whether the bytes alone are the whole value.
Which types are Copy
- all integer types, all float types,
bool,char - shared references
&T, including&str(copying a pointer does not copy what it points to, and a shared reference owns nothing; module 5) - tuples and arrays, if all their elements are Copy:
(i32, f64)and[u8; 16]are Copy;(i32, String)is not - your own types, if you opt in and all their fields are Copy (module 7)
Not Copy: anything that owns a resource and needs cleanup: String, Vec, Box, files, network connections. The rule is strict: a type that needs to run cleanup code when dropped can never be Copy. Otherwise a bitwise copy would produce two owners of one resource.
Clone: explicit deep copies
When you need a second, independent String, ask for it with .clone():
fn main() {
let s1 = String::from("hello");
let mut s2 = s1.clone();
s2.push_str(", world");
println!("s1 = {s1}");
println!("s2 = {s2}");
println!("s1 buffer {:p}, s2 buffer {:p}", s1.as_ptr(), s2.as_ptr());
}
s1 = hello
s2 = hello, world
s1 buffer 0x556232352b10, s2 buffer 0x556232352b30
clone allocates a new heap buffer, copies the bytes, and returns a new String that owns it:
s1 | ptr -----------> [h|e|l|l|o]
| len 5
| cap 5
two buffers at two addresses,
s2 | ptr -----------> [h|e|l|l|o|,| |w|o|r|l|d] two owners, each freed once
| len 12
| cap 12 (or more)
The two are completely independent: modifying s2 does not affect s1, and each frees its own buffer. Most standard types implement the Clone trait. Copy types are Clone too (cloning an i32 is just a copy).
The difference in one sentence: Copy is implicit and always cheap (a few bytes); Clone is explicit and may be expensive (proportional to the data). Rust never runs potentially expensive code behind a plain =. When you see .clone() in a program, you see a cost.
When to clone
Cloning is a legitimate tool. Use it when you genuinely need two independent values: keep the original while handing a modified copy elsewhere, or store the same name in two different collections. It is also fine while learning: a working program with a few clones beats a non-compiling one.
But treat each clone as a question. Often the code does not need a second copy at all:
- Reorder so the value is used before it is moved.
- Borrow instead of moving, so the function looks at the value without taking it (module 5). This is the usual answer, and why the next module exists.
- Move it if the original is not needed afterwards.
A clone of a 10-character string costs nanoseconds. A clone of a 100 MB buffer inside a loop is a performance bug. Rust's explicitness makes both easy to spot in review.
Copy semantics with functions
Passing a value to a function follows the same rules as assignment: Copy types are copied, others are moved.
fn bump(mut n: i32) -> i32 {
n += 1;
n
}
fn main() {
let a = 41;
let b = bump(a);
println!("a = {a}, b = {b}");
}
a = 41, b = 42
bump changed its own copy; a is untouched. Lesson 5 shows what happens when a String is passed.
Common mistakes
Expecting clone on a big value to be cheap. It copies everything.
Cloning to silence the compiler without thinking. Check whether a borrow would do.
Assuming a tuple is Copy because it is small:
fn main() {
let pair = (1, String::from("one"));
let other = pair;
println!("{:?}", pair);
}
error[E0382]: borrow of moved value: `pair`
--> main.rs:4:22
|
2 | let pair = (1, String::from("one"));
| ---- move occurs because `pair` has type `(i32, String)`, which does not implement the `Copy` trait
3 | let other = pair;
| ---- value moved here
4 | println!("{:?}", pair);
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let other = pair.clone();
| ++++++++
One non-Copy element makes the whole tuple non-Copy: the label says the type (i32, String) does not implement Copy. (rustc also warns that other is unused; that warning is omitted here.)
Expecting a modification through a copy to affect the original. With Copy types, let mut y = x; y += 1; never changes x. That is a feature, but sometimes surprising when you wanted shared state; that is what references are for.
Recap
- Types with no resources to free (numbers,
bool,char,&T, tuples and arrays of those) areCopy: assignment and passing duplicate them, and the original stays valid. - Types that own heap memory move instead; they can never be Copy.
.clone()makes an explicit deep copy: new allocation, copied data, independent owner.- Copy is implicit and cheap; Clone is explicit and may be expensive. Rust never deep-copies behind
=. - Before cloning, consider reordering, moving or borrowing.
Going further: print the addresses ({:p} with as_ptr()) of a String before and after moving it into another variable. They are the same: a move never touches the heap data. Then do the same with clone.