The Rust Engineering Handbook / Chapter 6
Moves, Copies, Clones, and Partial Moves
Predict ownership transfers and choose moves, copies, clones, or replacement operations by contract and cost.
Follow the value, not the variable name
A compiler diagnostic can tell you that a relationship is invalid. Part II begins one step earlier: predicting the relationship before compilation, then deciding whether a boundary should borrow, transfer, or duplicate. The useful instrument is an ownership ledger.
For every owned value, ask two questions: which path may use it now, and which operation changes that answer? Assignment, a by-value call, a return, destructuring, container extraction, and closure capture all answer the first question. Copy, Clone, and replacement answer the second in materially different ways.
After a move, the destination owns the value and the moved path at the source is unavailable until it is reinitialized. Copy preserves source usability by implicit duplication; Clone performs explicit, type-defined duplication. This is a capability rule, not a promise that bytes were relocated, zeroed, or immediately deallocated.
That distinction begins the ownership model: account for who may use a value, then judge identity, lifecycle, allocation, and operational cost. Consider a relay that accepts an Entry, validates it, records enough information for a possible retry, and hands work to a sender. If every stage clones the account, tags, and complete entry, all paths remain usable—but no path reveals which record is authoritative, which is a retry snapshot, or how long either may retain sensitive data. The code compiles because duplication avoids ownership conflicts. The design is still obscure.
Assignment, calls, and returns use the same rule
Consider an owned record:
#[derive(Debug)]
struct Entry {
account: String,
cents: i64,
tags: Vec<String>,
}
let first = Entry {
account: "cash".into(),
cents: 2_500,
tags: vec!["reviewed".into()],
};
let second = first;
second now owns the Entry. Using first.account is rejected because the whole first path was moved. Passing first to a by-value parameter or returning it from a function applies the same ownership transfer. Function arguments are values; a reference argument happens to be a value that grants borrowed access.
A move does not call Clone::clone. For many concrete implementations, moving a String copies its small handle while ownership of the same heap allocation changes. That observation helps with cost intuition, but the language contract is source unavailability plus destination ownership. Optimizations may remove physical copying, and unsafe reasoning must not depend on an imagined byte-transfer sequence.
Return values make ownership flow visible. A validator that may reject an entry should decide whether rejection also consumes the evidence:
#[derive(Debug)]
struct InvalidEntry {
entry: Entry,
reason: &'static str,
}
fn validate(entry: Entry) -> Result<Entry, InvalidEntry> {
if entry.cents == 0 {
Err(InvalidEntry {
entry,
reason: "zero-value entry",
})
} else {
Ok(entry)
}
}
Both branches consume the local binding. One transfers the entry into Ok; the other transfers it into InvalidEntry, where quarantine or audit code can recover it. Had the error stored only a message, the rejected entry would be dropped on that path. Encoding the rejected value in the error states the lifecycle directly and avoids a precautionary clone.
Now the relay’s ledger can be deliberate. Validation takes the entry and returns ownership on either edge. Logging borrows it. Retry storage receives one explicit clone only when the retry contract requires a stable snapshot. Finally, the sender receives the original by value. At every boundary, either one owner continues, temporary access is borrowed, or a second value exists for a named reason.
Figure 6-1 turns that rule into a path ledger. Read a crossed source as unavailable, not destroyed, and notice that a field path may move while its siblings remain usable.
Copy means implicit duplication is part of the type contract
Types such as i64, bool, and many small aggregates implement Copy. Reading them from a place produces another usable value while leaving the source usable:
let cents = entry.cents;
assert_eq!(entry.cents, cents);
Copy has important restrictions: a type with a destructor cannot implement it, and every field of a derived Copy type must itself be Copy. That keeps implicit duplication away from types whose resource identity or cleanup makes duplication consequential.
Do not infer that every Copy is costless. A large [u8; 4096] may be Copy; whether the optimizer avoids a physical copy is workload-, target-, and profile-specific. The API statement is that duplication may happen implicitly and that source and result can be used independently as values.
Choose Copy when identity is not meaningful, independent duplicated values are unsurprising, and the cost is appropriate for ordinary reads and calls. Avoid it for resource handles, large values where implicit duplication obscures cost, or types whose future evolution may need destruction.
Clone is an explicit semantic event
Clone permits code to request another value, but the operation is type-defined. String::clone allocates and copies text. Arc::clone increments an atomic reference count and creates shared ownership of the same allocation. A file type may offer a separate fallible handle-duplication method, while deriving Clone for a struct clones each field.
The right review question is not “is clone bad?” For the relay’s retry snapshot, ask:
- Does the design require a snapshot, shared identity, or independent mutation?
- How large is the cloned state, and how often is the path executed?
- Can the copy retain credentials or personal data longer than intended?
- Does the clone conceal a missing lifecycle boundary?
- If the source changes, may the consumer observe stale state?
A deliberate retry snapshot can be an excellent use of Clone: it lets delivery consume the original while retry storage owns an independent record. That snapshot may also become stale, double the retained payload, and extend the life of credentials in its tags. A clone added merely because a borrow was inconvenient has none of that contract and is unfinished reasoning.
Move paths make partial moves precise
Rust tracks ownership through paths. Destructuring can move one non-Copy field and copy another:
let account = entry.account; // moves the String field
let cents = entry.cents; // copies the i64 field
The entry.account path is unavailable. entry.cents remains usable. The whole entry cannot be used as an intact value because one of its fields is missing. This is a partial move.
Patterns make the same choice explicit:
let Entry { account, ref tags, cents } = entry;
Here account moves, tags is borrowed, and cents copies. Match ergonomics may insert borrows based on the scrutinee and pattern, so a review should annotate whether each binding owns, copies, or borrows when the distinction matters.
Types implementing Drop cannot generally have individual fields moved out through ordinary destructuring because their destructor expects a complete self. Use an API that explicitly transfers the resource, keep removable data in Option, or replace a field with a valid value.
Replacement preserves a valid value in the source place
std::mem::take replaces a place with Default::default() and returns the old value:
let tags = std::mem::take(&mut entry.tags);
assert!(entry.tags.is_empty());
std::mem::replace accepts the replacement explicitly:
let old = std::mem::replace(&mut entry.account, "retired".to_owned());
Both make the invariant reviewable: the field is never left uninitialized. Option::take is often sharper for state transitions because None visibly represents “no current value.”
These operations do not remove the need for panic analysis. Construct the replacement before mutating the place when construction may fail. If an invariant requires several fields to change atomically at the logical level, stage the new state and commit it as one transition rather than taking fields sequentially and leaving a half-transition visible to callbacks.
Moving out of a container needs a container operation
Indexing a Vec<T> produces a place behind borrowed container access. This is rejected for non-Copy T:
let names = vec![String::from("cash")];
let name = names[0]; // E0507: cannot move out of index
The container must remain structurally valid. Choose an operation matching the data structure and ordering contract:
| Operation | Ownership result | Structural cost | Use when |
|---|---|---|---|
Vec::remove(i) |
Returns owned element | Shifts later elements | Order must be preserved |
Vec::swap_remove(i) |
Returns owned element | Constant-time, reorders | Order is irrelevant |
Vec::pop() |
Returns last element | Constant-time | Stack order fits |
Option::take() in a slot |
Leaves explicit vacancy | No shift | Stable slots/state transitions matter |
Borrow &items[i] |
Container retains ownership | No ownership transfer | Consumer needs temporary access |
The compiler error is about ownership safety; the choice among repairs is an architecture and performance decision.
Closure capture records a delayed ownership decision
A closure captures only what its body needs, subject to capture precision and how it uses each capture. It may borrow shared, borrow mutably, or capture by value. The move keyword requests by-value capture; it does not promise that captured values implement Copy, nor does it mean every operation inside the closure moves them again.
let entry = Entry::new("cash", 2_500);
let job = move || format!("{}:{}", entry.account, entry.cents);
The closure now owns the fields it captured by value, allowing the closure to outlive the original scope if its other bounds permit. This is often right for thread or task ownership. It can also retain a large object graph longer than intended. Capture a narrow command or owned field instead of a service container when lifecycle and memory retention matter.
Capture mode also helps determine whether the closure implements Fn, FnMut, or only FnOnce. Chapter 22 develops that trait relationship; here the ownership review is enough: identify what enters the closure environment and whether calling consumes it.
When the ledger looks clean but lies
Ownership-correct code can still tell the wrong operational story. Cloning at every layer turns one record into ambiguous snapshots. Deriving Copy for a large value makes ordinary reads conceal duplication. mem::take can manufacture a default state that satisfies the type system but violates the domain. swap_remove can silently violate ordering, and a move closure can retain an entire service context when it needs one command.
The opposite mistake is to treat a moved source as destroyed. A move makes a source path unavailable; it does not promise immediate drop, erased bytes, or a particular physical relocation. Cleanup, secret-retention, and unsafe arguments must follow actual owners and drop points rather than the crossed-out variable in the source.
Senior review checklist
- Can every owned value be assigned one current owner at each boundary?
- Does every clone name snapshot, independence, or shared-identity semantics?
- Is
Copyunsurprising for the type’s size, identity, and likely evolution? - After a partial move, is every remaining field use obvious?
- Does replacement leave the type in a valid, meaningful state?
- Does the chosen container extraction preserve required order and complexity?
- Does a closure retain only the state and lifetime it needs?
- Are cleanup and secret-retention implications visible?
Engineering exercise: remove accidental clones
Take a dispatch routine that clones an Entry for validation, logging, retry storage, and worker delivery. Before changing it, mark the owner after every statement and identify the first clone whose removal makes the design question visible. Refactor so observation borrows, rejection returns the recoverable entry, retry storage owns one explicit snapshot only if the policy requires it, and delivery consumes the work item.
Then vary the problem: make tags contain a secret, require retries to observe the original payload, and place pending work in an order-preserving queue. Your review artifact must state the invariant, every remaining clone’s semantics and size bound, container extraction costs, closure captures, and tests proving failure paths do not lose recoverable work. A mechanical clone-removal pass cannot satisfy all three constraints; the ownership ledger must explain the policy.
Durable takeaways
- A move changes source usability and destination ownership; it is not a guaranteed byte-level event.
Copyauthorizes implicit duplication, whileClonemakes a type-defined duplication explicit.- Move paths allow one field to move while untouched fields remain usable, unless a destructor contract prevents partial movement.
take,replace, and container removal operations transfer ownership while preserving a valid container or field state.- Closure capture is ownership architecture delayed into an environment; review what it retains.
An ownership ledger tells you who may use a value after transfer or duplication. The next question is how code grants temporary access without creating another owner: the capability model of shared and mutable borrowing.
Sources and version notes
- Rust Reference: move expressions
- Rust Reference: destructors
std::marker::Copy,std::clone::Clone, andstd::mem- Verified with Rust 1.97.0 and the declared Rust 1.85.0 MSRV on
x86_64-unknown-linux-gnu; physical relocation and optimization observations are not language guarantees.
Continue reading
Full table of contents