The Republic did not fall because it had clone troopers. It fell because it did not understand what they were for.
The same thing happens in Rust codebases. Developers treat .clone() as if every call were Order 66, a hidden catastrophe waiting to fire. They spend thirty minutes wrestling with lifetime annotations to avoid a clone that copies fifty bytes. They add Arc<Mutex<>> to a struct that holds a single PathBuf. They complicate interfaces, propagate lifetime parameters through five layers of abstraction, and produce code that is harder to read, harder to refactor, and harder to reason about. All to avoid writing five characters: .clone().
The borrow checker does not tell you what a clone costs. It tells you when you need one. Those are very different things.
This series is about closing that gap. Not with ideology (“cloning is fine” or “cloning is bad”), but with mechanics: what .clone() does at the machine level for the types you actually use, and how to make the decision based on data rather than instinct.
What the Clone trait actually is #
The Clone trait has one required method:
pub trait Clone {
fn clone(&self) -> Self;
}The contract is simple: produce a new, independent value. After let b = a.clone(), modifying b must not affect a. That is the entire promise. The trait says nothing about cost, nothing about allocation, nothing about what happens internally. It says: “you get a copy, and it is yours.”
The mechanism behind that copy varies wildly.
The cost spectrum: six orders of magnitude #
Here is what .clone() actually does for the types you see every day:
| Type | What clone() does internally |
Approximate cost |
|---|---|---|
i32, f64, bool |
Bitwise copy (same as Copy) |
< 1 ns |
String (32 bytes) |
Allocates a new heap buffer, copies bytes | ~20-50 ns |
PathBuf (typical path) |
Same as String – it wraps an OsString |
~20-50 ns |
Vec<u8> (1 KB) |
Allocates a new buffer, copies 1,024 bytes | ~50-100 ns |
Vec<u8> (1 MB) |
Allocates a new buffer, copies 1,048,576 bytes | ~100-500 us |
HashMap<K,V> (100 entries) |
Allocates a new hash table, clones every key and value | ~1-5 us |
HashMap<K,V> (10,000 entries) |
Same, but ten thousand pairs | ~50-500 us |
Arc<T> |
Increments an atomic reference counter. No data copied. | ~5-10 ns |
Rc<T> |
Increments a non-atomic reference counter. No data copied. | ~2-5 ns |
File, TcpStream, MutexGuard |
Does not compile. No Clone impl. |
– |
Read that table again. The same five characters, .clone(), range from “faster than a function call” to “blocks your thread for half a millisecond.” The syntax is identical. The implications are not.
A clone trooper factory can produce a soldier in minutes or in months, depending on the template. A Kaminoan engineer knows the difference. A Rust programmer should, too.
Clone vs Copy: two very different conversations #
Every Rust beginner hits this question early: if Clone duplicates values, what is Copy for?
Copy |
Clone |
|
|---|---|---|
| When it fires | Implicitly, on assignment and function calls | Explicitly, only when you write .clone() |
| What the compiler does | Bitwise memcpy on the stack | Calls the clone() method, which can do anything |
| Cost guarantee | Always trivial. Always. | None. Can allocate, can do I/O, can take milliseconds. |
| Who can implement it | Only types where all fields are Copy and there is no Drop |
Any type |
| Semantic signal | “Duplicating this is as cheap as copying a register” | “Duplicating this might cost something. Look inside.” |
Copy is a promise of triviality. The compiler trusts it enough to do it behind your back. When you write let y = x where x: i32, the compiler copies four bytes without asking. You do not write .clone(). You do not think about it. It just works, because the cost is guaranteed to be negligible.
Clone is a capability without a cost promise. The type can be duplicated, but the cost is your responsibility to assess.
Why String cannot be Copy
#
This restriction is not arbitrary. A String owns a heap allocation. If String were Copy, every let y = x would silently double memory usage:
let x = String::from("a]".repeat(1_000_000)); // 1 MB on the heap
let y = x; // If this were Copy: silent 1 MB allocation. Every. Single. Time.
The language forces you to write .clone() explicitly so the allocation is visible. You see it in the code. You can grep for it. You can profile it. You can ask: “is this clone necessary, or can I restructure to avoid it?”
The same reasoning applies to Vec, PathBuf, HashMap, and every other heap-owning type. If duplication has a cost, Rust makes you acknowledge it.
Why Arc cannot be Copy
#
Arc<T> clone is cheap (an atomic increment). So why is it not Copy?
Because Arc implements Drop. When an Arc goes out of scope, it decrements the reference count and potentially deallocates the shared data. Copy types cannot have Drop; the compiler cannot guarantee correct cleanup if values are silently duplicated everywhere.
So Arc::clone(&x) is the compromise: explicit in the code, trivial in cost. The community convention is to write Arc::clone(&x) rather than x.clone() specifically to signal: “this is a refcount bump, not a deep copy.” Both compile identically. The convention exists for human readers.
What derive(Clone) generates
#
When you write #[derive(Clone)], the compiler generates field-by-field cloning:
#[derive(Clone)]
struct Config {
name: String, // String::clone() → new heap allocation
port: u16, // u16 is Copy → bitwise copy, trivial
tags: Vec<String>, // Vec::clone() → new allocation + clone each String element
}The generated clone() is conceptually:
impl Clone for Config {
fn clone(&self) -> Self {
Config {
name: self.name.clone(),
port: self.port, // Copy, no .clone() needed
tags: self.tags.clone(),
}
}
}The cost of cloning Config is the sum of cloning its fields. If tags contains 10,000 strings, the clone cost is dominated by those 10,000 string allocations. If tags is empty and name is 20 characters, the cost is two small allocations.
The derive tells you nothing about the cost. It is a mechanical code generator, not a cost analyzer. You have to look inside the struct to know what you are paying.
This is the critical insight that the borrow checker cannot provide. The compiler tells you “this value has been moved, you need a clone if you want to use it again.” It does not tell you “this clone will cost 50 nanoseconds” or “this clone will copy 10 megabytes.” That knowledge is yours to carry.
Types that refuse to clone #
Some types intentionally do not implement Clone:
File: an open file handle is a kernel resource. Duplicating it requires OS-leveldup(), which is not a simple memory copy. Rust exposes this asfile.try_clone(), returning aResultbecause the OS can refuse.TcpStream: a network socket. Same reasoning asFile.MutexGuard: a proof that you hold a lock. Duplicating it would mean two holders of the same lock, violating mutual exclusion.&mut T: an exclusive reference. Duplicating it would create two exclusive references to the same data, violating Rust’s core invariant.
When a type does not implement Clone, the type author is telling you: this value represents a unique resource that cannot be meaningfully duplicated. The absence of Clone is as informative as its presence. Like a lightsaber crystal: you do not mass-produce kyber crystals. Each one bonds to its wielder.
Where this leaves us #
The borrow checker tells you when you need to duplicate a value. The Clone trait tells you that duplication is possible. Neither tells you what it costs.
That cost depends on a single question: what is inside the struct?
- A
PathBuf? Fifty bytes. Clone and move on. - A
Vec<u8>with a million elements? One megabyte. Think twice. - An
Arc<HeavyData>? Eight bytes of pointer plus an atomic increment. Cheap, but understand why. - A
File? Cannot clone. Restructure.
In the next post, we look at the six strategies Rust gives you for sharing state when cloning is not the right call, or when it is, alongside the alternatives it competes against. Each strategy is a lightsaber form: effective against a specific threat, dangerous when misapplied.
Reference: