zero cost abstraction zero cost abstraction

Zero-Cost Abstractions in Rust: Understanding Copy Types and Clone Semantics

Rust is celebrated for its ability to write high-level, expressive code without sacrificing performance. One of the most powerful — and often misunderstood — features enabling this promise is the Copy trait and the broader concept of zero-cost abstractions. If you’ve ever wondered why some Rust values can be used after assignment while others can’t, or why passing an integer to a function doesn’t “move” it away, this article is for you.

In this comprehensive guide, we’ll cover:

  • What “zero-cost copy” means in Rust
  • The Copy and Clone traits and how they differ
  • Which types implement Copy by default
  • How to implement Copy on your own types
  • Real-world performance implications
  • Common pitfalls and best practices

Table of Contents

  1. The Ownership Problem Copy Solves
  2. What Is the Copy Trait?
  3. Copy vs Clone: A Critical Distinction
  4. Types That Are Copy by Default
  5. Implementing Copy on Custom Types
  6. How Copy Works Under the Hood
  7. Zero-Cost Abstractions Explained
  8. When NOT to Use Copy
  9. Performance Benchmarks and Analysis
  10. Common Pitfalls
  11. Best Practices

1. The Ownership Problem Copy Solves

Before we understand Copy, we need to understand the problem it solves. Rust’s ownership system means that every value has exactly one owner at any given time. When you assign a value to another variable, the ownership moves.

fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED into s2

// This will cause a compile-time error!
println!("{}", s1); // error[E0382]: borrow of moved value: `s1`
}

This is intentional — it prevents double-free errors and ensures memory safety. However, for simple, small values like integers or booleans, this behavior would be unnecessarily restrictive and verbose.

Consider how painful this would be:

fn add_one(x: i32) -> i32 {
x + 1
}

fn main() {
let num = 5;
let result = add_one(num);

// Without Copy, num would be gone here!
// We'd have to do: let result = add_one(num.clone());
println!("num: {}, result: {}", num, result); // Works fine because i32 is Copy
}

The Copy trait elegantly solves this by telling the Rust compiler: “This type is safe and cheap to duplicate bit-for-bit.”

2. What Is the Copy Trait?

The Copy trait is a marker trait — it carries no methods you need to implement. Its mere presence on a type signals to the compiler that values of that type should be copied (duplicated on the stack) rather than moved when assigned or passed to a function.

// From the Rust standard library (simplified)
pub trait Copy: Clone {
// This trait has no methods.
// It's purely a marker for the compiler.
}

Note that Copy has a supertrait requirement: any type that is Copy must also implement Clone. This makes logical sense — if you can trivially copy something, you can certainly clone it.

A Simple Demonstration

fn main() {
// i32 implements Copy
let x: i32 = 42;
let y = x; // x is COPIED, not moved

println!("x = {}", x); // Works! x is still valid
println!("y = {}", y); // Also works!

// String does NOT implement Copy
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED

// println!("{}", s1); // This would fail to compile
println!("s2 = {}", s2);
}

Copy Semantics Across Function Boundaries

fn print_number(n: i32) {
println!("The number is: {}", n);
}

fn consume_string(s: String) {
println!("The string is: {}", s);
// s is dropped here
}

fn main() {
let num = 100;
print_number(num);
print_number(num); // Fine! num was copied, not moved

let text = String::from("Rust is awesome");
consume_string(text);
// consume_string(text); // Error! text was moved
}

3. Copy vs Clone: A Critical Distinction

Understanding the difference between Copy and Clone is fundamental to writing efficient Rust code.

Clone Without Copy

#[derive(Debug, Clone)]
struct Config {
host: String, // String is not Copy (owns heap data)
port: u16,
max_connections: u32,
}

fn main() {
let config1 = Config {
host: String::from("localhost"),
port: 8080,
max_connections: 100,
};

// Must explicitly call .clone() — this allocates a new String on the heap
let config2 = config1.clone();

println!("Config 1: {:?}", config1);
println!("Config 2: {:?}", config2);

// Modifying config2 doesn't affect config1
// Because they own separate heap allocations
}

Copy With Clone (Both Derived)

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

fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1; // Implicit copy — no .clone() needed

// Both p1 and p2 are valid
println!("p1: {:?}", p1);
println!("p2: {:?}", p2);

// .clone() also works, and is identical to assignment for Copy types
let p3 = p1.clone();
println!("p3: {:?}", p3);
}

Custom Clone Implementation

Sometimes you might be tempted to write custom clone logic for a type that also implements Copy. Technically, the Rust compiler does not forbid this. You are fully allowed to derive (or manually implement) Copy, while simultaneously writing a manual Clone implementation that executes entirely different logic—such as logging to the console or mutating internal data.

However, doing so is considered a severe anti-pattern and violates standard Rust conventions.

When a type implements Copy, developers expect an explicit x.clone() to behave identically to an implicit copy (let y = x;). If your explicit .clone() does something different or more expensive than the implicit bitwise copy, it will lead to highly confusing, non-deterministic bugs depending on whether the compiler inferred a move/copy or the developer explicitly called .clone().

Because of this, if you need custom clone logic, you should purposely omit the Copy trait. Here’s an example of a type where we want custom clone logic (e.g., explicitly tracking the duplication of a resource), meaning it cannot and should not be Copy:

#[derive(Debug)]
struct ConnectionPool {
size: usize,
connections: Vec<String>, // Cannot be Copy due to Vec
}

impl Clone for ConnectionPool {
fn clone(&self) -> Self {
println!("Creating a deep copy of the connection pool...");
ConnectionPool {
size: self.size,
connections: self.connections.clone(),
}
}
}

fn main() {
let pool = ConnectionPool {
size: 5,
connections: vec!["conn1".to_string(), "conn2".to_string()],
};

let pool_backup = pool.clone(); // Explicit, custom clone logic executes
println!("Original: {:?}", pool);
println!("Backup: {:?}", pool_backup);
}

4. Types That Are Copy by Default

The Rust standard library implements Copy for a wide range of primitive and composite types.

Primitive Types

fn demonstrate_primitives() {
// All integer types
let i8_val: i8 = 127;
let i16_val: i16 = 32_767;
let i32_val: i32 = 2_147_483_647;
let i64_val: i64 = 9_223_372_036_854_775_807;
let i128_val: i128 = 170_141_183_460_469_231_731_687_303_715_884_105_727;
let isize_val: isize = -1;

// All unsigned integer types
let u8_val: u8 = 255;
let u16_val: u16 = 65_535;
let u32_val: u32 = 4_294_967_295;
let u64_val: u64 = 18_446_744_073_709_551_615;
let u128_val: u128 = 340_282_366_920_938_463_463_374_607_431_768_211_455;
let usize_val: usize = 42;

// Floating point types
let f32_val: f32 = 3.14_f32;
let f64_val: f64 = 3.141_592_653_589_793_f64;

// Boolean
let bool_val: bool = true;

// Character
let char_val: char = 'R';

// All of these can be freely copied
let _copy_of_i32 = i32_val;
let _copy_of_f64 = f64_val;
let _copy_of_bool = bool_val;
let _copy_of_char = char_val;

println!("All originals still valid: {}, {}, {}, {}",
i32_val, f64_val, bool_val, char_val);
}

Tuples (When All Elements Are Copy)

fn demonstrate_tuples() {
// Tuple of Copy types — the tuple itself is Copy
let tuple1: (i32, f64, bool) = (42, 3.14, true);
let tuple2 = tuple1; // Copied!

println!("tuple1: {:?}", tuple1); // Still valid
println!("tuple2: {:?}", tuple2);

// Tuple with a non-Copy type — NOT Copy
let mixed: (i32, String) = (42, String::from("hello"));
let mixed2 = mixed; // This MOVES!

// println!("{:?}", mixed); // Error: value moved
println!("{:?}", mixed2);
}

Arrays (When Element Type Is Copy)

fn demonstrate_arrays() {
// Array of Copy elements — the array is Copy
let arr1: [i32; 5] = [1, 2, 3, 4, 5];
let arr2 = arr1; // Copied!

println!("arr1: {:?}", arr1); // Still valid
println!("arr2: {:?}", arr2);

// Small fixed-size arrays are ideal Copy types
let rgb: [u8; 3] = [255, 128, 0];
process_color(rgb);
process_color(rgb); // No problem — rgb was copied each time
println!("rgb still available: {:?}", rgb);
}

fn process_color(color: [u8; 3]) {
let r = color[0];
let g = color[1];
let b = color[2];
println!("R: {}, G: {}, B: {}", r, g, b);
}

References

fn demonstrate_references() {
let original = String::from("I own this data");

// Shared references (&T) are Copy
let ref1: &String = &original;
let ref2 = ref1; // ref1 is COPIED (both refs point to original)

println!("ref1: {}", ref1); // Both still valid
println!("ref2: {}", ref2);

// This is why you can pass &str to multiple functions
let greeting = "Hello, world!";
print_str(greeting);
print_str(greeting); // greeting (&str) is Copy
}

fn print_str(s: &str) {
println!("{}", s);
}

Raw Pointers

fn demonstrate_raw_pointers() {
let value: i32 = 42;
let ptr1: *const i32 = &value as *const i32;
let ptr2 = ptr1; // Raw pointers are Copy

unsafe {
println!("ptr1 points to: {}", *ptr1);
println!("ptr2 points to: {}", *ptr2);
}
// Note: Raw pointers don't carry lifetime guarantees — be careful!
}

5. Implementing Copy on Custom Types

You can implement Copy on your own types in two ways: via derive or manually.

Method 1: Derive Macro (Recommended)

// The easiest way — let the compiler do the work
#[derive(Debug, Copy, Clone, PartialEq)]
struct Vector2D {
x: f32,
y: f32,
}

impl Vector2D {
fn new(x: f32, y: f32) -> Self {
Vector2D { x, y }
}

fn magnitude(&self) -> f32 {
(self.x * self.x + self.y * self.y).sqrt()
}

fn add(self, other: Vector2D) -> Vector2D {
Vector2D {
x: self.x + other.x,
y: self.y + other.y,
}
}

fn scale(self, factor: f32) -> Vector2D {
Vector2D {
x: self.x * factor,
y: self.y * factor,
}
}
}

fn main() {
let v1 = Vector2D::new(3.0, 4.0);
let v2 = Vector2D::new(1.0, 2.0);

// Because Vector2D is Copy, we can use v1 and v2 freely
let v3 = v1.add(v2); // v1 and v2 are copied into add()
let v4 = v1.scale(2.0); // v1 is still available!
let magnitude = v1.magnitude(); // And again!

println!("v1: ({}, {})", v1.x, v1.y);
println!("v1 + v2: ({}, {})", v3.x, v3.y);
println!("v1 * 2: ({}, {})", v4.x, v4.y);
println!("Magnitude of v1: {:.2}", magnitude);
}

Method 2: Manual Implementation

#[derive(Debug, Clone)]
struct Color {
r: u8,
g: u8,
b: u8,
a: u8,
}

// Manual Copy implementation (though derive is preferred)
impl Copy for Color {}

impl Color {
fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Color { r, g, b, a }
}

fn blend(self, other: Color, factor: f32) -> Color {
let blend_channel = |a: u8, b: u8| -> u8 {
((a as f32 * (1.0 - factor)) + (b as f32 * factor)) as u8
};

Color {
r: blend_channel(self.r, other.r),
g: blend_channel(self.g, other.g),
b: blend_channel(self.b, other.b),
a: blend_channel(self.a, other.a),
}
}
}

fn main() {
let red = Color::new(255, 0, 0, 255);
let blue = Color::new(0, 0, 255, 255);

let purple = red.blend(blue, 0.5); // red is copied, still usable after

println!("Red: ({}, {}, {}, {})", red.r, red.g, red.b, red.a);
println!("Blue: ({}, {}, {}, {})", blue.r, blue.g, blue.b, blue.a);
println!("Purple: ({}, {}, {}, {})", purple.r, purple.g, purple.b, purple.a);
}

Struct with Enum (Both Must Be Copy)

#[derive(Debug, Copy, Clone, PartialEq)]
enum Direction {
North,
South,
East,
West,
}

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

impl GridPosition {
fn new(x: i32, y: i32, facing: Direction) -> Self {
GridPosition { x, y, facing }
}

fn move_forward(self) -> GridPosition {
match self.facing {
Direction::North => GridPosition { y: self.y + 1, ..self },
Direction::South => GridPosition { y: self.y - 1, ..self },
Direction::East => GridPosition { x: self.x + 1, ..self },
Direction::West => GridPosition { x: self.x - 1, ..self },
}
}

fn turn_right(self) -> GridPosition {
let new_direction = match self.facing {
Direction::North => Direction::East,
Direction::East => Direction::South,
Direction::South => Direction::West,
Direction::West => Direction::North,
};
GridPosition { facing: new_direction, ..self }
}
}

fn main() {
let start = GridPosition::new(0, 0, Direction::North);

// Because everything is Copy, we can freely explore positions
let after_move = start.move_forward();
let turned = start.turn_right(); // start still valid!
let after_both = start.move_forward().turn_right();

println!("Start: ({}, {}, {:?})", start.x, start.y, start.facing);
println!("Moved: ({}, {}, {:?})", after_move.x, after_move.y, after_move.facing);
println!("Turned: ({}, {}, {:?})", turned.x, turned.y, turned.facing);
println!("Move+Turn: ({}, {}, {:?})", after_both.x, after_both.y, after_both.facing);
}

6. How Copy Works Under the Hood

Understanding what the compiler actually does when it copies a value is key to appreciating why Copy is truly zero-cost.

Stack vs Heap Memory

fn memory_layout_demo() {
// Stack-allocated: known size at compile time, fast allocation
let x: i32 = 42; // 4 bytes on the stack
let y = x; // 4 bytes copied on the stack — memcpy of 4 bytes

// Heap-allocated: dynamic size, slower allocation
let s1 = String::from("hello");
// Stack: pointer(8) + length(8) + capacity(8) = 24 bytes on stack
// Heap: 5 bytes for "hello"

// Moving s1 copies only the 24-byte stack portion
// Both would point to same heap memory — NOT allowed (hence the move)
let s2 = s1; // Stack pointer is moved, not cloned

println!("y = {}", y);
println!("s2 = {}", s2);
}

What the Compiler Does for Copy Types

For a Copy type like this:

#[derive(Copy, Clone)]
struct Rect {
width: u32,
height: u32,
}

fn area(rect: Rect) -> u32 {
rect.width * rect.height
}

fn main() {
let r = Rect { width: 10, height: 20 };
let area = area(r); // r's 8 bytes are memcpy'd onto the new stack frame

// r is still valid — the original 8 bytes in main's stack frame are untouched
println!("Rect: {}x{}, Area: {}", r.width, r.height, area);
}

The compiler essentially generates something equivalent to:

// What Rust's Copy semantics compile to (conceptually in C)
typedef struct { uint32_t width; uint32_t height; } Rect;

uint32_t area(Rect rect) { // rect is passed by value (8-byte copy)
return rect.width * rect.height;
}

int main() {
Rect r = {10, 20};
uint32_t a = area(r); // r is copied into area's stack frame
printf("%u x %u = %u\n", r.width, r.height, a); // r still valid
return 0;
}

LLVM IR Perspective

The compiler may even optimize away the copy entirely through inlining and register allocation:

// This Rust code
#[inline(always)]
fn double(x: i32) -> i32 {
x * 2
}

fn main() {
let num = 21;
let result = double(num); // After optimization: just `21 * 2` in a register
println!("{}", result);
}

The “copy” of num may never even touch memory — the compiler might keep everything in CPU registers.

7. Zero-Cost Abstractions Explained

The term “zero-cost abstraction” comes from C++ founder Bjarne Stroustrup:

“What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.”

Rust embraces this principle aggressively. The Copy trait is a perfect example.

Demonstrating Zero-Cost with a Generic Function

use std::ops::Add;

// A generic function that works for any Copy + Add type
fn sum_array<T>(arr: &[T]) -> T
where
T: Copy + Add<Output = T> + Default
{
arr.iter().fold(T::default(), |acc, &x| acc + x)
}

fn main() {
let ints = [1i32, 2, 3, 4, 5];
let floats = [1.1f64, 2.2, 3.3, 4.4, 5.5];

println!("Sum of ints: {}", sum_array(&ints));
println!("Sum of floats: {:.1}", sum_array(&floats));

// At compile time, Rust generates specialized versions of sum_array
// for i32 and f64 separately — just as efficient as hand-written code
// No vtable, no dynamic dispatch, no overhead
}

Newtype Pattern with Zero-Cost Copy

// Newtype wrappers are zero-cost — they compile away entirely
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
struct Meters(f64);

#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
struct Kilograms(f64);

impl Meters {
fn value(self) -> f64 { self.0 }
}

impl Kilograms {
fn value(self) -> f64 { self.0 }
}

// Type safety at zero runtime cost!
fn body_mass_index(weight: Kilograms, height: Meters) -> f64 {
weight.value() / (height.value() * height.value())
}

fn main() {
let weight = Kilograms(70.0);
let height = Meters(1.75);

let bmi = body_mass_index(weight, height);
println!("BMI: {:.2}", bmi);

// weight and height are still available because they're Copy!
println!("Weight: {:.1} kg", weight.value());
println!("Height: {:.2} m", height.value());

// This would be a compile-time error:
// body_mass_index(height, weight); // Type mismatch!
}

State Machine with Copy Enums — Zero-Cost Pattern

#[derive(Debug, Copy, Clone, PartialEq)]
enum TrafficLight {
Red,
Yellow,
Green,
}

impl TrafficLight {
fn next(self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}

fn duration_seconds(self) -> u32 {
match self {
TrafficLight::Red => 30,
TrafficLight::Green => 25,
TrafficLight::Yellow => 5,
}
}

fn can_go(self) -> bool {
self == TrafficLight::Green
}
}

fn simulate_traffic(mut light: TrafficLight, cycles: u32) {
for i in 0..cycles {
println!(
"Cycle {}: {:?} ({} seconds) - Can go: {}",
i + 1,
light,
light.duration_seconds(), // light is copied here
light.can_go() // and here — all free!
);
light = light.next(); // light is copied into next()
}
}

fn main() {
simulate_traffic(TrafficLight::Red, 6);
}

8. When NOT to Use Copy

Copy is not always the right choice. Here’s how to think about when to avoid it.

Large Structs — Accidental Performance Regression

// BAD: This struct is 256 bytes. Making it Copy means every
// assignment copies 256 bytes on the stack!
#[derive(Copy, Clone)]
struct LargeMatrix {
data: [[f32; 8]; 8], // 256 bytes
}

// BETTER: Don't make it Copy — force explicit borrows or clones
#[derive(Clone)]
struct LargeMatrixBetter {
data: [[f32; 8]; 8],
}

impl LargeMatrixBetter {
fn multiply(&self, other: &LargeMatrixBetter) -> LargeMatrixBetter {
// Takes references — no copying at all!
let mut result = LargeMatrixBetter { data: [[0.0; 8]; 8] };
for i in 0..8 {
for j in 0..8 {
for k in 0..8 {
result.data[i][j] += self.data[i][k] * other.data[k][j];
}
}
}
result
}
}

fn main() {
let m1 = LargeMatrixBetter { data: [[1.0; 8]; 8] };
let m2 = LargeMatrixBetter { data: [[2.0; 8]; 8] };

let result = m1.multiply(&m2); // No copies of m1 or m2!
println!("Result[0][0]: {}", result.data[0][0]);
}

Types with Semantic Ownership — Copy Would Be Wrong

Some types represent exclusive ownership of an external resource, such as a file handle, a network socket, or a mutex lock. For these types, a bitwise copy would be disastrous: if you copied a file handle, both the original and the copy would eventually try to close the same file descriptor, resulting in a double-free bug.

Fortunately, Rust does not rely on you to just “remember” not to implement Copy here. The language actively prevents it. If a type implements the Drop trait (which is used to define custom cleanup logic when a value goes out of scope), the Rust compiler strictly forbids it from implementing Copy.

struct FileHandle {
    fd: i32, // file descriptor
}
// Implementing Drop means we are defining custom cleanup logic.
impl Drop for FileHandle {
    fn drop(&mut self) {
        println!("Closing file descriptor: {}", self.fd);
    }
}
// If we tried to uncomment the code below, the compiler would immediately 
// throw Error [E0184]: the trait `Copy` cannot be implemented for this type; 
// the type has a destructor.
// impl Copy for FileHandle {} 
// impl Clone for FileHandle {
//     fn clone(&self) -> Self { *self }
// }

By making Drop and Copy mutually exclusive, the Rust compiler guarantees that types requiring custom destruction logic can never be accidentally duplicated via implicit bitwise copies.

Builder Pattern — Intentional Move Semantics

// Builder pattern intentionally uses moves to enforce usage patterns
struct QueryBuilder {
table: String,
conditions: Vec<String>,
limit: Option<usize>,
}

impl QueryBuilder {
fn new(table: &str) -> Self {
QueryBuilder {
table: table.to_string(),
conditions: Vec::new(),
limit: None,
}
}

// Takes self by value (moves it) — returns new builder
// This prevents using a "half-built" query
fn where_clause(mut self, condition: &str) -> Self {
self.conditions.push(condition.to_string());
self
}

fn limit(mut self, n: usize) -> Self {
self.limit = Some(n);
self
}

fn build(self) -> String {
let mut query = format!("SELECT * FROM {}", self.table);
if !self.conditions.is_empty() {
query.push_str(" WHERE ");
query.push_str(&self.conditions.join(" AND "));
}
if let Some(limit) = self.limit {
query.push_str(&format!(" LIMIT {}", limit));
}
query
}
}

fn main() {
let query = QueryBuilder::new("users")
.where_clause("age > 18")
.where_clause("active = true")
.limit(10)
.build();

println!("Generated query: {}", query);
}

9. Performance Benchmarks and Analysis

Let’s look at concrete performance comparisons between Copy and non-Copy types.

To see the true cost of deep cloning versus bitwise copying, we can benchmark what happens when we duplicate a large slice of data. When you call .to_vec() on a slice of Copy types, Rust optimizes this into a lightning-fast memory copy (like C’s memcpy). When you call it on non-Copy types, Rust is forced to iterate through and call .clone() on every single element.

Benchmark Setup

use std::time::Instant;
#[derive(Copy, Clone, Debug)]
struct SmallVec2 {
    x: f32,
    y: f32,
}
#[derive(Clone, Debug)]
struct HeapVec2 {
    components: Vec<f32>, // Heap allocated, requires deep cloning
}
fn main() {
    const N: usize = 1_000_000;
    // 1. Setup: Create large collections of both types
    let copy_points: Vec<SmallVec2> = (0..N)
        .map(|i| SmallVec2 { x: i as f32, y: i as f32 })
        .collect();
    let heap_points: Vec<HeapVec2> = (0..N)
        .map(|i| HeapVec2 { components: vec![i as f32, i as f32] })
        .collect();
    // 2. Benchmark Copy type
    // Because SmallVec2 is Copy, this uses a highly optimized block memory copy.
    let start_copy = Instant::now();
    let _result_copy = copy_points.to_vec(); 
    let copy_duration = start_copy.elapsed();
    // 3. Benchmark Clone type
    // Because HeapVec2 is NOT Copy, this must individually call .clone() 
    // 1,000,000 times, performing 1,000,000 separate heap allocations!
    let start_heap = Instant::now();
    let _result_heap = heap_points.to_vec();
    let heap_duration = start_heap.elapsed();
    println!("Copy type (bitwise memcpy): {:?}", copy_duration);
    println!("Clone type (deep allocation): {:?}", heap_duration);
    
    // Typical Output:
    // Copy type (bitwise memcpy): 480.2µs
    // Clone type (deep allocation): 38.5ms
}

Analysis

The performance gap here is massive (often 50x to 100x slower for the Clone type).

The SmallVec2 duplication is virtually instantaneous because the compiler knows it is safe to just blast the raw bytes from one location in memory to another. The HeapVec2 duplication grinds to a halt because the operating system’s memory allocator has to step in and find fresh heap space for a new Vec inside every single struct, one by one. This is why keeping small data structures perfectly stack-allocated and Copy is a core tenet of writing high-performance Rust.

10. Common Pitfalls

Pitfall 1: Assuming Copy Means Cheap

// This is technically Copy but copies 1024 bytes on every assignment!
#[derive(Copy, Clone)]
struct BigCopyType {
data: [u64; 128], // 1024 bytes
}

fn process(data: BigCopyType) -> u64 {
data.data.iter().sum() // Copies 1024 bytes onto stack here!
}

// Better approach: use references
fn process_by_ref(data: &BigCopyType) -> u64 {
data.data.iter().sum() // No copy — just borrows
}

fn main() {
let big = BigCopyType { data: [1u64; 128] };

// Potentially expensive — 1024 bytes copied
let _sum1 = process(big);

// Free — just passes a pointer
let _sum2 = process_by_ref(&big);
}

Pitfall 2: Forgetting Copy Means No Mutation Through Copies

#[derive(Copy, Clone, Debug)]
struct Counter {
value: i32,
}

impl Counter {
fn increment(&mut self) {
self.value += 1;
}
}

fn main() {
let original = Counter { value: 0 };
let mut copy = original; // copy is a separate value

copy.increment();
copy.increment();

// Surprise! original is unchanged — copy mutations don't propagate
println!("original: {}", original.value); // 0
println!("copy: {}", copy.value); // 2

// This behavior is correct, but can surprise developers
// expecting reference-like semantics
}

Pitfall 3: Copy Doesn’t Preserve Identity

use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;

// If you need shared mutable state, Copy is the WRONG tool
fn pitfall_shared_state() {
// WRONG approach: Copy types don't share state
#[derive(Copy, Clone)]
struct SharedCounterWrong {
count: i32,
}

let mut counter = SharedCounterWrong { count: 0 };
let mut counter_copy = counter; // This is NOT shared!

counter.count += 1;
counter_copy.count += 10;

println!("counter: {}", counter.count); // 1
println!("counter_copy: {}", counter_copy.count); // 10
// They diverge — no sharing!

// CORRECT approach: Use Arc<AtomicI32> for shared mutable state
let shared = Arc::new(AtomicI32::new(0));
let shared_clone = Arc::clone(&shared); // Clone the Arc, not the data

shared.fetch_add(1, Ordering::SeqCst);
shared_clone.fetch_add(10, Ordering::SeqCst);

println!("Shared value: {}", shared.load(Ordering::SeqCst)); // 11
}

fn main() {
pitfall_shared_state();
}

Pitfall 4: Trying to Implement Copy on Ineligible Types

// This will FAIL to compile — you cannot implement Copy
// if any field does not implement Copy

// struct BadCopy {
// name: String, // String is NOT Copy
// value: i32,
// }
// impl Copy for BadCopy {} // Error! String doesn't implement Copy

// Similarly, this derive will fail:
// #[derive(Copy, Clone)]
// struct AlsoBad {
// data: Vec<i32>, // Vec is NOT Copy
// }

// The correct version — use only Copy fields
#[derive(Copy, Clone, Debug)]
struct GoodStruct {
id: u32,
score: f64,
active: bool,
}

fn main() {
let s = GoodStruct { id: 1, score: 9.5, active: true };
let s2 = s; // Fine!
println!("{:?} {:?}", s, s2);
}

11. Best Practices

Practice 1: The Copy Candidate Checklist

Before implementing Copy on a type, ask:

// ✅ Good Copy candidates:
// 1. All fields are already Copy
// 2. The type is small (ideally fits in a few cache lines — ≤ 64 bytes)
// 3. Copying the type has no side effects
// 4. The type doesn't represent exclusive ownership of a resource
// 5. Having multiple independent copies is semantically correct

#[derive(Copy, Clone, Debug)]
struct Transform2D { // 16 bytes — excellent Copy candidate
tx: f32, // 4 bytes
ty: f32, // 4 bytes
rotation: f32, // 4 bytes
scale: f32, // 4 bytes
}
// Note: Because all fields are 4 bytes and share the same alignment,
// the compiler does not need to insert any padding. 4 x 4 = 16 bytes exactly.

Practice 2: Use Phantom Types for Zero-Cost Safety

use std::marker::PhantomData;

#[derive(Copy, Clone, Debug)]
struct Angle<Unit> {
value: f64,
_unit: PhantomData<Unit>, // PhantomData is zero-sized and Copy!
}

#[derive(Copy, Clone, Debug)]
struct Radians;

#[derive(Copy, Clone, Debug)]
struct Degrees;

impl Angle<Degrees> {
fn new(degrees: f64) -> Self {
Angle { value: degrees, _unit: PhantomData }
}

fn to_radians(self) -> Angle<Radians> {
Angle {
value: self.value * std::f64::consts::PI / 180.0,
_unit: PhantomData
}
}
}

impl Angle<Radians> {
fn new(radians: f64) -> Self {
Angle { value: radians, _unit: PhantomData }
}

fn sin(self) -> f64 {
self.value.sin()
}
}

fn main() {
let angle_deg = Angle::<Degrees>::new(90.0);
let angle_rad = angle_deg.to_radians();

// angle_deg is still available because Angle<Degrees> is Copy!
println!("{}° = {:.4} radians", angle_deg.value, angle_rad.value);
println!("sin(90°) = {:.4}", angle_rad.sin());

// Type safety at zero runtime cost:
// angle_deg.sin(); // Compile error! sin() only works on Angle<Radians>
}

Practice 3: Combining Copy with Generic Bounds

use std::fmt::Display;

// Generic data structure that works efficiently with Copy types
struct Ring<T: Copy, const N: usize> {
data: [T; N],
head: usize,
size: usize,
}

impl<T: Copy + Default, const N: usize> Ring<T, N> {
fn new() -> Self {
Ring {
data: [T::default(); N],
head: 0,
size: 0,
}
}

fn push(&mut self, value: T) {
let idx = (self.head + self.size) % N;
if self.size < N {
self.data[idx] = value;
self.size += 1;
} else {
self.data[self.head] = value;
self.head = (self.head + 1) % N;
}
}

fn peek(&self) -> Option<T> {
if self.size == 0 {
None
} else {
Some(self.data[self.head]) // Returns a Copy — no lifetime needed!
}
}
}

impl<T: Copy + Default + Display, const N: usize> Ring<T, N> {
fn print_all(&self) {
for i in 0..self.size {
let idx = (self.head + i) % N;
print!("{} ", self.data[idx]); // Each access copies — cheap for Copy types
}
println!();
}
}

fn main() {
let mut ring: Ring<i32, 5> = Ring::new();

for i in 0..7 {
ring.push(i);
}

ring.print_all(); // Should show: 2 3 4 5 6

if let Some(front) = ring.peek() {
println!("Front element: {}", front);
// front is a copy — ring still owns its data!
println!("Ring still intact:");
ring.print_all();
}
}

Practice 4: Testing Copy Semantics

A common source of confusion is how Copy interacts with closures. By default, Rust closures capture variables from their environment in the least restrictive way possible (usually by immutable reference).

Look at this standard closure:

#[test]
fn test_standard_closure_capture() {
    let scale = String::from("I am not Copy"); 
    let points = vec![1, 2, 3];
    // Notice there is no 'move' keyword here
    let _scaled: Vec<String> = points.iter().map(|&p| {
        format!("{} - {}", p, scale) // scale is captured by immutable reference (&String)
    }).collect();
    // scale is still available here! 
    // Not because it's Copy, but because it was only borrowed.
    println!("Still have: {}", scale); 
}

To actually see the Copy trait in action with a closure, you must use the move keyword. The move keyword forces the closure to take ownership of the variables it captures. If a variable implements Copy, it will be copied into the closure. If it does not, it will be moved (and thus consumed).

#[test]
fn test_copy_in_move_closure() {
    let copy_scale = 2.0f64; // f64 implements Copy
    let move_string = String::from("I will be consumed"); // String does not
    
    let points = vec![
        Point { x: 1.0, y: 1.0 },
        Point { x: 2.0, y: 2.0 },
    ];
    // The 'move' keyword forces ownership transfer
    let scaled: Vec<Point> = points.iter().map(move |&p| {
        // Because of 'move', move_string is moved into this closure's environment
        println!("Using string: {}", move_string); 
        
        Point {
            x: p.x * copy_scale, // copy_scale is COPIED into the closure
            y: p.y * copy_scale,
        }
    }).collect();
    // copy_scale is STILL available here because it was a Copy type!
    assert_eq!(copy_scale, 2.0);
    
    // ERROR: move_string is gone. The line below would fail to compile:
    // println!("{}", move_string); 
}

Summary and Key Takeaways

Let’s recap the most important concepts about zero-cost copy in Rust:

// Quick reference summary
fn summary() {
// 1. Copy = implicit, automatic bitwise duplication (stack only)
let x: i32 = 5;
let y = x; // Both x and y are valid

// 2. Clone = explicit, potentially expensive duplication
let s = String::from("hello");
let s2 = s.clone(); // Must call .clone() explicitly

// 3. Copy requires Clone as supertrait
// #[derive(Copy, Clone)] — always derive both together

// 4. Copy types cannot own heap data or implement Drop

// 5. Zero-cost = the abstraction compiles to optimal machine code
// No runtime overhead compared to manual pointer management

// 6. Prefer borrows (&T) for large Copy types to avoid stack pressure

println!("x={}, y={}, s={}, s2={}", x, y, s, s2);
}

Decision Tree for Copy vs Clone vs Borrow


Conclusion

The Copy trait in Rust is one of the most elegant examples of zero-cost abstractions in any modern systems language. By letting the compiler know which types are safe to duplicate bit-for-bit, you get:

  • Safety — No double-free, no dangling pointers
  • Performance — Stack copies are as fast as the hardware allows
  • Ergonomics — No .clone() noise for simple value types
  • Expressiveness — Generic code that works optimally for all Copy types

The key insight is that zero-cost means the abstraction adds no overhead. When you use Copy types, you’re not paying for any Rust-specific mechanism — the generated assembly is identical to what you’d write in C or assembly by hand. That’s the Rust promise: write high-level, expressive code and get low-level performance for free.