Warning users when lacking a Clone impl due to imperfect derive

It's well known that the following:

#[derive(Clone)]
struct Foo<T>(PhantomData<T>);

Yields:

impl<T: Clone> Clone for Foo<T> { /* */ }

Instead of

impl<T> Clone for Foo<T> where PhantomData<T>: Clone { /* */ }

And the reasons for why have been discussed here on IRLO.

What I'm thinking the language is lacking is an extra diagnostic, hence this post to gauge interest and feedback before I open an issue. Suppose I want to do:

fn bar<T: Clone>(_: T) {}

#[derive(Default)]
struct DefaultNotClone;

// assume I've also done a #[derive(Default)] for Foo
bar(Foo::<DefaultNotClone>::default());

Currently I get:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `Foo<DefaultNotClone>: Clone` is not satisfied
  --> src/main.rs:10:9
   |
10 |     bar(Foo::<DefaultNotClone>::default());
   |     --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `Foo<DefaultNotClone>`
   |     |
   |     required by a bound introduced by this call
   |
note: required for `Foo<DefaultNotClone>` to implement `Clone`
  --> src/main.rs:1:19
   |
 1 | #[derive(Default, Clone)]
   |                   ^^^^^ unsatisfied trait bound introduced in this `derive` macro
note: required by a bound in `bar`
  --> src/main.rs:7:11
   |
 7 | fn bar<T: Clone>(_: T) {}
   |           ^^^^^ required by this bound in `bar`
help: consider borrowing here
   |
10 |     bar(&Foo::<DefaultNotClone>::default());
   |         +

But I think it would be useful if the compiler, when possible (i.e. when it can easily tell that the field does implement Clone, and omitted for the cases involving extra analysis) could warn the user that they might've assumed a perfect derive (i.e. the one with a where clause) instead of actual derive behaviour:

help: derive produces bounds on generics rather than fields
   |
10 |     bar(Foo::<DefaultNotClone>::default());
   |               ^^^^^^^^^^^^^^^ derive expects this to implement `Clone`
...
   |
2  |     struct Foo<T>(std::marker::PhantomData<T>);
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^ despite this implementing `Clone`

Not super happy with the exact wording, but something along those lines would be useful to let users who are new to this particular part of the derive rules know why they're encountering this issue.


P.S. didn't know which category this would best fit in, but since I'm asking about a helpful diagnostic emitted by the compiler, it seemed appropriate to put it in the compiler category.

4 Likes

I think it'd be better if the warning could be placed on the derive instead of the call site trying to use it, since the compiler could tell if the derive is imperfect[1].

Personally, I've never written #[derive(Clone)] struct Foo<T>(PhantomData<T>); and wanted it to only implement Clone when T: Clone, and always would have followed a lint suggesting I replace it with an unconditional impl<T> Clone for Foo<T>. So it would be nice to catch this limitation when defining the struct, instead of waiting for it to be used (possibly in another crate).


  1. I’m sure the type system could be used for turing-complete logic on whether or not a specific value for T results in a Clone field or not, but the compiler could at least catch easy cases, where a type parameter is only used in unconditionally-Clone types. ↩︎

5 Likes

True, especially since a "deliberate" imperfect derive could express so simply by allowing such a lint. It could even start as allow-by-default if deemed too jarring / to start experimenting with it.

  • I do feel, however, like this is going to expose the problem of perfect derive more to users, who shall, in turn, realize how frustrating it is. Indeed, none of the workarounds is fully satisfactory:

    Workarounds

    Click to hide
    1. Manually impl the traits in question

      Ughh, so mechanical and repetitive. But it gives you flexibility, control, and clarity, in exchange. This is what most library crates end up doing, thus.

    2. Use ::derivative [actual docs], or something similar

      Actually probably the best approach, but it involves pulling in a full third-party crate "just" for this, and with the feeling that it may result in suboptimal code for the compiler and whatnot compared to standard library derives, which are under heavier optimization scrutiny and resources, as well as their having access to unstable features and whatnot[1].

    3. Renamed-type hack to trick the compiler into producing a perfect derive

      This is better explained with an illustrating example, using the OP's situation:

      #[derive(Clone, Copy, Default, Debug)] // etc.
      struct Foo_<Phantom>(Phantom);
      
      type Foo<T> = Foo_<PhantomData<T>>;
      
      fn demo<T>(it: Foo<T>) {
          it.clone(); // Look ma, no `T : Clone`!
      }
      

      Needless to say, this results in awful docs, so it is extremely unadvisable for public types, and the type definition won't "forward" stuff such as braced/tuple struct literal syntax, nor the corresponding patterns. But it is quite convenient for the occasional internal helper type :upside_down_face:


    Which means that somebody running into this lint can either:

    • silence the lint;
    • go with one of these unsatisfactory workarounds.

    Which may leave a sour taste in the developer, a bit more than if they never ran into the lint in the first place perhaps because they did not need it :sweat_smile: (but if so, silencing the lint would be warranted)


  1. e.g., IIRC, the #[derive()]-generated impl Clone, in the #[derive(Clone, Copy)] case, would specialize over Copy to make Clone be a bit copy in that situation. A third-party lib cannot do that. ↩︎

2 Likes