1.1 · Clone, Copy, and Drop: The Lifecycle Trio
Domain 1 — Ownership Mechanics in the Standard Library Duration: ~15 minutes Library components:
std::clone::Clone,std::marker::Copy,std::ops::Drop,clone_from
Introduction
Each value in Rust has exactly one owner. When that owner goes out of scope, Rust destroys the value. Three traits of the standard library control how Rust duplicates and destroys values: Copy, Clone, and Drop. The three traits interact, and the interactions can cause mistakes, also for experienced developers.
This tutorial shows:
- When the compiler makes implicit copies, and when you must clone explicitly.
- The rule that
CopyandDropare mutually exclusive. - How
clone_fromprevents unnecessary allocations. - The performance cost of
Cloneon collection types in real programs.
This tutorial assumes that you know ownership and borrowing. It examines the standard library traits that extend those concepts.
Figure: Copy, Clone, or Move?
Copy: Implicit Bitwise Duplication
Copy is a marker trait: it has no methods. When a type implements Copy, the compiler can duplicate a value with a bitwise memcpy. The compiler does this when you assign the value, pass it to a function, or return it. The copy is implicit. You never call a .copy() method.
Which types are Copy?
These types are Copy:
- All primitive scalar types (
i32,f64,bool,char). - References (
&T). - Raw pointers.
- Tuples and arrays that contain only
Copytypes.
String, Vec<T>, and Box<T> are not Copy. No type that owns heap memory is Copy.
Use case: Primitive values survive assignment
When you assign an integer to a second variable, the two variables stay usable. The reason is that i32 is Copy. Without that trait, the assignment would be a move.
fn main() {
let x: i32 = 42;
let y = x; // implicit copy: x stays valid
println!("x = {x}, y = {y}");
assert_eq!(x, 42);
assert_eq!(y, 42);
// A tuple of Copy types is also Copy.
let point = (3.0_f64, 4.0_f64);
let other = point; // implicit copy: point stays valid
println!("point = {point:?}, other = {other:?}");
assert_eq!(point, other);
// String is not Copy: the assignment moves ownership.
let s1 = String::from("hello");
let s2 = s1; // move: s1 is not valid after this line
// println!("{s1}"); // compile error E0382: borrow of moved value: `s1`
println!("s2 = {s2}");
// A struct can derive Copy when all its fields are Copy.
// Clone is a supertrait of Copy, so the derive list has the two traits.
#[derive(Debug, Clone, Copy)]
struct Pixel { r: u8, g: u8, b: u8 }
let red = Pixel { r: 255, g: 0, b: 0 };
let also_red = red; // implicit copy: red stays valid
println!("red = {red:?}, also_red = {also_red:?}");
assert_eq!(red.r, also_red.r);
// A function call copies a Copy argument.
fn double(n: i32) -> i32 { n * 2 }
let val = 10;
let doubled = double(val); // the function gets a copy of val
println!("val = {val}, doubled = {doubled}"); // val is still usable
assert_eq!(val, 10);
}
01_01_copy_semantics.rs prints:
x = 42, y = 42
point = (3.0, 4.0), other = (3.0, 4.0)
s2 = hello
red = Pixel { r: 255, g: 0, b: 0 }, also_red = Pixel { r: 255, g: 0, b: 0 }
val = 10, doubled = 20
The important point: Copy is an opt-in promise to the compiler that a bitwise duplicate of your type is semantically correct. The compiler accepts the promise. Thus it does not call custom code. It only copies the bytes.
Clone: Explicit, Potentially Expensive Duplication
Clone is a regular trait with a method: fn clone(&self) -> Self. A call to clone() is always explicit, which is different from Copy. The implementation can do any operation. For example, it can allocate heap memory, copy nested structures, or increment reference counts.
Each Copy type is also Clone, because Clone is a supertrait of Copy. But not all Clone types are Copy.
Use case: Duplicating heap-owning types
String, Vec<T>, and each struct that contains them need an explicit .clone() call to make a second independent copy.
fn main() {
// String is Clone but NOT Copy.
let original = String::from("deep sea");
let cloned = original.clone(); // allocates a new heap buffer
println!("original = {original:?}");
println!("cloned = {cloned:?}");
assert_eq!(original, cloned);
// The two strings are independent: a change to one does not change the other.
let mut cloned = cloned; // moves the clone into a mutable binding
cloned.push_str(" diving");
println!("after mutation:");
println!(" original = {original:?}");
println!(" cloned = {cloned:?}");
assert_ne!(original, cloned);
// A struct can derive Clone when all its fields are Clone.
#[derive(Debug, Clone)]
struct Document {
title: String,
pages: Vec<String>,
}
let doc = Document {
title: String::from("Rust Handbook"),
pages: vec![String::from("Chapter 1"), String::from("Chapter 2")],
};
let doc2 = doc.clone(); // deep clone: duplicates the title AND each page
println!("\ndoc = {doc:?}");
println!("doc2 = {doc2:?}");
assert_eq!(doc.title, doc2.title);
}
01_02_clone_vs_copy.rs prints these lines for the code above:
original = "deep sea"
cloned = "deep sea"
after mutation:
original = "deep sea"
cloned = "deep sea diving"
doc = Document { title: "Rust Handbook", pages: ["Chapter 1", "Chapter 2"] }
doc2 = Document { title: "Rust Handbook", pages: ["Chapter 1", "Chapter 2"] }
The derived Clone does not show an important cost. A clone of a Document does 2 + N allocations. One is for the title, one is for the Vec buffer, and N are for the page strings. The cost increases linearly with the quantity of data. A later section of this tutorial measures this cost.
The Copy/Drop Mutual Exclusion
Drop is the trait that gives a type a destructor: fn drop(&mut self). The compiler calls the destructor automatically when a value goes out of scope.
The rule is: a type cannot implement Copy and Drop together. The compiler enforces this rule.
The reason is that Copy means "a copy of the bytes is a valid duplicate". If a type has a destructor, a bitwise copy would make two owners of the same resource. When the two owners go out of scope, the destructor runs two times. The result is a double free, a double close, or a second run of some other resource cleanup.
Figure: Copy and Drop Are Mutually Exclusive
Use case: Why file handles are not Copy
fn main() {
// A type that prints a line when Rust drops it.
#[derive(Debug)]
struct Noisy { id: u32 }
impl Drop for Noisy {
fn drop(&mut self) {
println!(" Dropping Noisy(id={})", self.id);
}
}
// Noisy implements Drop, so it CANNOT implement Copy.
// impl Copy for Noisy {} // compile error E0184: the type has a destructor
println!("Creating two Noisy values:");
{
let a = Noisy { id: 1 };
let b = Noisy { id: 2 };
println!(" a = {a:?}, b = {b:?}");
// The scope ends here: b drops first (reverse declaration order), then a.
}
println!();
// Contrast: a Copy type has no destructor.
#[derive(Debug, Clone, Copy)]
struct Point { x: f32, y: f32 }
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1; // bitwise copy: no destructor runs
let p3 = p1; // a second copy: p1 is still valid
println!("p1 = {p1:?}");
println!("p2 = {p2:?}");
println!("p3 = {p3:?}");
}
01_03_copy_drop_exclusion.rs prints these lines for the code above:
Creating two Noisy values:
a = Noisy { id: 1 }, b = Noisy { id: 2 }
Dropping Noisy(id=2)
Dropping Noisy(id=1)
p1 = Point { x: 1.0, y: 2.0 }
p2 = Point { x: 1.0, y: 2.0 }
p3 = Point { x: 1.0, y: 2.0 }
This mutual exclusion is not an arbitrary rule. It is a direct result of memory safety. If std::fs::File were Copy, an assignment would silently make two file handles for the same OS descriptor. If the program then closed the two handles, the behavior would be undefined.
Drop Order
It is also important to know when destructors run and in what order.
There are three rules:
- Local variables drop in reverse declaration order: the last variable drops first.
- Struct fields drop in declaration order: the first field drops first.
- Vec elements drop in front-to-back order: index 0 drops first.
To destroy a value before the end of its scope, call std::mem::drop(value).
Use case: Releasing a lock before doing more work
fn main() {
// A type that prints its name when Rust drops it.
#[derive(Debug)]
struct Named(&'static str);
impl Drop for Named {
fn drop(&mut self) {
println!(" Dropped: {}", self.0);
}
}
// Local variables: reverse declaration order.
println!("=== Local variable drop order ===");
{
let _first = Named("first");
let _second = Named("second");
let _third = Named("third");
println!(" All three are alive");
// The scope ends here: the drop order is third, second, first.
}
// Manual drop before the end of the scope.
println!("\n=== Early drop with std::mem::drop ===");
{
let guard = Named("mutex_guard"); // represents a lock guard
let _resource = Named("resource");
println!(" Both alive — doing work...");
drop(guard); // moves guard into drop(), which destroys it immediately
println!(" Guard released — resource still alive");
// The scope ends here: only _resource drops.
}
// Vec elements: front to back.
println!("\n=== Vec element drop order ===");
{
let _v = vec![Named("vec[0]"), Named("vec[1]"), Named("vec[2]")];
println!(" Vec is alive");
// The scope ends here: the drop order is vec[0], vec[1], vec[2].
}
}
01_06_drop_order.rs prints these lines for the code above:
=== Local variable drop order ===
All three are alive
Dropped: third
Dropped: second
Dropped: first
=== Early drop with std::mem::drop ===
Both alive — doing work...
Dropped: mutex_guard
Guard released — resource still alive
Dropped: resource
=== Vec element drop order ===
Vec is alive
Dropped: vec[0]
Dropped: vec[1]
Dropped: vec[2]
Figure: Drop Order — Locals vs. Struct Fields vs. Vec Elements
clone_from: Reusing Existing Allocations
Most developers know .clone(). Fewer developers know .clone_from(). Its signature is:
// A provided method of the Clone trait. `self` is the destination.
fn clone_from(&mut self, source: &Self) { ... }
The default implementation is *self = source.clone(). It discards the old value and makes a new allocation. But String and Vec override the method to use the existing heap buffer again when the buffer is sufficiently large. This prevents one deallocation and one new allocation.
Use case: A processing loop that reuses a buffer each iteration
fn main() {
// The destination: a string with a heap buffer of at least 100 bytes.
let mut buffer = String::with_capacity(100);
buffer.push_str("old content that allocated a nice big buffer");
let ptr_before = buffer.as_ptr(); // the address of the heap buffer
let cap_before = buffer.capacity();
println!("Before clone_from:");
println!(" buffer = {buffer:?}");
println!(" capacity = {cap_before}");
let source = String::from("new");
buffer.clone_from(&source); // copies "new" into the existing buffer
let ptr_after = buffer.as_ptr();
let cap_after = buffer.capacity();
println!("\nAfter clone_from:");
println!(" buffer = {buffer:?}");
println!(" capacity = {cap_after}");
assert_eq!(buffer, "new");
// The pointer is the same: clone_from used the same allocation.
assert_eq!(ptr_before, ptr_after);
// The capacity is the same: clone_from did not shrink the buffer.
assert_eq!(cap_before, cap_after);
// A processing loop that uses one buffer for all the messages.
println!("\n--- Simulated processing loop ---");
let incoming_messages = vec![
String::from("msg-alpha"),
String::from("msg-beta-longer"),
String::from("msg-gamma"),
];
let mut work_buffer = String::with_capacity(64);
for msg in &incoming_messages {
// Each message fits in the 64 bytes, so this call does not allocate.
work_buffer.clone_from(msg);
println!(" Processing: {work_buffer:?} (cap={})", work_buffer.capacity());
}
}
01_04_clone_from.rs prints these lines for the code above:
Before clone_from:
buffer = "old content that allocated a nice big buffer"
capacity = 100
After clone_from:
buffer = "new"
capacity = 100
--- Simulated processing loop ---
Processing: "msg-alpha" (cap=64)
Processing: "msg-beta-longer" (cap=64)
Processing: "msg-gamma" (cap=64)
The capacity stays at 64 in all three iterations. Without clone_from, each iteration would allocate a new buffer and deallocate the old buffer.
The general rule: when you overwrite a variable that already has an allocation, prefer dest.clone_from(&src) to dest = src.clone().
Performance Implications of Clone on Collections
A clone has a cost, and the cost depends very much on the contents of the collection.
Vec<i32>.clone()does one memcpy of the backing buffer. It is fast.Vec<String>.clone()clones eachStringin the vector, and each clone allocates. The cost is O(n) in the element count and in the total string length.HashMap<String, Vec<u8>>.clone()clones each key and each value. It is expensive.
Use case: Measuring and avoiding clone costs
use std::time::Instant;
fn main() {
// 100,000 strings: each string has its own heap buffer.
let big_vec: Vec<String> = (0..100_000).map(|i| format!("element-{i:06}")).collect();
let start = Instant::now();
let cloned_vec = big_vec.clone(); // clones each String: one allocation for each element
let clone_time = start.elapsed(); // the Duration of the clone
println!(
"Cloning Vec<String> with {} elements: {:?}",
big_vec.len(),
clone_time
);
assert_eq!(big_vec.len(), cloned_vec.len());
// 100,000 integers: all the data is in one buffer.
let int_vec: Vec<i32> = (0..100_000).collect();
let start = Instant::now();
let cloned_ints = int_vec.clone(); // one memcpy of the backing buffer
let int_clone_time = start.elapsed();
println!(
"Cloning Vec<i32> with {} elements: {:?}",
int_vec.len(),
int_clone_time
);
assert_eq!(int_vec.len(), cloned_ints.len());
// `.max(1)` prevents a division by zero if the measured time is 0 ns.
let ratio = clone_time.as_nanos() as f64 / int_clone_time.as_nanos().max(1) as f64;
println!("\nVec<String> clone was roughly {ratio:.0}x slower than Vec<i32> clone");
}
01_05_clone_performance.rs prints these lines for the code above. The times and the ratio are different in each run:
Cloning Vec<String> with 100000 elements: 1.46925ms # (varies)
Cloning Vec<i32> with 100000 elements: 30.75µs # (varies)
Vec<String> clone was roughly 48x slower than Vec<i32> clone # (varies)
(The exact numbers vary by machine, but the order-of-magnitude difference is consistent.)
Strategies to avoid unnecessary clones
- Pass a slice (
&[T]), and do not clone the collection. If you only read the data, borrow it. - Clone one element, not the full collection. If you need one item, clone that item.
- Consume the collection with
into_iter(). If you do not need the original collection after this point, move the elements and do not clone them. - Use
clone_fromwhen you overwrite a value that has an allocation (see theclone_fromsection).
Summary
| Trait | Method | Implicit? | Cost | Requires |
|---|---|---|---|---|
Copy | None (the compiler inserts a memcpy) | Yes | Low (bitwise copy) | All fields must be Copy, and the type has no Drop impl |
Clone | .clone() | No (explicit call) | Arbitrary (may allocate) | All fields must be Clone |
Drop | .drop() (the compiler calls it) | Yes | Arbitrary (cleanup code) | The type cannot be Copy |
The three traits make one system:
Copymeans that duplication is trivially safe: the compiler only copies the bytes.Clonemeans that duplication needs work: you call theclonemethod.Dropmeans that destruction needs work: the compiler calls the destructor.Copytogether withDropis not permitted, because trivial duplication and non-trivial destruction are incompatible.
clone_from is the performance tool for Clone types. It lets you use an existing allocation again when you overwrite a value.
Code Examples
| File | Description |
|---|---|
01_01_copy_semantics.rs | Implicit copies of primitives, tuples, and custom structs |
01_02_clone_vs_copy.rs | Explicit clones of String, Vec, and nested structs |
01_03_copy_drop_exclusion.rs | Why a type cannot implement Copy and Drop together |
01_04_clone_from.rs | How clone_from uses an existing allocation again |
01_05_clone_performance.rs | The clone cost of collections, and how to prevent unnecessary clones |
01_06_drop_order.rs | Drop order of local variables, struct fields, and Vec elements, and the early drop |