896-clone-copy — Clone and Copy Traits
Tutorial
The Problem
Rust's ownership model means that values are moved by default. For types where copying is cheap and always valid — integers, booleans, simple structs — implicit copying is desirable. For types with heap allocation — String, Vec, complex structs — copying is potentially expensive and must be explicit. Rust encodes this distinction in two traits: Copy (implicit bitwise copy, zero-cost) and Clone (explicit .clone(), potentially expensive). Seeing .clone() in code signals a potentially costly heap duplication. Not seeing it means the copy is stack-only. This visual distinction makes performance characteristics readable from the code.
🎯 Learning Outcomes
Copy (implicit) and Clone (explicit)Copy and Clone for simple structs and recognize when Copy is appropriateString, Vec, and other heap-owning types cannot be Copy.clone() as a visible signal of heap allocation in code reviewsCode Example
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Point { pub x: f64, pub y: f64 }
impl Point {
pub fn translate(self, dx: f64, dy: f64) -> Self {
Self { x: self.x + dx, y: self.y + dy }
}
}
fn copy_demo() {
let origin = Point { x: 0.0, y: 0.0 };
let moved = origin.translate(1.0, 2.0); // origin copied implicitly
assert_eq!(origin, Point { x: 0.0, y: 0.0 }); // still valid
assert_eq!(moved.x, 1.0);
let x: i32 = 42;
let y = x; // silent bitwise copy — both x and y are valid
assert_eq!(x, y);
}Key Differences
Copy types copy silently; Clone types require .clone(). OCaml passes a shared pointer always — no per-value choice..clone() signals potential allocation; in OCaml, copying is never visible in the source — profiling is required to detect it.Copy only if all its fields are Copy; OCaml values are always shareable regardless of field types.String, Vec, Box cannot be Copy (they own heap memory); in OCaml, all heap-allocated values are shareable via GC.OCaml Approach
OCaml has no Copy/Clone distinction. All values (including strings and lists) are implicitly sharable — passing a value to a function passes a pointer, not a copy. Immutability makes sharing safe: OCaml strings used to be mutable (a historical mistake); Bytes.t is now the mutable string type. For actual copying: String.copy (deprecated), Bytes.copy, Array.copy. OCaml's structural sharing means no distinction between "move" and "share" — the GC handles it.
Full Source
#![allow(clippy::all)]
// Example 896: Clone and Copy Traits
//
// Copy: bitwise copy, implicit, for simple stack types (i32, f64, bool, tuples of Copy, etc.)
// Clone: explicit deep copy via .clone(), for heap-owning types (String, Vec, etc.)
//
// The key insight: seeing `.clone()` in code signals a potentially expensive heap allocation.
// Not seeing it means the copy is cheap (stack-only). No hidden costs.
// --- Copy types ---
/// Demonstrates Copy semantics: assignment silently duplicates the value.
/// Both the original and the copy are independently valid after assignment.
pub fn copy_integer(x: i32) -> (i32, i32) {
let y = x; // bitwise copy — x is still valid
(x, y)
}
/// Tuples of Copy types are themselves Copy.
pub fn copy_tuple(p: (f64, f64), dx: f64, dy: f64) -> (f64, f64) {
let q = p; // p is copied, both remain valid
(q.0 + dx, q.1 + dy)
}
/// A simple point struct that derives both Copy and Clone.
/// Copy enables silent duplication; Clone enables `.clone()` calls.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
/// Returns a new translated point — original is preserved via Copy.
pub fn translate(self, dx: f64, dy: f64) -> Self {
Self {
x: self.x + dx,
y: self.y + dy,
}
}
}
// --- Clone types ---
/// Demonstrates Clone semantics: explicit `.clone()` required for heap-owning types.
/// After `s2 = s1.clone()`, both strings are independent heap allocations.
pub fn clone_string(s: &str) -> (String, String) {
let s1 = s.to_owned();
let s2 = s1.clone(); // explicit — you see the cost
(s1, s2)
}
/// Clones a Vec: each clone is a fresh heap allocation with copied elements.
pub fn clone_vec(v: &[i32]) -> (Vec<i32>, Vec<i32>) {
let v1 = v.to_vec();
let v2 = v1.clone(); // explicit deep copy
(v1, v2)
}
/// Shows that modifying a cloned value does not affect the original.
pub fn independent_after_clone(original: &str) -> (String, String) {
let s1 = original.to_owned();
let mut s2 = s1.clone();
s2.push_str(" (copy)");
(s1, s2)
}
// --- Mixed: a struct that owns heap data, so only Clone (not Copy) ---
/// A named point that owns its label string — cannot be Copy, only Clone.
#[derive(Debug, Clone, PartialEq)]
pub struct NamedPoint {
pub label: String,
pub x: f64,
pub y: f64,
}
impl NamedPoint {
pub fn new(label: &str, x: f64, y: f64) -> Self {
Self {
label: label.to_owned(),
x,
y,
}
}
pub fn translate(&self, dx: f64, dy: f64) -> Self {
Self {
label: self.label.clone(), // must clone the String
x: self.x + dx,
y: self.y + dy,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_copy_integer_both_valid() {
let (x, y) = copy_integer(42);
assert_eq!(x, 42);
assert_eq!(y, 42);
}
#[test]
fn test_copy_tuple_independence() {
let p = (1.0_f64, 2.0_f64);
let translated = copy_tuple(p, 3.0, 4.0);
// original p is still (1.0, 2.0) — Copy means independent
assert_eq!(p, (1.0, 2.0));
assert_eq!(translated, (4.0, 6.0));
}
#[test]
fn test_copy_struct_translate_preserves_original() {
let origin = Point::new(0.0, 0.0);
let moved = origin.translate(1.0, 2.0);
// origin unchanged — Point is Copy
assert_eq!(origin, Point::new(0.0, 0.0));
assert_eq!(moved, Point::new(1.0, 2.0));
}
#[test]
fn test_clone_string_independence() {
let (s1, s2) = clone_string("hello");
assert_eq!(s1, "hello");
assert_eq!(s2, "hello");
// they are equal but independent allocations
assert_eq!(s1, s2);
}
#[test]
fn test_clone_vec_independence() {
let (v1, v2) = clone_vec(&[1, 2, 3]);
assert_eq!(v1, vec![1, 2, 3]);
assert_eq!(v2, vec![1, 2, 3]);
}
#[test]
fn test_independent_after_clone_no_aliasing() {
let (original, copy) = independent_after_clone("hello");
assert_eq!(original, "hello");
assert_eq!(copy, "hello (copy)");
// modifying the clone did not affect the original
assert_ne!(original, copy);
}
#[test]
fn test_named_point_clone_and_translate() {
let np = NamedPoint::new("origin", 0.0, 0.0);
let moved = np.translate(5.0, -3.0);
// np is still valid — we borrowed it in translate
assert_eq!(np.label, "origin");
assert_eq!(np.x, 0.0);
assert_eq!(moved.label, "origin");
assert_eq!(moved.x, 5.0);
assert_eq!(moved.y, -3.0);
}
#[test]
fn test_named_point_clone_is_independent() {
let np1 = NamedPoint::new("point", 1.0, 2.0);
let mut np2 = np1.clone();
np2.label = "clone".to_owned();
assert_eq!(np1.label, "point"); // original unaffected
assert_eq!(np2.label, "clone");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_copy_integer_both_valid() {
let (x, y) = copy_integer(42);
assert_eq!(x, 42);
assert_eq!(y, 42);
}
#[test]
fn test_copy_tuple_independence() {
let p = (1.0_f64, 2.0_f64);
let translated = copy_tuple(p, 3.0, 4.0);
// original p is still (1.0, 2.0) — Copy means independent
assert_eq!(p, (1.0, 2.0));
assert_eq!(translated, (4.0, 6.0));
}
#[test]
fn test_copy_struct_translate_preserves_original() {
let origin = Point::new(0.0, 0.0);
let moved = origin.translate(1.0, 2.0);
// origin unchanged — Point is Copy
assert_eq!(origin, Point::new(0.0, 0.0));
assert_eq!(moved, Point::new(1.0, 2.0));
}
#[test]
fn test_clone_string_independence() {
let (s1, s2) = clone_string("hello");
assert_eq!(s1, "hello");
assert_eq!(s2, "hello");
// they are equal but independent allocations
assert_eq!(s1, s2);
}
#[test]
fn test_clone_vec_independence() {
let (v1, v2) = clone_vec(&[1, 2, 3]);
assert_eq!(v1, vec![1, 2, 3]);
assert_eq!(v2, vec![1, 2, 3]);
}
#[test]
fn test_independent_after_clone_no_aliasing() {
let (original, copy) = independent_after_clone("hello");
assert_eq!(original, "hello");
assert_eq!(copy, "hello (copy)");
// modifying the clone did not affect the original
assert_ne!(original, copy);
}
#[test]
fn test_named_point_clone_and_translate() {
let np = NamedPoint::new("origin", 0.0, 0.0);
let moved = np.translate(5.0, -3.0);
// np is still valid — we borrowed it in translate
assert_eq!(np.label, "origin");
assert_eq!(np.x, 0.0);
assert_eq!(moved.label, "origin");
assert_eq!(moved.x, 5.0);
assert_eq!(moved.y, -3.0);
}
#[test]
fn test_named_point_clone_is_independent() {
let np1 = NamedPoint::new("point", 1.0, 2.0);
let mut np2 = np1.clone();
np2.label = "clone".to_owned();
assert_eq!(np1.label, "point"); // original unaffected
assert_eq!(np2.label, "clone");
}
}
Deep Comparison
OCaml vs Rust: Clone and Copy
Side-by-Side Code
OCaml
(* OCaml: all values are implicitly shared/copied by the GC.
No distinction between cheap stack copy and expensive heap copy. *)
type point = { x : float; y : float }
let translate p dx dy = { x = p.x +. dx; y = p.y +. dy }
let () =
let origin = { x = 0.0; y = 0.0 } in
let moved = translate origin 1.0 2.0 in
(* origin is still valid — GC manages both records implicitly *)
assert (origin.x = 0.0);
assert (moved.x = 1.0);
(* Strings: structural sharing, no explicit clone needed *)
let s1 = "hello" in
let s2 = s1 ^ " world" in (* creates new string, GC handles old *)
assert (s1 = "hello");
assert (s2 = "hello world");
print_endline "ok"
Rust (Copy — stack types, implicit duplication)
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Point { pub x: f64, pub y: f64 }
impl Point {
pub fn translate(self, dx: f64, dy: f64) -> Self {
Self { x: self.x + dx, y: self.y + dy }
}
}
fn copy_demo() {
let origin = Point { x: 0.0, y: 0.0 };
let moved = origin.translate(1.0, 2.0); // origin copied implicitly
assert_eq!(origin, Point { x: 0.0, y: 0.0 }); // still valid
assert_eq!(moved.x, 1.0);
let x: i32 = 42;
let y = x; // silent bitwise copy — both x and y are valid
assert_eq!(x, y);
}
Rust (Clone — heap types, explicit deep copy)
fn clone_demo() {
let s1 = String::from("hello");
let s2 = s1.clone(); // explicit — you see the heap allocation cost
assert_eq!(s1, s2); // s1 still valid, s2 is a new allocation
let v1 = vec![1, 2, 3];
let v2 = v1.clone(); // another explicit deep copy
assert_eq!(v1, v2);
}
Rust (Clone-only struct — owns heap data)
#[derive(Debug, Clone, PartialEq)]
pub struct NamedPoint {
pub label: String, // owns heap data → cannot be Copy
pub x: f64,
pub y: f64,
}
impl NamedPoint {
pub fn translate(&self, dx: f64, dy: f64) -> Self {
Self {
label: self.label.clone(), // must clone the String explicitly
x: self.x + dx,
y: self.y + dy,
}
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Cheap copy | Implicit (GC handles everything) | Copy trait — bitwise, silent |
| Expensive copy | Implicit (GC allocates new heap object) | Clone trait — explicit .clone() call |
| Stack-only struct | type point = { x: float; y: float } | #[derive(Copy, Clone)] struct Point |
| Heap-owning struct | type named = { label: string; x: float } | #[derive(Clone)] struct NamedPoint { label: String, .. } |
| String copy | let s2 = s1 (shared/copied implicitly) | let s2 = s1.clone() (explicit heap allocation) |
| Function taking struct | let f p = ... (implicit copy or share) | fn f(p: Point) (moved or copied, depending on Copy) |
Key Insights
Copy (cheap, stack-only) is invisible while Clone (potentially expensive, heap-allocating) must be written explicitly, making costs visible at the call site.Copy is a subset of Clone**: A Copy type must also implement Clone; Clone is the general interface, Copy is the optimized silent variant. If a type contains any non-Copy field (like String), it cannot be Copy, only Clone.Copy are moved on assignment — the original binding becomes invalid. Types with Copy are silently duplicated. OCaml has neither concept because the GC tracks all references..clone() in a Rust code review, it is a deliberate signal — something heap-heavy is being duplicated. In OCaml you never get that signal, which can hide performance problems.String field to an otherwise stack-only struct removes the ability to derive Copy. This makes the memory model explicit in the type definition itself, not just at use sites.When to Use Each Style
**Derive Copy when:** the struct holds only stack-sized, trivially-copyable fields (i32, f64, bool, arrays of Copy, etc.) and implicit duplication is semantically correct (e.g., coordinate types, small value objects).
**Derive only Clone when:** the struct owns heap data (String, Vec, Box, etc.) and copies must be explicit to make their cost visible to the reader.
Exercises
Matrix2x2 struct with four f64 fields, derive Copy + Clone, and implement matrix multiplication returning a new matrix.cloned_pipeline(data: &[String]) -> Vec<String> that clones elements conditionally — only those passing a filter — and explain why .clone() is needed.Config struct with String fields and #[derive(Clone)], then write apply_override(base: &Config, override: Config) -> Config using struct update syntax.