Why Some Types in Rust Don't Move

Ownership in Rust moves values by default.

You bind a value to a new variable, the old one dies, the compiler enforces it.

Try to use it again and you get this:

let s1 = String::from("hello");
let s2 = s1;

println!("{}", s1); // error[E0382]: borrow of moved value: `s1`

Makes sense for a String.

It owns a heap allocation — pointer, length, capacity. If both s1 and s2 pointed at the same buffer, you'd either need to deep-copy the data on every assignment, or risk a double free when both go out of scope.

Move semantics dodge both problems: only one owner, ever, no copy needed, no cleanup ambiguity.

Now try the same thing with an i32:

let x = 5;
let y = x;

println!("{}", x); // this just... works

No error.

x is still alive after y = x.

First time I ran into this I assumed I was misremembering how move worked.

I wasn't — this is a different mechanism entirely.

Why simple types don't need to move

The reasoning behind move semantics is about avoiding expensive, implicit copies of heap data.

But an i32, a bool, a char, an f64 — none of that applies.

They're fixed-size, they live entirely on the stack, there's no heap allocation attached to them at all. They're just a sequence of bits.

Copying a sequence of bits is cheap. Cheaper, in most cases, than the bookkeeping required to enforce "only one owner."

So Rust doesn't move these types — it copies them. y = x duplicates the bits and hands y its own independent copy. x never stopped being valid, because nothing was ever taken away from it.

This is the Copy trait. Types that implement it get duplicated on assignment instead of moved.

What would happen without it

Imagine if i32 didn't implement Copy and moved like String does.

Every time you passed a number into a function, or assigned it to another variable, the original would become unusable:

fn double(n: i32) -> i32 {
    n * 2
}

let x = 5;
let y = double(x);
let z = x + 1; // if i32 moved, x would be gone by now

That's not memory safety, it's friction for no reason.

You'd be sprinkling .clone() everywhere just to keep using a number twice. Copy exists so simple types don't force you into that.

It's not just primitives

The same logic extends to composite types, as long as every field inside is also Copy and the whole thing has a fixed, known size at compile time.

A tuple of integers, a fixed-size array of Copy values — Rust lets these implement Copy too:

let point = (3, 4);
let other = point; // copied, not moved

println!("{:?}", point); // still valid

There's no heap data anywhere in that tuple, no size that depends on runtime state.

Same reasoning as i32, just applied to a slightly bigger bag of bits.

Your own types can opt in too

By default, a struct you write doesn't implement Copy — even if every field inside it would qualify on its own.

Rust doesn't assume; you have to ask for it:

struct StatusCode {
    value: u16,
}

As written, StatusCode moves. But nothing about it needs to.

value is a u16 — fixed size, no heap, no destructor. So you can just tell Rust to derive Copy for it:

#[derive(Copy, Clone)]
struct StatusCode {
    value: u16,
}

let a = StatusCode { value: 404 };
let b = a;

println!("{}", a.value); // still valid, `a` was copied, not moved

derive(Copy) requires Clone too — Copy is really just "clone, but implicit and cheap," so the compiler asks for both.

The rule doesn't care if the type is a primitive or something you defined yourself: if every field is Copy and there's no heap allocation hiding anywhere inside, your struct can be Copy as well.

The actual rule

It's not "simple types are special-cased to be flexible."

It's: Rust always weighs the cost of moving against the cost of copying, and for anything that's just stack data with no allocation and no destructor, copying wins.

Copy isn't an exception to ownership — it's ownership rules applied consistently, all the way down to the cost of an assignment.