Skip to content
← All Rust articles

Rust explainer

Choose between cloning, moving and borrowing

About 5 min to read. Allow extra time to work through examples.

Decide what must remain usable, what should be shared, and which cloning costs are worth measuring.

This article uses Rust. You can make it your practice mode to browse related content across the site.

Use clone when another owned value is useful. Before adding it, check what the type clones, which values must remain usable, and whether the cost matters in this part of the program. A compiling solution and an efficient ownership design are related questions, but neither proves the other.

Start with the type

Clone is a trait with type-specific behavior. A String clone produces another owned string; cloning a Vec clones its elements. Cloning Rc or Arc creates another owner of the same allocation rather than copying the stored value. A custom type can implement different behavior, including sharing resources. Do not assume clone always means a deep copy or exactly one allocation.

Copy is different: it permits implicit duplication when values are assigned or passed. For an i64, a plain assignment is enough. A String is not Copy. See the Clone documentation.

Keep an independent copy when you need one

This example keeps the original name while sending an owned copy elsewhere:

fn store_name(name: String) -> String {
    name
}

fn main() {
    let name = String::from("Ada");
    let stored = store_name(name.clone());
    assert_eq!(name, "Ada");
    assert_eq!(stored, "Ada");
}

If name is never needed again, move it into store_name instead. If the receiver only needs to read it during the call, a borrowed parameter may be a better API. Those alternatives depend on what the receiver must do; neither is a universal replacement for an owned copy.

An index that owns String keys can outlive the records it was built from or retain a snapshot while the records change. An index with borrowed string keys avoids those copies but ties its validity to the borrowed data. Even when the lifetimes permit borrowing, independent ownership can be a deliberate choice.

Borrow before moving a field

Suppose logging needs to inspect a request and parsing needs to own its body. A borrow for logging followed by a field move can avoid a copy:

struct Request {
    id: i64,
    body: String,
}

fn log_request(request: &Request) -> i64 {
    request.id
}

fn parse(body: String) -> usize {
    body.len()
}

fn main() {
    let request = Request { id: 7, body: String::from("hello") };
    assert_eq!(log_request(&request), 7);
    assert_eq!(parse(request.body), 5);
}

Here log_request returns the id as a small stand-in for recording it. Its shared borrow ends before the body moves. If logging instead consumes the whole Request, moving body first and then passing the partially moved request to the logger does not compile. Reordering alone cannot give both consumers ownership of the same body. You would need a copy, a changed API, or a helper that returns ownership after using it.

The Request above has no Drop implementation. Types with Drop impose further restrictions on moving fields out. Techniques such as Option::take or mem::replace can leave a valid replacement when that matches the type's design.

Shared ownership is a different decision

Arc::clone keeps the same underlying value alive for another owner:

use std::sync::Arc;

fn main() {
    let shared = Arc::new(vec![1, 2, 3]);
    let worker_data = Arc::clone(&shared);
    let worker = std::thread::spawn(move || worker_data.len());
    assert_eq!(worker.join().unwrap(), 3);
    assert_eq!(shared.as_slice(), [1, 2, 3]);
}

This does not clone the vector's elements. It updates shared ownership using an atomic reference count, which still has a cost. Arc also does not make an arbitrary contained type safe to mutate across threads. Its thread-safety bounds and any required synchronization still apply.

Arc is one option, not the only way to use data in another thread. Moving sole ownership or borrowing within a scoped thread can also be appropriate. Rc is for shared ownership within a thread and uses a non-atomic count. See the Arc documentation.

Decide which costs deserve attention

A one-time copy of a small configuration value may simplify an API at little cost. Repeatedly copying a large collection may dominate memory traffic or allocation. Cloning a vector of n fixed-size elements once for each of its n elements copies O(n²) elements; variable-length strings add their byte-copying costs too. The relevant questions are size, frequency and what each element's Clone implementation does.

Measure a representative workload before making performance claims. Include allocation and memory use when they matter, rather than counting clone calls as if all were equivalent. A profiler can help locate expensive copying, but it does not decide whether the resulting ownership behavior is necessary.

Clippy can catch some suspicious clones, such as cloning a Copy value. Its lint coverage is not a proof that every remaining clone is useful or cheap. Likewise, adding clone to resolve an ownership error is not automatically a design mistake: explain which independent value or additional owner is needed.

When considering an alternative, ask:

  • Can the original move because it is no longer needed?
  • Can the receiver borrow for the duration of its work?
  • Can one operation finish before the next borrow begins?
  • Can a consuming helper return the value for reuse?
  • Does the program need independent data, shared ownership, or only an index into data owned elsewhere?

Choose the version whose ownership behavior is clear, then investigate its cost where that cost matters. Keep prototype shortcuts visible when they leave a real design question unresolved, rather than treating every clone as either an error or a free operation.

step 20 of 20

Ownership I: Moves, Copy, Clone, Drop

You've reached the last step

Revisit the track to review its lessons and exercises, or explore another topic.

Explore more tracks