Rust Clone vs Copy: Duplication Semantics
Rust's ownership system prevents data races and memory errors at compile time, but it comes with a learning curve. One of the first challenges developers encounter is understanding when values are...
Key Insights
- Copy enables implicit, bitwise duplication for stack-only types, while Clone requires explicit
.clone()calls and supports heap-allocated data - Copy types must be trivially copyable (no heap allocations or custom logic), making Copy a subset of Clone—every Copy type must also implement Clone
- Prefer references over cloning when possible; use Copy for small, simple types and Clone for complex types where explicit duplication makes ownership transfer clear
Introduction to Ownership and Duplication
Rust’s ownership system prevents data races and memory errors at compile time, but it comes with a learning curve. One of the first challenges developers encounter is understanding when values are moved versus copied, and how to intentionally duplicate data when needed.
Consider this common beginner mistake:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
println!("{}", s1); // Compile error: value borrowed after move
}
This fails because String owns heap-allocated data. When you assign s1 to s2, Rust moves ownership rather than copying the data. This prevents double-free errors, but it means s1 is no longer valid.
Rust provides two traits to handle duplication explicitly: Copy for cheap, implicit duplication and Clone for explicit, potentially expensive duplication. Understanding when and how to use each is fundamental to writing idiomatic Rust.
The Copy Trait: Implicit Duplication
The Copy trait marks types that can be duplicated by simply copying bits. When a type implements Copy, assignment creates a duplicate rather than moving ownership. This happens implicitly—no special syntax required.
All primitive types implement Copy:
fn main() {
let x = 5;
let y = x; // x is copied, not moved
println!("x: {}, y: {}", x, y); // Both are valid
let a = true;
let b = a; // bools are Copy
println!("a: {}, b: {}", a, b);
let c = 'z';
let d = c; // chars are Copy
println!("c: {}, d: {}", c, d);
}
Tuples and arrays of Copy types are also Copy:
fn main() {
let point = (3, 4);
let point_copy = point;
println!("{:?} and {:?}", point, point_copy); // Both valid
let bytes = [1u8, 2u8, 3u8];
let bytes_copy = bytes;
println!("{:?} and {:?}", bytes, bytes_copy); // Both valid
}
You can implement Copy for your own types, but only if all fields are Copy and the type doesn’t manage resources:
#[derive(Copy, Clone, Debug)]
struct Point {
x: i32,
y: i32,
}
fn process_point(p: Point) {
println!("Processing {:?}", p);
}
fn main() {
let p1 = Point { x: 10, y: 20 };
process_point(p1);
process_point(p1); // p1 is still valid because Point is Copy
}
Note that Copy requires Clone. This is because Copy is a marker trait with no methods—it piggybacks on Clone’s implementation. The compiler ensures that Clone for Copy types performs a bitwise copy.
The critical restriction: Copy types cannot contain heap allocations or implement Drop. If a type needs custom cleanup logic, implicit copying would be dangerous. This is why String, Vec, and Box cannot be Copy.
The Clone Trait: Explicit Duplication
Clone provides explicit duplication through the .clone() method. Unlike Copy, Clone can perform deep copies of heap-allocated data and execute arbitrary code during duplication.
Here’s Clone with heap-allocated types:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicit clone
println!("{} and {}", s1, s2); // Both valid
let v1 = vec![1, 2, 3];
let v2 = v1.clone(); // Clones heap data
println!("{:?} and {:?}", v1, v2);
}
For simple structs, derive Clone:
#[derive(Clone, Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let person1 = Person {
name: String::from("Alice"),
age: 30,
};
let person2 = person1.clone();
println!("{:?}", person1); // Still valid
println!("{:?}", person2);
}
For complex types, implement Clone manually:
struct Database {
connection: String,
cache: Vec<String>,
}
impl Clone for Database {
fn clone(&self) -> Self {
println!("Cloning database connection...");
Database {
connection: self.connection.clone(),
cache: self.cache.clone(), // Deep copy the cache
}
}
}
fn main() {
let db1 = Database {
connection: String::from("localhost:5432"),
cache: vec![String::from("cached_data")],
};
let db2 = db1.clone(); // Explicit and visible
}
Manual implementations let you customize cloning behavior—skip expensive fields, log operations, or implement copy-on-write semantics.
Copy vs Clone: Key Differences
| Aspect | Copy | Clone |
|---|---|---|
| Invocation | Implicit (automatic) | Explicit (.clone()) |
| Cost | Always cheap (bitwise) | Potentially expensive |
| Heap Data | Not allowed | Allowed |
| Drop Implementation | Cannot have Drop |
Can have Drop |
| Trait Requirement | Requires Clone |
Standalone |
| Use Case | Small, stack-only types | Complex types with resources |
Here’s a direct comparison:
// Copy type: implicit duplication
#[derive(Copy, Clone, Debug)]
struct CopyStruct {
value: i32,
}
// Clone-only type: explicit duplication
#[derive(Clone, Debug)]
struct CloneStruct {
data: String,
}
fn main() {
// Copy behavior
let c1 = CopyStruct { value: 42 };
let c2 = c1; // Implicit copy
println!("{:?} and {:?}", c1, c2); // Both valid
// Clone behavior
let cl1 = CloneStruct { data: String::from("hello") };
let cl2 = cl1.clone(); // Must be explicit
println!("{:?} and {:?}", cl1, cl2);
// This would fail: let cl3 = cl1; println!("{:?}", cl1);
}
Common Patterns and Pitfalls
Pitfall: Trying to Copy types with heap data
// This won't compile
#[derive(Copy, Clone)] // Error: Copy trait cannot be implemented
struct Container {
items: Vec<i32>, // Vec allocates on heap
}
If any field isn’t Copy, the struct can’t be Copy. Use Clone instead.
Pitfall: Unnecessary clones in function parameters
// Bad: unnecessary clone
fn process_bad(data: String) {
println!("{}", data);
}
fn main() {
let s = String::from("hello");
process_bad(s.clone()); // Wasteful clone
println!("{}", s);
}
// Good: use a reference
fn process_good(data: &str) {
println!("{}", data);
}
fn main() {
let s = String::from("hello");
process_good(&s); // No clone needed
println!("{}", s);
}
Only clone when you truly need ownership. References are usually sufficient.
Pattern: Clone-on-write with Cow
use std::borrow::Cow;
fn process_data(input: &str) -> Cow<str> {
if input.contains("bad") {
// Only clone when modification is needed
Cow::Owned(input.replace("bad", "good"))
} else {
// No clone needed
Cow::Borrowed(input)
}
}
fn main() {
let s1 = "hello world";
let s2 = "bad word";
let r1 = process_data(s1); // No allocation
let r2 = process_data(s2); // Allocates only when needed
println!("{}, {}", r1, r2);
}
Pattern: Reference counting instead of cloning
use std::rc::Rc;
#[derive(Debug)]
struct LargeData {
bytes: Vec<u8>,
}
fn main() {
let data = Rc::new(LargeData {
bytes: vec![0; 1000],
});
let data2 = Rc::clone(&data); // Cheap: only increments counter
let data3 = Rc::clone(&data);
println!("Reference count: {}", Rc::strong_count(&data)); // 3
}
Rc (single-threaded) and Arc (thread-safe) provide shared ownership without cloning the underlying data.
Best Practices and Guidelines
When to implement Copy:
- Type fits in a few CPU registers (typically ≤ 16 bytes)
- All fields are
Copy - No heap allocations
- No custom drop logic
- Copying is semantically meaningful
Examples: coordinates, IDs, configuration flags, small fixed-size arrays.
When to implement Clone only:
- Type contains heap-allocated data
- Type manages resources (file handles, connections)
- Copying is expensive enough to warrant explicit calls
- Type implements
Drop
Examples: collections, strings, smart pointers, complex business objects.
Decision tree in code:
// 1. Small, simple type? → Copy
#[derive(Copy, Clone)]
struct UserId(u64);
// 2. Contains String/Vec? → Clone only
#[derive(Clone)]
struct User {
name: String,
id: UserId, // UserId is Copy, but String makes User Clone-only
}
// 3. Resource management? → Clone with custom logic
struct FileHandle {
path: String,
}
impl Clone for FileHandle {
fn clone(&self) -> Self {
// Custom logic: maybe open a new handle
println!("Opening new file handle for {}", self.path);
FileHandle {
path: self.path.clone(),
}
}
}
// 4. Need shared ownership? → Use Rc/Arc instead
use std::sync::Arc;
struct SharedData {
inner: Arc<Vec<u8>>,
}
Performance considerations:
Copyhas zero runtime cost—it’s just a memcpyClonecost depends on implementation—measure if it matters- Avoid cloning in hot loops; prefer references or
Rc/Arc - Profile before optimizing; most clones aren’t bottlenecks
API design:
When designing functions, prefer this order:
- Borrowed references (
&T,&mut T) when you don’t need ownership Copytypes by value (cheap)Clonetypes by value only when you need ownership- Accept
impl CloneorT: Clonebounds when callers should decide
The key insight: make expensive operations explicit. Copy hides the duplication because it’s cheap; Clone requires .clone() to signal that something non-trivial is happening. This explicitness helps readers understand where allocations occur and makes performance characteristics visible in the code.