When is a clone the right answer?
Reviewing AI-written Rust opens with a clone that should not be there: a function that only reads a vector demands to own it, so the caller copies the whole vector. Read on its own, that could suggest every .clone() is a mistake. It is not, and code that avoids every clone often pays for it somewhere less visible.
Knowing when a clone is the right answer starts with two questions: what does it copy, and how often does it run? The rest of this article works through the cases where the answer is “keep it”. Every example compiles without warnings, and Clippy’s default lints say nothing about any of them (with the Rust version shown above). The compile check has the evidence.
What a clone copies
.clone() comes from the Clone trait (a trait is a named set of methods a type can implement, much like an interface). It produces a new value of the same type, but how much is copied, and how much is still shared, depends on the type. Integers, bool and other small Copy types are copied implicitly and never need it; Clippy’s default clone_on_copy lint flags the attempt.
use std::sync::Arc;
let text = String::from("a long document");
let copy = text.clone();
let shared = Arc::new(String::from("a long document"));
let handle = Arc::clone(&shared);Cloning a non-empty String allocates a new buffer and copies the bytes. A Vec<T> clone makes a new buffer and clones each element with the element’s own Clone.
Arc (“atomically reference counted”) is a pointer to a shared value plus a count of how many such pointers exist; Rc is its single-threaded sibling. Cloning one copies the pointer and adds one to the count. The string is not copied. The count is updated atomically, which the standard library says is “more expensive than ordinary memory accesses”, so the clone is cheap rather than free. Rc avoids that cost but cannot be sent to another thread. And Arc makes the count thread-safe, not the contents: an Arc<T> can only cross threads if T can.
So a clone is not always an independent copy. Cloning a Vec<Arc<T>> gives a new vector pointing at the same Ts.
“Small and cheap” is therefore about field types, not field count. A derived Clone clones each field: a number is a plain copy, a non-empty String is one allocation. A struct with a short string and two numbers, cloned once per request, is very unlikely to show up in a profile.
Cloning at a boundary
The most common good clone hands a value to something that has to own it.
#[derive(Clone, Debug, PartialEq)]
pub struct Settings {
pub endpoint: String,
pub retries: u32,
}
pub struct Client {
settings: Settings,
}
impl Client {
pub fn new(settings: Settings) -> Self {
Self { settings }
}
}
let primary = Client::new(settings.clone());
let fallback = Client::new(settings);Each client keeps its own settings for as long as it lives. new takes them by value because it stores them, as the Rust API Guidelines advise, so the clone is visible at the call site and the last use takes the original without one. A new(&Settings) that cloned internally would hide that cost from every caller.
Borrowing instead, Client<'a> { settings: &'a Settings }, removes the clone but adds a lifetime parameter (a name for how long the borrow lasts) that spreads to every type holding a Client, and a client that borrows local settings cannot be handed to a spawned thread. For a few strings copied at start-up, that is a poor trade. If the settings are large, share one copy through Arc<Settings>.
Threads are the other common boundary:
use std::thread::{self, JoinHandle};
pub fn spawn_reporter(settings: Arc<Settings>) -> JoinHandle<String> {
thread::spawn(move || format!("reporting to {}", settings.endpoint))
}
let shared = Arc::new(settings);
let reporter = spawn_reporter(Arc::clone(&shared));thread::spawn needs a closure that is Send (safe to move to another thread) and 'static (borrowing nothing from the caller except data that lasts for the whole program), because the thread may outlive the function that started it. So the thread owns its Arc, and the caller clones it to keep a handle of its own: one count increment.
Cloning to end a borrow early
pub struct Scheduler {
jobs: Vec<String>,
log: Vec<String>,
}
impl Scheduler {
fn run(&mut self, job: &str) {
self.log.push(format!("ran {job}"));
if job == "build" {
self.jobs.push("test".to_string());
}
}
pub fn run_all(&mut self) {
for job in self.jobs.clone() {
self.run(&job);
}
}
}run takes &mut self, an exclusive borrow of the whole scheduler, and may push a follow-up job. Loop with for job in &self.jobs instead and the compiler refuses:
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutableThat loop would modify the vector it is iterating over. Java usually answers with a ConcurrentModificationException at run time; C++ can leave you holding an invalidated iterator. The clone makes a snapshot: run_all runs the jobs that existed when it started, and follow-ups wait for the next call. The code says so plainly, and the cost is one copy of a list of names.
The alternatives cost more than they save here. The compiler suggests an index-based loop, but self.run(&self.jobs[i]) fails with the same error, so it needs self.jobs[i].clone(): the same clone, one name at a time. std::mem::take swaps an empty vector into the field, which avoids the copy, but then the follow-ups must be stitched back on, and run sees no jobs while it works. If the list were long and run_all hot, the fix would be a different design, such as a queue.
Cow, briefly
use std::borrow::Cow;
pub fn expand_tabs(input: &str) -> Cow<'_, str> {
if input.contains('\t') {
Cow::Owned(input.replace('\t', " "))
} else {
Cow::Borrowed(input)
}
}Cow (“clone on write”) holds either a borrowed value or an owned one, so expand_tabs allocates only when it has something to change. The '_ says the result may borrow from input. The price is a lifetime in the caller’s type and a check of the variant on each access. It pays off on a busy path where most inputs need no change; elsewhere, return a String.
Judging a clone in review
Some clones are cheap once and expensive in bulk:
use std::collections::HashMap;
pub fn tally(words: &[String]) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in words {
*counts.entry(word.clone()).or_insert(0) += 1;
}
counts
}HashMap::entry takes its key by value, so this allocates for every word, including those already counted. For a few hundred words, that does not matter. For a large log with a small vocabulary, nearly every allocation is waste. Checking with get_mut first clones once per distinct word; a HashMap<&str, usize> clones nothing but cannot outlive words.
So, in review:
- What does it copy? A pointer, or all of the data? Check the type.
- How big, and how often? Cost is roughly size times frequency. Once at start-up rarely matters; once per item of input deserves a look.
- Does the receiver need to own it? If it only reads, the signature should borrow, as in the reviewing article.
- Is it a hand-off or a snapshot? Then the clone states something true.
Still unsure? Measure. A profiler settles in minutes what a review thread can argue about for days.
A rule of thumb
If removing the clone needs a signature change that asks for less, such as a borrow instead of ownership, the clone was probably hiding a design problem. If it needs lifetime gymnastics, such as lifetime parameters spreading through structs, mem::take juggling, or restructuring purely to please the borrow checker, the clone is probably fine. Leave it, perhaps with a comment saying why.
Neither half is a law. Module 1 of Rust Primer, Ownership by repair, practises exactly this judgement: how moves, copies, borrows and clones differ, and when a clone is the right answer.