You have now spent a whole track on ownership, and you have used .clone() as
a legitimate tool the entire time. Track 3 takes it away — deliberately, as a
named event — and replaces it with &. Before that happens, it is worth
settling what you actually think about cloning, because if you do not decide
now, one of two things will happen to you.
Either you become a reflexive cloner: the compiler complains, you add
.clone(), it compiles, you move on. Six months later a profile shows 40% of
your runtime in memcpy and you have no idea which of the four hundred clones
in the codebase are the problem.
Or you become a principled refuser: cloning is admitting defeat, so you spend two hours restructuring a function that runs once at startup on a three-element vector, and the restructured version is worse to read.
Both come from treating “should I clone?” as a rule question. It is a judgement question, and the two positions are both defensible. Here they are, each in its strongest form.
The case for cloning freely
Compile time is your time; runtime is the machine’s. A clone that costs 200 nanoseconds in a code path that runs once per request is free. A clone that costs you forty minutes of restructuring is not. Optimising the wrong one is the most common way for a competent programmer to be slow.
You cannot see the hot path from here. Most clones are in setup code, error paths, config parsing, tests, and CLI argument handling — places where the allocation count is measured in dozens over a program’s lifetime. The clones that matter are in loops, and you will find them with a profiler in ten minutes when you need to.
Restructuring to avoid a clone often makes the code worse. The lifetime
gymnastics that let you avoid one allocation frequently produce a signature
that infects every caller, a struct with a 'a on it, and a type that cannot
be stored in a collection. That is a real, permanent readability cost paid to
avoid a cost that may be zero.
Prototypes are supposed to be disposable. If you are still discovering what the program should do, ownership design is premature. Clone your way to a working version, then let the shape you discovered tell you where the boundaries belong. The classic advice —
let name = record.name.clone(); // TODO: clone to satisfy borrowck
— is genuinely good practice, because it converts an unresolved design question into a grep-able marker instead of a stall.
Rc::clone is not the same operation at all. It bumps a refcount. Code
that “clones a lot” may be cloning nothing but pointers, and reading a clone
count without reading the types tells you nothing.
The case against cloning freely
A clone in a hot loop is a design smell, not a micro-optimisation.
Cloning a Vec<String> inside a loop over that same vector is quadratic. It
will not show up in a small test, it will show up in production, and the fix
at that point is a refactor rather than a one-line change.
A clone inside a data structure is a design decision you made by accident.
If your struct Index { names: Vec<String> } is populated by cloning strings
that already exist elsewhere, you have chosen to store the data twice — and
you chose it to satisfy the borrow checker, not because you wanted two copies.
That is the kind of thing that should be argued for out loud, and clones let
it happen silently.
A clone is usually the compiler telling you the ownership story is wrong. In most cases where a clone silences E0382, there was an ordering, a return value, or a split that would have worked and would have been clearer. The clone hides the design question instead of answering it.
Nothing will tell you. This is the honest part, and it deserves emphasis:
clippy::redundant_clone — the lint that detects a clone whose original is
never used again — is in the nursery group. It is off by default, it stays
off unless a project opts in, and it is known to have false positives. Under
the default -D warnings a gratuitous clone passes the gate in silence.
Compare that to clone_on_copy, which is on by default and catches a clone of
an i64 immediately. The tool is inconsistent, and pretending otherwise is
how learners end up believing “if clippy is happy, my clones are fine.”
💡Here are four clones. Which are fine, which are wrong, and which need more information before you can say? click to reveal
// (1)
fn main() {
let config = load_config();
let logger = Logger::new(config.log_path.clone());
run(config);
}
// (2)
for user in &users {
index.insert(user.name.clone(), user.id);
}
// (3)
fn handle(req: Request) -> Response {
let id = req.id; // i64
let body = req.body.clone();
log(req);
parse(body)
}
// (4)
let handle = shared_state.clone();
thread::spawn(move || work(handle));
(1) Fine, and probably the best version. It runs once, it clones one path
string, and the alternative — restructuring load_config to hand out the path
separately, or making Logger borrow from config — buys nothing and costs a
lifetime parameter. This is exactly the “cold path” case.
(2) Needs more information, and is the most interesting one. If index
genuinely needs to own the names — because it outlives users, or because it
is returned — then the clone is not a workaround, it is the design, and the
right response is to make that explicit rather than to hide it. If index
does not outlive users, this is a borrow (&str keys, or an index into
the vector) and the clone is buying nothing but a second copy of every name.
The number of users decides whether “buying nothing” costs microseconds or
seconds.
(3) Wrong, and fixable by reordering. log(req) consumes the request, so
the clone exists only to survive that line. Move log(req) after parse, or
take the body out with req.body as a partial move, and the clone disappears
entirely. Note that id needed no ceremony at all — i64 is Copy — which
is a good reminder that “clone” and “duplicate” are different words for
different costs.
(4) Not a clone in the sense this article is about. shared_state is
presumably an Arc, so .clone() here increments an atomic counter and
copies a pointer. It is the only way to give a thread access to shared data,
and calling it “a clone I should feel bad about” is a category error. Some
codebases write Arc::clone(&shared_state) instead, precisely so this reads
differently from a deep clone — clippy’s clone_on_ref_ptr enforces that
style, and it is in the restriction group because it is a matter of taste.
The defensible middle
Here is a position you can actually hold:
-
Clone freely in prototypes, tests, setup and error paths. Leave a
marker —
// TODO: clone to satisfy borrowck— so the decision is findable later. - Treat a clone in a hot loop as a design-review trigger. Not forbidden; discussed. Someone should be able to say why it is there.
- Treat a clone that populates a long-lived data structure as an ownership decision. Write it down. “This index owns its keys” is a fine thing for a struct to do — as long as somebody chose it.
- Never clone to avoid understanding an error. If you cannot say what the clone bought, you have not read the error yet.
Rule 4 is the one this track was building toward, and it is why the article sits at the end of the track rather than the start. “Just clone it” is responsible advice only when you own the alternatives. You now do.
The counter-moves you own
When a clone is tempting, these are the tools you have already used. Run down
the list before reaching for .clone():
| Move | When it applies | Where you met it |
|---|---|---|
| Reorder | The two uses do not actually need to overlap in time | the header in Ownership through function calls |
| Catch what came back | The consuming helper returns the value | Move semantics: the relay, Reading E0382 |
| Return owned data | The caller needs it to outlive your frame | Giving ownership back |
| Partial move | You only need one field | Partial moves |
mem::take / replace / swap |
The value is behind a &mut |
Moving out of &mut |
Option::take |
The type has a destructor | E0509: a Drop type is an atom |
| A consuming builder | The value flows through a chain | Method receivers |
Track 3 adds the big one — borrow it — and it will retire perhaps three
quarters of the clones you would otherwise write. Two more arrive later:
Rc / Arc, when the sharing is genuinely real and not just a borrow you
could not express; and an arena or index-based design, when the structure
is a graph and every path through lifetimes is worse than storing usize
keys.
One more thing worth saying about the borrow-checker rewrite that made most of this easier. Non-lexical lifetimes (NLL, stabilised in the 2018 edition) mean a borrow ends at its last use, not at the end of the enclosing block. So the classic “just clone it, the borrow checker is being stupid” complaint from 2016 code is frequently no longer true — a great many of those programs compile unchanged today. If you find advice on the internet about cloning around a borrow-checker limitation, check the date.
In one sentence
A clone is not a failure and it is not free. It is a purchase — one allocation and one deep copy, or one refcount bump, depending on the type — and the only thing that makes it right or wrong is whether you know what you bought.