Skip to content
← All articles

#[derive] lies about its bounds

The derive macros add a bound on every type parameter, whether or not the impl needs it. The error lands at the use site rather than at the derive, which makes it genuinely hard to debug — until you know the rule.

#[derive(Clone)] looks like magic. It is a macro, and once you know the specific impl it emits, an entire category of confusing errors becomes obvious. This is the point where you stop treating derive as a language feature and start treating it as code somebody wrote.

The rule

For a generic type, #[derive(Trait)] emits an impl with T: Trait on every type parameter. Not on the parameters the impl actually needs — on all of them.

#[derive(Clone)]
struct Holder<T>(Rc<T>);

expands to, in effect:

impl<T: Clone> Clone for Holder<T> {
    fn clone(&self) -> Self { Holder(self.0.clone()) }
}

Look at that bound and then look at the field. Rc<T> implements Clone unconditionally — cloning an Rc bumps a refcount and never touches the T. The body of that generated clone does not need T: Clone at all. The bound is there anyway, because the derive macro sees the syntax of your struct, not the semantics of Rc.

What that costs you

struct NotClone;

let h = Holder(Rc::new(NotClone));
let _h2 = h.clone();
error[E0599]: the method `clone` exists for struct `Holder<NotClone>`,
              but its trait bounds were not satisfied
   |
 4 | struct NotClone;
   | --------------- doesn't satisfy `NotClone: Clone`
...
14 | struct Holder<T>(Rc<T>);
   | ---------------- method `clone` not found for this struct because it
   |                  doesn't satisfy `Holder<NotClone>: Clone`
...
25 |     let _h2 = h.clone();
   |                 ^^^^^ method cannot be called on `Holder<NotClone>`
   |                       due to unsatisfied trait bounds
   |
note: trait bound `NotClone: Clone` was not satisfied
   |
13 | #[derive(Clone)]
   |          ----- in this derive macro expansion
   = help: consider manually implementing the trait to avoid undesired bounds
help: consider annotating `NotClone` with `#[derive(Clone)]`

Two things about that diagnostic are worth dwelling on.

It points at the use site. The error is on line 25, where you called clone, not on line 13 where the mistake actually is. In a real codebase the derive is in one file, the type parameter is chosen in another, and the call is in a third. Without the rule in your head this is genuinely hard to debug.

The suggested fix is the wrong one. rustc says “consider annotating NotClone with #[derive(Clone)]“, which would work but is backwards: you would be making an unrelated type cloneable to work around a bound your wrapper never needed. The right fix appears one line earlier, in the understated help: consider manually implementing the trait to avoid undesired bounds.

💡Here is a wrapper that holds no T at all: #[derive(Clone, Copy)] struct W<T>(PhantomData<T>);. Predict what happens for W<NotClone>, then explain it. click to reveal

It fails, with the same E0599:

the method `clone` exists for struct `W<NotClone>`,
but its trait bounds were not satisfied
note: trait bound `NotClone: Clone` was not satisfied
      type parameter would need to implement `Clone`

W<NotClone> contains zero bytes of NotClone. PhantomData<T> is a zero-sized marker; there is nothing to clone. The derive still emits impl<T: Clone> Clone for W<T>, because the rule is purely syntactic: one parameter, one bound.

This is the cleanest possible demonstration that the derive is not reasoning about your type. It is a macro doing a textual transformation, and it has no idea PhantomData is special.

The fix is four lines:

impl<T> Clone for W<T> {
    fn clone(&self) -> Self { *self }
}
impl<T> Copy for W<T> {}

Now W<NotClone> is Clone and Copy, which is correct, because copying a zero-sized marker cannot possibly require anything of T. Verified on rustc 1.95: with those two impls, assert_clone::<W<NotClone>>() and assert_copy::<W<NotClone>>() both compile.

Hand-writing the impl: one trap

Notice the body above is *self, not W(PhantomData). That is not a style choice, and clippy will stop you:

error: non-canonical implementation of `clone` on a `Copy` type
  |
8 |     fn clone(&self) -> Self { W(PhantomData) }
  |                             ^^^^^^^^^^^^^^^^^^ help: change this to: `{ *self }`
  |
  = note: `-D clippy::non-canonical-clone-impl` implied by `-D warnings`

clippy::non_canonical_clone_impl is suspicious/default-on, and the reasoning is sharp: for a Copy type the compiler is entitled to duplicate values by memcpy without calling clone at all. So a clone that does anything other than *self is a lie — it will sometimes run and sometimes be skipped, and you will never find out which. If your Clone needs to do work, your type must not be Copy.

Which derives have this problem

All of them, in the same way. Some hurt more than others:

derive emitted bound typically wrong when
Clone T: Clone the field is Rc<T>, Arc<T>, PhantomData<T>, &T
Copy T: Copy same
Debug T: Debug the field is PhantomData<T> or a marker
Default T: Default the field is a Vec<T>, Option<T> or any empty collection
PartialEq T: PartialEq the field is PhantomData<T>

The Default row is the one you are most likely to hit in ordinary code. A Stack<T> { items: Vec<T> } has a perfectly good empty default for every T, because Vec::new() needs nothing — but #[derive(Default)] would restrict it to T: Default anyway. That is exactly why the generic-stack problem earlier in this track asks you to write

impl<T> Default for Stack<T> {
    fn default() -> Self { Self::new() }
}

by hand.

What about “perfect derive”?

You will find discussion of perfect derive — a hypothetical smarter derive that emits Rc<T>: Clone instead of T: Clone, i.e. bounds on the field types rather than on the parameters. It would fix everything above.

Do not plan around it. It has been discussed for years, it is not stable, and it is not obviously a free win:

  • It would leak your private field types into your public API’s bounds, so changing a private field could break downstream callers — a semver hazard that does not exist today.
  • It interacts badly with inference in some cases, because the bound is no longer visible from the type’s declaration.
  • It cannot be turned on by default without breaking existing code, so it would need an opt-in attribute, which is roughly as much typing as writing the impl.

The practical position for now: derive by default, and hand-write the impl the moment a type parameter appears in a position that does not really need the bound.

Two neighbouring lints

  • clippy::derived_hash_with_manual_eqdeny by default. Deriving Hash while hand-writing PartialEq almost always breaks the a == b implies hash(a) == hash(b) contract, and the failure mode is a HashMap key that silently cannot be found.
  • clippy::derive_partial_eq_without_eq — nursery, allow by default, so it will not fire under this course’s gate. Its advice is still reasonable: if a type has no floats in it, add Eq and unlock HashMap and BTreeMap keys.

The rule, one line

#[derive(Trait)] bounds every type parameter with Trait, whether the impl needs it or not. When the answer is “it does not”, write the impl yourself — and if the type is Copy, make clone return *self.

Generics and Traits · step 24 of 24

That's the end of this track. Review it or pick another.

← Back to Generics and Traits