RFA-136 · Case file with fixtures · Case 108 of 694 · Compiler evidence
Why Copy Cannot Be Implemented for a Type With Drop
Copy creates implicit bitwise duplicates, while Drop assigns an automatic cleanup action to every value. Rust forbids combining them because implicit duplication would make cleanup ownership and the number of destructor calls incoherent; use explicit Clone or separate the guard.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- check, dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Copy duplicates values implicitly while Drop gives every value an observable cleanup action, so allowing both would make the number and ownership of destructor calls incoherent.
- First discriminating check
- Find the type's Drop implementation and decide whether callers need explicit Clone semantics or whether cleanup ownership belongs in a separate non-Copy guard.
The failing program defines an empty CleanupToken, derives Clone and Copy, and gives the token a destructor. Rust rejects Copy with E0184 even though the type stores no non-copyable field.
The conflict is not about bytes. It is about what one value means during its lifecycle.
Copy can happen without an explicit operation
The Copy documentation describes types whose values can be duplicated by a simple bitwise copy. Assignment and passing by value leave the original usable:
let second = first;
use_again(first);
There is no visible copy() call at the point where the duplicate appears. Generic code, patterns, and ordinary assignments may all create another value.
This is a good contract for integers, small coordinates, and handles whose duplication has no independent cleanup consequence. It is a dangerous contract for a guard that releases a lock, closes a resource, decrements external state, commits a measurement, or marks an operation finished.
Drop gives each value an automatic action
The Drop documentation explains that Rust calls drop automatically when a value leaves scope. If a cleanup token could be copied implicitly, both the original and its duplicate would later receive that action.
Which copy owns the cleanup? Should both run it? Should only the last one run it? A plain bitwise copy does not carry a protocol for answering these questions.
Rust therefore rejects the combination. The E0184 explanation states the direct rule: a type with a destructor cannot implement Copy.
I read this as a semantic consistency check:
Copy -> duplicating the value needs no ownership event
Drop -> destroying each value performs an ownership event
The same type cannot honestly promise both under Rust's current model.
Explicit Clone makes duplication reviewable
The repaired program removes Copy and keeps Clone. Duplication now appears as token.clone().
This does not automatically make the design correct. The Clone implementation still needs to define what another cleanup-capable value means. For a reference-counted handle, cloning may acquire another counted ownership share, and dropping each clone releases one share. For a database transaction guard, cloning may make no sense and should not be implemented at all.
The important improvement is that Clone is an explicit operation with an implementation. It can update bookkeeping, fail indirectly only through a chosen design, or be omitted from the API.
In the minimal fixture, the destructor only prints, so two explicit clones produce two cleanup messages. A real type would need a stronger invariant.
An empty type can still carry ownership
The token has zero stored data, but zero-sized does not mean semantically irrelevant. Its existence can represent permission, an active scope, or a pending cleanup. Rust's ownership system tracks values and destructors even when their runtime size is zero.
This is useful for guards that restore thread-local state or mark a tracing span complete. Such a guard should normally move, not copy. The compiler then helps ensure there is one owner of the completion action.
If callers only need a cheap identifier, I separate it from the guard:
#[derive(Clone, Copy)]
struct TokenId(u64);
struct CleanupGuard {
id: TokenId,
}
TokenId can travel freely. CleanupGuard retains unique lifecycle responsibility.
Removing Drop is not always a repair
Another quick fix is deleting the Drop implementation. This is correct only when cleanup is unnecessary or performed safely somewhere else. If callers depend on the guard to release a resource, removing the destructor trades a compiler error for a resource leak or stale state.
I write down the invariant first: what action must happen, exactly once or once per ownership share, and who owns that action? The answer decides whether the type should be move-only, explicitly cloneable with reference counting, or split into a copyable ID and a non-copyable guard.
My debugging sequence
When E0184 appears, I do this:
- Find the
Dropimplementation, including one generated by another layer. - State the cleanup side effect and whether it belongs to exactly one owner.
- Remove
Copy; then decide separately whetherCloneis meaningful. - If cloning is valid, implement the bookkeeping that makes every later drop correct.
- Split identity from cleanup ownership when callers only need a copyable label.
- Test move, explicit clone, normal drop, panic unwinding, and early-return paths.
The broad principle is that representation does not decide lifecycle semantics. A value may occupy zero bytes and still own a real obligation. Rust refuses implicit copying when destruction carries such an obligation, forcing the API to make duplication an explicit design choice.