Why don't primitive enums automatically implement Copy?

It feels like they easily could; they all can implement Copy, and it feels like a repr is as much a part of the public API as implementing Copy is, so removing the repr having the side effect of automatically un-implementing Copy sounds reasonable…

Are there places where it wouldn’t make sense for the compiler to auto-implement Copy for all enums with a primitive repr?

Because there are reasons to make a type not Copy. For example as a proof of work.

struct Empty(());
struct Fill<T>(T);

impl Empty {
    fn fill<T>(self, value: T) -> Fill<T> {
        // do something special here
        Fill(value)
    }
}

fn do_work<F: FnOnce(Empty) -> Fill<String>>(f: F) {
    f(Empty(()))
}

With this formulation, it is guarenteed that Empty::fill will only be called once or that the closure will panic. Using this information you can build some other apis on top of this. For example, a version of scoped threads or some form of the builder pattern.

None of this would be possible if you forced the compiler to auto-implement Copy.

2 Likes

I understand this in general, but I’m having trouble thinking of when this might apply to something which you’re specifically marking as being implemented as a primitive integer (which itself is Copy)…

But that is just one case, we can’t have the compiler special case only a few things or make Rust even more hard to learn. Also, how would Rust know when to implement Copy? Would it blindly try and implement Copy if it could? If so, then you would have to really think about every type and see if it should be Copy, because making it non-Copy would be a breaking change.

In my example, you could make Empty auto-impl Copy, and that would be a huge difference in semantics.

2 Likes

The specific algorithm I’d suggest is:

If the type is an enum, and has a repr attribute which shows that it has an explicit repr which is a primitive integer type, implement Copy.

Definitely nothing more general than that.

This doesn’t seem too crazy, because such an enum effectively is an integer (for which Copy is already implemented). Because of this, it also doesn’t feel like a particularly special case, if you view it through the lens of “an enum with repr(u8) is just a u8 with some sugar around it, and because u8 is Copy so is the enum”.

In particular, https://doc.rust-lang.org/std/marker/trait.Copy.html#when-should-my-type-be-copy states that a type should generally implement Copy if it can, and this seems like a corner of the language where that would generally apply.

I definitely don’t think this is a particularly important feature request, but I’m curious about it :slight_smile:

2 Likes

Oh, I see, that seems more reasonable. I think I misunderstood what you said earlier. I don’t think we can do this because it would be a breaking change.

1 Like

Keep in mind that in the same paragraph it also says:

If the type might become non- Copy in the future, it could be prudent to omit the Copy implementation now, to avoid a breaking API change.

Which would definitely apply to many enums that are #[repr(u8)] purely as part of guaranteeing that the layout of the enum won’t change.

#[repr(u8)] enums are laid out like a union of #[repr(C)] structs with a u8 as the first field of all those structs.

This comment is irrelevant if what you mean was field-less enums with a #[repr(u8)] attribute.

As an example of a #[repr(u8)] type which is not Copy here is an enum declared in abi_stable,as part of implementing basic reflection:

#[repr(u8)]
#[derive(Debug,Serialize,Deserialize)]
pub enum MRFieldAccessor {
    /// Accessible with `self.field_name`
    Direct,
    /// Accessible with `fn field_name(&self)->FieldType`
    Method{
        name:Option<String>,
    },
    /// Accessible with `fn field_name(&self)->Option<FieldType>`
    MethodOption,
    /// This field is completely inaccessible.
    Opaque,
}

If I had removed the Method variant,this type would suddenly become Copy(with those rules),even though I don’t want to guarantee that it can be Copy .

Reference:RFC 2195,where the representation attributes are given a defined layout.

1 Like

A similar case like struct NewType(i32) also doesn’t implement Copy automatically, not even when it has #[repr(transparent)]. Supporting Copy has semantic meaning that must be explicitly opted into.

7 Likes

This suggestion seems reasonable, but it has huge learnability problem. Currently all types do not impls any trait by default, except for auto traits. But with this suggestion, it becomes: all types do not impls any trait by default, except for auto traits, or if the type is an enum, and has a repr attribute which shows that it has an explicit repr which is a primitive integer type, then it impls Copy. And everybody should remember this edge case, or they will face the “Why my enum impls Copy? Oh wait, this is the case when…” moment. Compare this with just adding #[derive(Clone, Copy)] line over such types. Will it worth it?

Anyway, it would be breaking change for all existing non-Copy such enum types.

2 Likes

Here is a potentially interesting trick: if you are afraid of forgetting to impl / derive Copy for a “trivial” case such as a simple enum (when there is no gigantic variant among them), you can enable the #![deny(missing_copy_implementations)] lint and then use #[allow(missing_copy_implementations)] as an “opt-out” Copy on the defined structs / enums that you may wish to “un-Copy” (there is also missing_debug_implementations, which I find extremely helpful)

6 Likes