Rust Clone vs Copy: Duplication Semantics

Rust's ownership system prevents data races and memory errors at compile time, but it comes with a learning curve. One of the first challenges developers encounter is understanding when values are...

Key Insights

  • Copy enables implicit, bitwise duplication for stack-only types, while Clone requires explicit .clone() calls and supports heap-allocated data
  • Copy types must be trivially copyable (no heap allocations or custom logic), making Copy a subset of Clone—every Copy type must also implement Clone
  • Prefer references over cloning when possible; use Copy for small, simple types and Clone for complex types where explicit duplication makes ownership transfer clear

Introduction to Ownership and Duplication

Rust’s ownership system prevents data races and memory errors at compile time, but it comes with a learning curve. One of the first challenges developers encounter is understanding when values are moved versus copied, and how to intentionally duplicate data when needed.

Consider this common beginner mistake:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is moved to s2
    println!("{}", s1); // Compile error: value borrowed after move
}

This fails because String owns heap-allocated data. When you assign s1 to s2, Rust moves ownership rather than copying the data. This prevents double-free errors, but it means s1 is no longer valid.

Rust provides two traits to handle duplication explicitly: Copy for cheap, implicit duplication and Clone for explicit, potentially expensive duplication. Understanding when and how to use each is fundamental to writing idiomatic Rust.

The Copy Trait: Implicit Duplication

The Copy trait marks types that can be duplicated by simply copying bits. When a type implements Copy, assignment creates a duplicate rather than moving ownership. This happens implicitly—no special syntax required.

All primitive types implement Copy:

fn main() {
    let x = 5;
    let y = x; // x is copied, not moved
    println!("x: {}, y: {}", x, y); // Both are valid
    
    let a = true;
    let b = a; // bools are Copy
    println!("a: {}, b: {}", a, b);
    
    let c = 'z';
    let d = c; // chars are Copy
    println!("c: {}, d: {}", c, d);
}

Tuples and arrays of Copy types are also Copy:

fn main() {
    let point = (3, 4);
    let point_copy = point;
    println!("{:?} and {:?}", point, point_copy); // Both valid
    
    let bytes = [1u8, 2u8, 3u8];
    let bytes_copy = bytes;
    println!("{:?} and {:?}", bytes, bytes_copy); // Both valid
}

You can implement Copy for your own types, but only if all fields are Copy and the type doesn’t manage resources:

#[derive(Copy, Clone, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn process_point(p: Point) {
    println!("Processing {:?}", p);
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    process_point(p1);
    process_point(p1); // p1 is still valid because Point is Copy
}

Note that Copy requires Clone. This is because Copy is a marker trait with no methods—it piggybacks on Clone’s implementation. The compiler ensures that Clone for Copy types performs a bitwise copy.

The critical restriction: Copy types cannot contain heap allocations or implement Drop. If a type needs custom cleanup logic, implicit copying would be dangerous. This is why String, Vec, and Box cannot be Copy.

The Clone Trait: Explicit Duplication

Clone provides explicit duplication through the .clone() method. Unlike Copy, Clone can perform deep copies of heap-allocated data and execute arbitrary code during duplication.

Here’s Clone with heap-allocated types:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // Explicit clone
    println!("{} and {}", s1, s2); // Both valid
    
    let v1 = vec![1, 2, 3];
    let v2 = v1.clone(); // Clones heap data
    println!("{:?} and {:?}", v1, v2);
}

For simple structs, derive Clone:

#[derive(Clone, Debug)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let person1 = Person {
        name: String::from("Alice"),
        age: 30,
    };
    let person2 = person1.clone();
    println!("{:?}", person1); // Still valid
    println!("{:?}", person2);
}

For complex types, implement Clone manually:

struct Database {
    connection: String,
    cache: Vec<String>,
}

impl Clone for Database {
    fn clone(&self) -> Self {
        println!("Cloning database connection...");
        Database {
            connection: self.connection.clone(),
            cache: self.cache.clone(), // Deep copy the cache
        }
    }
}

fn main() {
    let db1 = Database {
        connection: String::from("localhost:5432"),
        cache: vec![String::from("cached_data")],
    };
    let db2 = db1.clone(); // Explicit and visible
}

Manual implementations let you customize cloning behavior—skip expensive fields, log operations, or implement copy-on-write semantics.

Copy vs Clone: Key Differences

Aspect Copy Clone
Invocation Implicit (automatic) Explicit (.clone())
Cost Always cheap (bitwise) Potentially expensive
Heap Data Not allowed Allowed
Drop Implementation Cannot have Drop Can have Drop
Trait Requirement Requires Clone Standalone
Use Case Small, stack-only types Complex types with resources

Here’s a direct comparison:

// Copy type: implicit duplication
#[derive(Copy, Clone, Debug)]
struct CopyStruct {
    value: i32,
}

// Clone-only type: explicit duplication
#[derive(Clone, Debug)]
struct CloneStruct {
    data: String,
}

fn main() {
    // Copy behavior
    let c1 = CopyStruct { value: 42 };
    let c2 = c1; // Implicit copy
    println!("{:?} and {:?}", c1, c2); // Both valid
    
    // Clone behavior
    let cl1 = CloneStruct { data: String::from("hello") };
    let cl2 = cl1.clone(); // Must be explicit
    println!("{:?} and {:?}", cl1, cl2);
    
    // This would fail: let cl3 = cl1; println!("{:?}", cl1);
}

Common Patterns and Pitfalls

Pitfall: Trying to Copy types with heap data

// This won't compile
#[derive(Copy, Clone)] // Error: Copy trait cannot be implemented
struct Container {
    items: Vec<i32>, // Vec allocates on heap
}

If any field isn’t Copy, the struct can’t be Copy. Use Clone instead.

Pitfall: Unnecessary clones in function parameters

// Bad: unnecessary clone
fn process_bad(data: String) {
    println!("{}", data);
}

fn main() {
    let s = String::from("hello");
    process_bad(s.clone()); // Wasteful clone
    println!("{}", s);
}

// Good: use a reference
fn process_good(data: &str) {
    println!("{}", data);
}

fn main() {
    let s = String::from("hello");
    process_good(&s); // No clone needed
    println!("{}", s);
}

Only clone when you truly need ownership. References are usually sufficient.

Pattern: Clone-on-write with Cow

use std::borrow::Cow;

fn process_data(input: &str) -> Cow<str> {
    if input.contains("bad") {
        // Only clone when modification is needed
        Cow::Owned(input.replace("bad", "good"))
    } else {
        // No clone needed
        Cow::Borrowed(input)
    }
}

fn main() {
    let s1 = "hello world";
    let s2 = "bad word";
    
    let r1 = process_data(s1); // No allocation
    let r2 = process_data(s2); // Allocates only when needed
    
    println!("{}, {}", r1, r2);
}

Pattern: Reference counting instead of cloning

use std::rc::Rc;

#[derive(Debug)]
struct LargeData {
    bytes: Vec<u8>,
}

fn main() {
    let data = Rc::new(LargeData {
        bytes: vec![0; 1000],
    });
    
    let data2 = Rc::clone(&data); // Cheap: only increments counter
    let data3 = Rc::clone(&data);
    
    println!("Reference count: {}", Rc::strong_count(&data)); // 3
}

Rc (single-threaded) and Arc (thread-safe) provide shared ownership without cloning the underlying data.

Best Practices and Guidelines

When to implement Copy:

  • Type fits in a few CPU registers (typically ≤ 16 bytes)
  • All fields are Copy
  • No heap allocations
  • No custom drop logic
  • Copying is semantically meaningful

Examples: coordinates, IDs, configuration flags, small fixed-size arrays.

When to implement Clone only:

  • Type contains heap-allocated data
  • Type manages resources (file handles, connections)
  • Copying is expensive enough to warrant explicit calls
  • Type implements Drop

Examples: collections, strings, smart pointers, complex business objects.

Decision tree in code:

// 1. Small, simple type? → Copy
#[derive(Copy, Clone)]
struct UserId(u64);

// 2. Contains String/Vec? → Clone only
#[derive(Clone)]
struct User {
    name: String,
    id: UserId, // UserId is Copy, but String makes User Clone-only
}

// 3. Resource management? → Clone with custom logic
struct FileHandle {
    path: String,
}

impl Clone for FileHandle {
    fn clone(&self) -> Self {
        // Custom logic: maybe open a new handle
        println!("Opening new file handle for {}", self.path);
        FileHandle {
            path: self.path.clone(),
        }
    }
}

// 4. Need shared ownership? → Use Rc/Arc instead
use std::sync::Arc;
struct SharedData {
    inner: Arc<Vec<u8>>,
}

Performance considerations:

  • Copy has zero runtime cost—it’s just a memcpy
  • Clone cost depends on implementation—measure if it matters
  • Avoid cloning in hot loops; prefer references or Rc/Arc
  • Profile before optimizing; most clones aren’t bottlenecks

API design:

When designing functions, prefer this order:

  1. Borrowed references (&T, &mut T) when you don’t need ownership
  2. Copy types by value (cheap)
  3. Clone types by value only when you need ownership
  4. Accept impl Clone or T: Clone bounds when callers should decide

The key insight: make expensive operations explicit. Copy hides the duplication because it’s cheap; Clone requires .clone() to signal that something non-trivial is happening. This explicitness helps readers understand where allocations occur and makes performance characteristics visible in the code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.