Make `#[derive(Copy)]` enough for both `Clone` and `Copy`

Also for PartialEq and Eq, and PartialOrd and Ord. This would help in reducing the derive noise on types:

// Previously: #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
#[derive(Eq, Ord, Copy, Hash)]
struct Foo {
    x: i32,
    y: i64,
}

There may be some implementation difficulty about conflicting impls generated by derive(Copy) and derive(Clone) or manual Clone impls. But since these are builtin, the compiler can handle it by using a #[rustc_interanal_ignore_this_impl_in_presence_of_others]. We can fix the semantics and emit error about conflicting impls over an edition.

This is called specialization, so no, it can't.

2 Likes

It doesn't need to operate at the trait solver level, It can syntactically disappear the impl block if there is an impl Clone for whatever variant of the type with any generic bounds. If it removes the impl Clone too aggressively, people can just add the Clone derive again.

I'm not saying this is the best way to implement it, just that the implementation side is not dead-end.

Then it will be a breaking change.

Why? Can you give an example of the code that will break with this change?

I think making macros be conditional on trait solving is a big ask. Especially since which traits are dependent on macro output.

1 Like

If you scan the whole crate, no, but macros cannot do that currently (not even built-in macros), this will be a big change, hurt performance and incrementality, and something that rust-analyzer cannot implement (and as a member of T-rust-analyzer makes me feel deeply uneasy).

1 Like

I meant a pure syntactic solution, based on syntax heuristics. False positives would result in this feature not working in some rare cases.

Makes sense. So the syntax based solution is probably not a good idea.

What about doing this in the trait solver, but not with completely solving and relying on specialization? Trait solver needs to collect all trait impls from the crate. If it sees a #[ignorable] impl Clone for Foo<Something> and another impl Clone for Foo<Another> or impl Clone for T, it ignores the #[ignorable] one.

What should happen if two impls partially overlap?

The ignorable one becomes completely removed, and the user should add the Clone derive again. It may lead to some surprising corner cases, but I expect it to work in 99% of cases.

To me it sounds like solving the "I don't like writing Clone," with an over-complicated solution which has a high chance of false-positives and/or significant performance regressions. Also becoming a potential SemVer hazard and taking from Rust's intentional explicitness.

Also, can't this be implemented as an external crate with some #[derive(Clopy)] to explore the actual usefulness for the ecosystem?

5 Likes

Can you please explain this? I don't see any semver risk here.

Since the Clone is super trait of Copy, this is similar to implied bounds. Do you consider T: Copy instead of T: Clone + Copy a bad thing, reducing the Rust's intentional explicitness?

There is a macro_rules_attribute which does this among other things, and also a derive_aliases which is more focused on this problem. We can do an analysis on popularity of this pattern, but it is definitely not a problem invented by me.