8.2 The Copy Trait: Implicit Bitwise Duplication
Right, let’s talk about Copy. You’ve probably seen it, that little trait that looks like this: #[derive(Copy, Clone)]. It seems simple, almost trivial. And in a way, it is. But its implications are massive, and misunderstanding them is a rite of passage for every new Rustacean. Consider this your cheat sheet to skipping that particular headache.
Fundamentally, Copy is a marker trait. It doesn’t require you to implement any methods. Its entire job is to tell the compiler: “Hey, for this type, please don’t do that whole ‘move’ thing. Just duplicate the bits in place whenever you would normally move it.”
Think of a move in Rust. When you do let x = y; for a non-Copy type, y is gone. Kaput. You can’t use it again. But if the type is Copy, that assignment—or any other operation that would normally be a move—becomes a simple, implicit bitwise duplication. The original is left perfectly intact and usable. It’s the difference between handing someone your only house key (a move) and handing them a cheap, perfect copy of that key (Copy). You still have your key.
What Copy Actually Means Semantically
Semantically, Copy represents a type where a bit-for-bit copy is also a valid independent value. This is true for primitive types like integers, booleans, and characters. An i32 is just 32 bits. Copying those bits gives you another, completely independent i32. There are no hidden pointers, no references to other data, no ownership of some external resource. The value is self-contained.
This is why Copy is a subset of Clone. If a type can be trivially duplicated by just copying its bits, it can certainly be duplicated by calling a method (which, for Copy types, should do exactly the same thing).
let age: i32 = 42;
let age_copy = age; // This is a Copy, not a move.
// We can still use the original because it was copied, not moved.
println!("Original: {}, Copy: {}", age, age_copy);
Trying this with a String, which is not Copy, would rightly anger the compiler.
The Rules of Engagement: Auto-Traits and Derives
You can’t just slap #[derive(Copy)] on any type. The compiler imposes strict, non-negotiable rules:
- Your type must consist entirely of
Copyfields. If a single field in your struct or enum isn’tCopy, then the whole type can’t be. It’s an all-or-nothing deal. - Your type must not require any custom destruction logic (a
Dropimplementation). This is the big one. If your type implementsDrop, the compiler assumes that when it goes out of scope, some special cleanup code must run. Now imagine your type isCopy. What happens when the copy goes out of scope? Does it run the destructor? And then what about the original? You’d have two entities trying to manage the same resource, a classic recipe for a double-free disaster. Rust nips this in the bud by makingCopyandDropmutually exclusive.
Here’s a type that can be Copy:
#[derive(Copy, Clone, Debug)]
struct Point {
x: f64,
y: f64,
}
let origin = Point { x: 0.0, y: 0.0 };
let another_origin = origin; // implicit copy
println!("{:?}", origin); // Still perfectly fine.
And here’s one that can’t be, breaking both rules:
// This will NOT compile.
#[derive(Copy)] // ILLEGAL.
struct ResourceGuard {
handle: std::fs::File, // File isn't Copy (Rule 1 broken)
}
impl Drop for ResourceGuard {
fn drop(&mut self) {
// ... close the file handle
}
} // Also makes Copy illegal (Rule 2 broken)
To Copy or Not to Copy? A Matter of Semantics
Just because a type can be Copy doesn’t mean it should be. This is a design decision with real consequences.
The primary downside of Copy is that it’s implicit. Every assignment, every function argument pass, every return value becomes a copy. For large types, this can silently become a performance footgun. A [f64; 16] is Copy. Passing it into a function will copy all 128 bytes onto the stack. That might be fine, but if it were a [f64; 1000], you’d probably want to pass that by reference to avoid a costly, hidden memcpy.
The best practice is to reserve Copy for small, primitive types where the semantics of “trivial duplication” make intuitive sense. A Point, a Complex number, a KeyCode—these are perfect candidates. A String, a Socket, or a DatabaseConnection are not. Their duplication should be explicit (via .clone()) so the programmer is aware of the potential cost.
The Clone-Copy Symbiosis
Notice we almost always see #[derive(Copy, Clone)]. Why? Because the implementation of Clone for a Copy type is utterly trivial: it should just return *self. The derived Clone implementation does exactly that. Manually implementing Clone for a Copy type is a waste of keystrokes; just derive it.
// This is what the derive macro generates for a Copy type.
impl Clone for Point {
fn clone(&self) -> Self {
*self // This dereferences and copies, which is exactly what we want.
}
}
So, the rule of thumb: if your type is Copy, its Clone implementation should be derived and will do nothing more than a bitwise copy. This maintains the important semantic distinction: Copy means implicit duplication, Clone means explicit duplication.