Move, Copy & Clone in Rust
Every time you assign a value or pass it to a function in Rust, one of three things happens: the value is moved, copied, or cloned. Understanding which one applies — and why — is essential for writing idiomatic, efficient Rust code.
Move Semantics
A move transfers ownership from one variable to another. After a move, the original variable is considered uninitialised and the compiler will reject any attempt to use it:
let s1 = String::from("hello");
let s2 = s1; // ownership moves from s1 to s2
println!("{}", s2); // OK — s2 owns the value
println!("{}", s1); // COMPILE ERRORMoves apply to heap-owning types — types that allocate memory on the heap whose size cannot be known at compile time. Examples: String, Vec<T>, Box<T>, HashMap<K, V>.
When a move happens, only the small stack header (pointer, length, capacity) is copied. The heap data stays in place. The original variable is invalidated to ensure only one owner calls drop on that heap memory.
The Copy Trait
Types that implement the Copy trait are duplicated bitwise on assignment, rather than moved. Both the original and the new variable remain valid:
let x: i32 = 42;
let y = x; // x is COPIED — no move
println!("x = {}, y = {}", x, y); // both are validThis works because Copy types live entirely on the stack. Their size is fixed and known at compile time, and duplicating them is just a memcopy of a few bytes — cheap enough that Rust does it automatically.
Which Types Implement Copy?
All integer types —
i8,i16,i32,i64,i128,isize,u8,u16,u32,u64,u128,usizeFloating-point types —
f32,f64The boolean type —
boolThe character type —
charTuples, if every element type is
Copy— e.g.(i32, bool)isCopy, but(i32, String)is notFixed-size arrays, if the element type is
Copy— e.g.[i32; 4]isCopyReferences (
&T) areCopy— sharing a borrow is cheap and safe
Types That Are NOT Copy
Any type that owns heap data does NOT implement Copy. Duplicating the stack header while leaving the heap buffer shared would break ownership rules (both copies would try to free the same memory):
String— owns a growable heap bufferVec<T>— owns a growable heap arrayBox<T>— owns a heap-allocated valueHashMap<K, V>— owns a heap-allocated hash tableAny type that implements
Drop— if you wrote custom cleanup logic, it cannot be trivially copied
let v1: Vec<i32> = vec![1, 2, 3];
let v2 = v1; // moved, NOT copied
println!("{:?}", v2); // OK
println!("{:?}", v1); // COMPILE ERROR — v1 was movedThe Clone Trait: Explicit Deep Copy
When you genuinely need a second, independent copy of a heap-owning value, you call .clone(). This performs a deep copy — both the stack header and the heap data are duplicated, producing two fully independent values:
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy — new heap allocation, new stack header
println!("s1 = {}", s1); // still valid — s1 owns its own copy
println!("s2 = {}", s2); // s2 owns a separate copyAfter .clone(), both s1 and s2 own separate heap allocations. Modifying or dropping one has no effect on the other. When each goes out of scope, its own heap buffer is freed independently.
Copy vs Clone: The Key Differences
Property | Copy | Clone |
|---|---|---|
How triggered | Implicit — happens on assignment, function call | Explicit — you must call |
What is duplicated | Stack bits only (shallow copy) | Stack bits + heap data (deep copy) |
Cost | Negligible — just a few bytes | Potentially expensive — proportional to data size |
Requires heap allocation | No | Usually yes (for heap-owning types) |
Can a type implement both? | Yes — |
|
Can a type implement Drop? | No — | Yes — |
Implementing Copy on Your Own Types
You can derive Copy (and the required Clone) on your own structs, but only if every field is also Copy and the type does not implement Drop:
#[derive(Copy, Clone, Debug)]
struct Point {
x: f64,
y: f64,
}
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1; // copied — p1 still valid
println!("p1 = {:?}", p1);
println!("p2 = {:?}", p2);If you add a String field to Point, deriving Copy will fail because String is not Copy:
#[derive(Copy, Clone)]
struct BadPoint {
x: f64,
label: String, // String does not implement Copy
}Clone in Trait Bounds
In generic code, T: Clone is a very common bound — you are saying "I may need to clone a T inside this function." T: Copy is a stricter requirement and is used less often:
// Works for any type that can be explicitly cloned
fn duplicate<T: Clone>(item: T) -> (T, T) {
(item.clone(), item)
}
// Works only for types that are automatically copied
fn copy_both<T: Copy>(item: T) -> (T, T) {
(item, item) // no .clone() needed — copied implicitly
}
let pair = duplicate(String::from("hi")); // String: Clone but not Copy
println!("{:?}", pair); // ("hi", "hi")
let nums = copy_both(42i32); // i32: both Copy and Clone
println!("{:?}", nums); // (42, 42)When to clone() vs Restructure to Avoid It
.clone() is the right tool when you genuinely need two independent copies of data. But it is often a symptom of an ownership design issue that can be solved without extra allocation:
Use borrowing first. If a function only needs to read data, pass
&strinstead ofString, or&[T]instead ofVec<T>. No clone needed.Use references in structs. A struct that borrows its data (
&str) is cheaper than one that owns a clone (String) — but requires lifetime annotations.Use
Rc<T>orArc<T>to share ownership. When multiple parts of your program need to read the same data and you can't use references, a reference-counted pointer lets you clone a cheap pointer rather than the underlying data.Accept the clone. In non-hot-path code — startup, config parsing, error handling — clarity is worth more than saving an allocation. Do not optimise prematurely.
Rc and Arc: Sharing Without Cloning Data
Rc<T> (single-threaded reference counting) and Arc<T> (atomic, thread-safe reference counting) let multiple variables share ownership of the same heap value. Cloning an Rc or Arc just increments a counter — it does NOT copy the inner data:
use std::rc::Rc;
let data = Rc::new(vec![1, 2, 3]); // one heap allocation
let clone_a = Rc::clone(&data); // increments ref count — no vec copy
let clone_b = Rc::clone(&data); // same
println!("count = {}", Rc::strong_count(&data)); // 3
println!("{:?}", clone_a);When the last Rc pointing to the data is dropped, the inner value is freed. Use Arc instead of Rc when you need to share data across threads.
Common Types: Copy, Clone, or Neither?
Type | Copy? | Clone? | Notes |
|---|---|---|---|
| Yes | Yes | All numeric primitives |
| Yes | Yes | Single byte on stack |
| Yes | Yes | Unicode scalar value (4 bytes) |
| Yes | Yes | Tuple of Copy types |
| Yes | Yes | Fixed array of Copy type |
| Yes | Yes | Shared references are always Copy |
| No | Yes | Owns heap buffer; clone is deep copy |
| No | Yes | Clone duplicates entire buffer |
| No | Yes (if T: Clone) | Unique heap pointer |
| No | Yes (if K,V: Clone) | Clone copies all entries |
| No | No | OS resources; cannot be duplicated |
| No | No | Must not be copied or cloned |
Move Semantics with Functions
The same rules apply when passing values to and returning values from functions. Heap types are moved; Copy types are copied:
fn print_string(s: String) {
println!("{}", s);
} // s is dropped here
fn double(n: i32) -> i32 {
n * 2
}
fn main() {
let owned = String::from("moved into function");
print_string(owned);
// println!("{}", owned); // ERROR — owned was moved
let num = 21;
let result = double(num);
println!("num={}, result={}", num, result); // num is still valid — i32 is Copy
}