Avoiding Clones
clone() is explicit and often expensive. Design APIs around borrows, Cow, and moved ownership so copies happen only at intentional boundaries.
Recipe
use std::borrow::Cow;
fn greet(name: Cow<'_, str>) {
println!("Hello, {name}");
}
greet(Cow::Borrowed("Ada"));
greet(Cow::Owned("Bob".into()));When to reach for this: Clippy redundant_clone fires in hot paths or review spots clone() in loops.
Working Example
struct Request<'a> {
path: &'a str,
body: Cow<'a, [u8]>,
}
fn handle(req: Request<'_>) -> Status {
if req.path == "/health" {
return Status::Ok;
}
process_body(req.body.as_ref())
}What this demonstrates:
&str/&[u8]borrow when caller owns dataCowaccepts borrowed or owned without forcing clone upfront- Move
Stringonce into struct instead of clone per use Arc<str>for shared immutable strings across threads
Deep Dive
Strategies
| Pattern | Use |
|---|---|
&T / &str parameters | Read-only hot paths |
Cow<'a, T> | Sometimes needs ownership |
| Move by value | Single consumer |
Arc<T> | Shared read-heavy data |
Rc<T> | Single-thread share |
Axum Example
async fn handler(Json(body): Json<CreateOrder>) -> impl IntoResponse {
// Json<T> owns body once; pass references into domain layer
service.create(&body).await
}Gotchas
- Clone to satisfy borrow checker - band-aid. Fix: restructure lifetimes, use owned data in async tasks, or
Arc. - Cloning
Arcthinking it's cheap data clone -Arc::cloneis cheap; innerStringclone is not. Fix: distinguish pointer clone vs data clone. - Cow everywhere - API complexity. Fix: use where both borrowed and owned callers exist.
- Implicit clone in
to_owned()in loop - same cost asclone(). Fix: batch or borrow. - serde always owned - use
#[serde(borrow)]on&strfields when deserializing from&[u8]buffer.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Arc<str> | Immutable shared strings | Unique mutation needed |
Pin<Box<T>> | Self-referential | Simple moved values |
&'static str | Compile-time literals | Dynamic user input |
FAQs
When is clone OK?
At IO boundaries, spawning tasks needing 'static, or cheap Copy types.
Cow performance?
Small enum discriminant; branch cost negligible vs heap clone saved.
Async and borrowed data?
Spawned tasks need 'static; use owned String or Arc at spawn boundary once.
How find redundant clones?
cargo clippy -- -W clippy::redundant_clone and profilers.
Clone vs copy?
Copy types (integers, refs) are bitwise; Clone may heap-allocate.
Iterator collect clones?
collect::<Vec<_>>() clones if iterating references; use into_iter() when consuming.
sqlx Row get clones?
Use try_get to typed values; fetch &str only with compatible lifetime patterns or copy once.
Polars clone columns?
Lazy plan shares; clone() on DataFrame is data copy - minimize collects.
Builder pattern clones?
Builder moves fields; avoid #[derive(Clone)] on large configs.
Review rule of thumb?
One clone() per request OK; clone per row in million-row loop needs justification.
Related
- Understanding Allocations - heap cost
- Cow, Clone on Write - smart pointer detail
- Common Anti-Patterns - over-cloning
- 30 Performance Rules
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.