Skip to content
← All articles

Copy and Drop are mutually exclusive

The moment Copy stops being a list of primitives to memorise and becomes a theorem you can apply to any type: Copy means no destructor, and all fields Copy. Two compiler errors, E0184 and E0204, are the proof.

You have now met Copy twice: as the reason i64 survives being passed to a function, and as the reason String does not. So far it has probably felt like a list you have to memorise — integers yes, String no, &T yes, &mut T no, tuples of Copy things yes.

It is not a list. It is a theorem, and it is short enough to hold in your head:

A type may be Copy if and only if every field is Copy and the type has no destructor.

With that, you can decide for any type — including one you have not seen — whether assignment moves or duplicates. That is a much better position to be in than remembering a list, and it is the difference between predicting Rust and guessing at it.

This is an article rather than a problem for a mechanical reason: everything interesting here is a compile failure, and a submission that passes the gate cannot contain one. So we will read the errors instead of producing them. Copy the snippets into a scratch file if you want to see them yourself.

The two halves, as diagnostics

Half one: a Drop type can never be Copy — E0184

#[derive(Clone, Copy)]
struct Session { id: i32 }

impl Drop for Session {
    fn drop(&mut self) { println!("closing {}", self.id); }
}
error[E0184]: the trait `Copy` cannot be implemented for this type;
              the type has a destructor
 --> src/main.rs:2:8
  |
1 | #[derive(Clone, Copy)]
  |                 ---- in this derive macro expansion
2 | struct Session { id: i32 }
  |        ^ `Copy` not allowed on types with destructors
  |
note: destructor declared here
 --> src/main.rs:5:19

Every field of Session is Copy — it is one i32. The derive is still rejected, and the note points straight at the reason.

Half two: one non-Copy field is enough — E0204

#[derive(Clone, Copy)]
struct Record { id: i32, label: String }
error[E0204]: the trait `Copy` cannot be implemented for this type
 --> src/main.rs:2:8
  |
1 | #[derive(Clone, Copy)]
  |                 ---- in this derive macro expansion
2 | struct Record { id: i32, label: String }
  |        ^        -------------- this field does not implement `Copy`

rustc underlines the exact field. No destructor anywhere in sight; one String is enough.

💡Take the theorem seriously for a moment. Why should a destructor disqualify a type from being Copy? Reason it out before reading on. click to reveal

Because Copy means “duplicating the bytes produces a second, fully independent value” — and a destructor is a claim that the value owns something the bytes only describe.

Play it out. Suppose Session were both Copy and had a Drop impl:

let a = Session { id: 7 };
let b = a;                  // Copy: `a` is still valid

Now there are two Sessions, both claiming id 7, and a was not invalidated because that is what Copy means. At the end of the scope both are destroyed, so drop runs twice for the same session. If id were a file descriptor, that is a double close. If it were a *mut T, a double free.

The only way to make that safe would be for the compiler to track, at runtime, which of the copies is “the real one” — which is a reference count, which is Rc, which is a completely different feature with a completely different cost.

So the rule is not a restriction bolted onto Copy; it is what Copy means. A type that needs cleanup has an identity, and identity is exactly what duplication destroys.

The same argument explains half two. A String field owns a heap buffer. Duplicating the three stack words gives two structs pointing at one buffer, and one of them will free it while the other is still using it — the exact situation ownership exists to prevent. String has no Drop impl of its own, by the way; its cleanup comes from the Vec<u8> inside it, which gets it from RawVec. “Has a destructor” means anywhere in the tree, not just at the top.

The rule stated as a decision procedure

Given any type, ask two questions:

  1. Does it — or anything it contains, at any depth — have a Drop impl?
  2. Is every field Copy?

If the answer is “no” and “yes”, it can be Copy. Otherwise it cannot, ever, no matter how you write the impl.

Run it on the standard library and everything falls out:

Type Copy? Why
i64, f64, bool, char yes no fields, no destructor
(i32, bool) yes tuple of Copy fields
[u8; 16] yes array of a Copy element
&T yes a shared reference is just an address; nothing is owned
&mut T no exclusive access cannot be duplicated and stay exclusive
String, Vec<T>, Box<T> no owns a heap allocation, so has a destructor
Option<T> only if T is its payload is a field
Rc<T>, Arc<T> no its destructor decrements a refcount
MutexGuard<'_, T> no its destructor unlocks the mutex

The &T / &mut T row is the one that pays for itself fastest. A &T can be handed to a dozen functions without ceremony because copying an address is harmless. A &mut T moves when you pass it — which is why you will sometimes see &mut *r (a reborrow) in code that needs to use a &mut twice. That is Track 3’s material, and the reason it works is the theorem above.

Where the rule shows up in real code

Clone on a Copy type must be trivial

If a type is Copy, then let b = a; already duplicates it by memcpy. That means a.clone() must produce the same thing as let b = a; — anything else and the two would disagree, and callers would have no way to know which one they were getting. Clippy enforces it, on by default:

#[derive(Copy)]
struct Counter { x: i32 }

impl Clone for Counter {
    fn clone(&self) -> Counter { Counter { x: self.x + 1 } }   // !
}
error: non-canonical implementation of `clone` on a `Copy` type
  |
  |     fn clone(&self) -> C { C { x: self.x + 1 } }
  |                          ^^^^^^^^^^^^^^^^^^^^^^^ help: change this to: `{ *self }`
  |
  = note: `-D clippy::non-canonical-clone-impl` implied by `-D warnings`

non_canonical_clone_impl is a suspicious-level lint, which means it is on by default and -D warnings turns it into an error. It is a nice, concrete consequence of the theorem: on a Copy type, Clone has exactly one correct body, and it is *self.

There is a companion, expl_impl_clone_on_copy (pedantic, off by default), which says the same thing more bluntly: “you are implementing Clone explicitly on a Copy type — consider deriving Clone or removing Copy.

💡Copy has Clone as a supertrait, so every Copy type must also implement Clone. If Clone on a Copy type is forced to be a byte copy, why does the supertrait exist at all? click to reveal

Two reasons, one historical and one still load-bearing.

The generic-code reason is the one that matters. A function written as

fn duplicate<T: Clone>(v: &[T]) -> Vec<T> { v.to_vec() }

should work for i32 as well as for String. If Copy types did not implement Clone, every generic bound would have to be written T: Clone + Copy or duplicated, and the two would be gratuitously separate worlds. Making Copy: Clone means “Copy is a promise that the clone is free”, which is a strictly stronger statement in the same vocabulary rather than a different vocabulary.

The cost reason: it is genuinely free. For a Copy type the derived Clone::clone is *self, which inlines to nothing. You are not paying for the supertrait.

A related trap, worth knowing now so you recognise it later: #[derive(Copy)] on a generic struct silently adds a T: Copy bound. So

#[derive(Clone, Copy)]
struct Pair<T> { a: T, b: T }

gives you a Pair<i32> that is Copy and a Pair<String> that is not, from the same definition. That is usually what you want and occasionally a mystery; the derive’s generated impl is impl<T: Copy> Copy for Pair<T>, and writing the impl by hand is how you take control of the bound.

The consequence that matters most

Here is the practical payoff, and it is worth stating in one line:

A type gains a destructor the moment it owns a resource — and at that moment, assignment stops duplicating and starts moving.

You cannot add a String field, or an Rc, or a file handle, and keep Copy. This is a good thing, and it is the deepest reason Copy is a derive rather than something you can talk the compiler into. The set of types for which bit-duplication is meaningful is not a matter of opinion.

It also means that going from Copy to non-Copy is a breaking change to your API in a way that is invisible in the signature. Every caller that happily used one of your values twice suddenly gets E0382. If you are designing a small value type for other people to use, deciding whether it is Copy is a decision about your future, not just about today.

Where this goes next

You now have the whole rule, and the two errors that enforce each half. The next place the theorem earns its keep is Track 3, where &T being Copy and &mut T not being Copy turns out to explain roughly half of the borrow checker’s behaviour — including why a &mut sometimes seems to vanish when you pass it to a function, and what a reborrow is doing about that.