When you need a real copy, not a moved one
You write a function that takes a configuration struct, tweaks a setting, and returns the result. You pass your struct in, the function runs, and then you try to log the original variable. The compiler stops you with value used here after move. In Python or JavaScript, passing an object to a function leaves the original perfectly intact. Rust takes the value by default. This is the first time most developers feel the weight of ownership. The fix usually involves either borrowing the value with a reference or handing the function its own duplicate. This article covers the duplication path. You will learn how to make a copy of a struct, why Rust splits copying into two separate traits, and how to choose between them without guessing.
Two traits, two stories
Rust divides duplication into two distinct traits. The names are not interchangeable. Clone represents an explicit, potentially expensive duplication. You call it with .clone(), and the call site signals that you are asking for a duplicate and accept the cost. Cloning a String allocates a fresh heap buffer and copies every byte. Cloning a Vec does the same. If a struct contains a String, cloning the struct recursively clones the inner string. It is a deep copy by default.
Copy represents a cheap, implicit, bit-for-bit duplication. Integers, floats, booleans, and char all implement it. When a type is Copy, assigning it to a new variable or passing it to a function leaves the original completely valid. The compiler quietly copies the bytes into a new register or stack slot. There is no heap allocation, no destructor logic, and no method call overhead.
Think of Copy like writing a number on a sticky note. You can tear off as many copies as you want. They are independent, they cost nothing to reproduce, and tearing one off never destroys the original. Clone is like commissioning a replica of a physical model. You have to ask for it, it takes time and materials to build, and the result is a separate object that happens to look identical.
The compiler enforces a strict hierarchy. Every type that is Copy must also be Clone. The reverse is false. String is Clone but not Copy. Silently duplicating a String would copy the pointer to the heap buffer, not the buffer itself. Two variables would claim ownership of the same memory. When one goes out of scope, it frees the buffer. The other becomes a dangling pointer. Rust refuses to allow that pattern. Copy is reserved for types that are fundamentally a fixed bag of bytes with no cleanup logic. Clone is the general-purpose mechanism for everything else.
The minimal example
Here is the smallest working case: a struct holding two integers, both of which are Copy types.
// Derive both traits. The compiler generates the implementation automatically.
#[derive(Clone, Copy, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
// p1 occupies 8 bytes on the stack.
let p1 = Point { x: 3, y: 4 };
// Assignment triggers a bitwise copy because Point implements Copy.
// p1 remains fully valid and usable.
let p2 = p1;
// Both variables print correctly. No move occurred.
println!("{:?} and {:?}", p1, p2);
}
The #[derive(Clone, Copy, Debug)] attribute does the heavy lifting. The compiler writes the Clone::clone method and marks the type as Copy. You cannot derive Copy without Clone. The compiler will reject #[derive(Copy)] alone and remind you to add Clone. This is a deliberate design choice. The Copy trait is technically a subtrait of Clone, meaning the compiler expects the clone method to exist even if it never calls it.
If you attach a String field to this struct, the derive fails immediately.
error[E0204]: the trait `Copy` may not be implemented for this type
--> src/main.rs:1:17
|
1 | #[derive(Clone, Copy)]
| ^^^^
2 | struct Person {
3 | name: String,
| ----------- this field does not implement `Copy`
The compiler protects you from double-free bugs. String owns a heap buffer. A bitwise copy would create two String instances pointing at the same memory. The compiler blocks Copy for any type containing a non-Copy field.
Trust the compiler here. If it refuses Copy, the type owns resources that require careful cleanup.
What happens at compile and runtime
When you mark a struct as Copy, the compiler changes how it treats assignments and function calls. Instead of generating code that transfers ownership, it inserts a memcpy instruction at the machine code level. The original variable stays in its register or stack frame. The new variable gets its own slot with identical bytes. At runtime, this is indistinguishable from passing an integer. There is zero overhead.
When you use Clone, the compiler generates a method call. That method walks through every field of the struct and calls .clone() on it. If a field is a Vec, the method allocates a new contiguous array on the heap, copies the pointers to the elements, and increments the reference counts of any inner Rc or Arc wrappers. If a field is a String, it allocates a new buffer and copies the UTF-8 bytes. The runtime cost scales with the size of the data. The compiler does not optimize away the allocation because the data might change independently after the clone.
This distinction shapes how you design APIs. Copy types flow through your program like water. They pass through function boundaries without friction. Clone types require a deliberate pause. The developer must decide whether the duplication is worth the allocation. The compiler forces that decision by requiring the explicit method call.
A realistic example
Most custom structs contain heap-owning fields. They cannot be Copy, but they can be Clone. Here is a configuration struct that needs duplication.
// Derive Clone alone. Copy is impossible due to String and Vec.
#[derive(Clone, Debug)]
struct AppConfig {
name: String,
timeout_secs: u32,
features: Vec<String>,
}
// Takes a reference, clones internally, and returns a modified owned value.
fn with_feature(config: &AppConfig, flag: &str) -> AppConfig {
// Deep clone: allocates a new Vec and new String buffers for each element.
let mut new_config = config.clone();
// Mutating the clone leaves the original reference untouched.
new_config.features.push(flag.to_string());
new_config
}
fn main() {
let base = AppConfig {
name: "prod".into(),
timeout_secs: 30,
features: vec!["auth".into(), "metrics".into()],
};
// Pass a reference. The function clones when it needs ownership.
let extended = with_feature(&base, "tracing");
// base remains valid and unchanged.
println!("base: {:?}", base);
println!("extended: {:?}", extended);
}
This pattern is standard in Rust codebases. Take an immutable reference, clone inside the function when you need to produce a modified version, and return the owned result. The cost is visible at the clone call site. If profiling later reveals this is a hot path, you have an obvious target to refactor toward in-place mutation or a builder pattern.
Convention note: the community prefers explicit .clone() calls over implicit duplication. The visible method call acts as a performance marker. When you grep your codebase for .clone(), you instantly see where memory allocation happens. Do not hide it behind helper functions unless the helper is strictly about cloning.
Common pitfalls
Deriving Clone fails when a field lacks the trait. If you attach a custom handle or a file descriptor to your struct, the compiler emits E0277 (trait bound not satisfied). It points directly to the offending field. You must either implement Clone for that field type or accept that the outer struct cannot be cloned.
Beginners often reach for .clone() to silence borrow-checker errors. Sometimes that is correct. Often it is a workaround for a design that should use references. Cloning a Vec<String> just to read a single element wastes memory and CPU cycles. Borrowing the vector costs nothing and keeps the data in one place.
Removing Copy from a type changes its public API. If a function takes a Point by value and Point is Copy, callers can reuse their original variable. If you strip Copy and keep only Clone, the same function signature now moves the value. Callers will hit E0382 (use of moved value) and must update their code. Adding or removing Copy is a breaking change for downstream users.
Treat the Copy trait as a contract. If the type can be safely duplicated with a single CPU instruction, mark it Copy. If it requires allocation or custom logic, stick to Clone.
When to reach for what
Use Copy for small, fixed-size types that require no cleanup: coordinates, numeric IDs, enums with no payloads, and simple flags. The user should never have to think about duplication. Adding Copy makes the type behave exactly like an integer.
Use Clone when your struct owns heap memory (String, Vec, Box, Rc) or when duplication requires explicit logic. Most custom application structs belong here. The visible .clone() call documents the allocation cost.
Use a reference (&T) when you only need to read the data or pass it through a function without taking ownership. Start function parameters as references. Switch to owned values only when the callee must outlive the caller or mutate the data in place.