Is Copy on enums OK?

Another possible option is to just not have the getter.

The main time getters are idiomatic in Rust is when you want to keep the field private to maintain an invariant or maximal flexibility, but need to allow other modules/crates to access the field. If this is true for you, getting rid of the getter probably isn't an option.

Even when you have a getter, it's more idiomatic to just access the field when possible, as that is more flexible with regards to the borrow checker. I thought perhaps there would be a lint for this, but I didn't find one.

4 Likes

Sure thing. https://doc.rust-lang.org/std/marker/trait.Copy.html

2 Likes

Wow, you're right. I thought I'd narrowed it down in my real code to the Copy. Now I'm even more confused. <thinking...>

For what it's worth, I tend to lean in this direction...

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.

Start with enums non-Copy then add it if not having it becomes a burden or having it is obviously the correct choice.

A good example are state machine states (which I think is your example). Non-Copy has served me well. In the rare circumstances I've need to copy the value .clone() is not too much typing to be a burden and makes the intent crystal clear.

:+1:

1 Like

I'd say Copy is great. If you have a simple enum without data you should make it Copy, because it makes using it much easier, and possibly even a bit more efficient (since a copy of the value is smaller than a pointer).

The footgun here is more from having a in-place-modifying &mut self method on a Copy type. The mutate() could have been #[must_use] fn into_banana(self) -> Self.

4 Likes

Is there a clippy here for &mut self and Copy?

1 Like

Hmm, is there anything specifically because of the enum-ness that causes this? Wouldn't calling a &mut method on a non-enum (like a u32) have exactly the same behaviour?

1 Like

I guess the Copy here is a red herring. Or at least it's surely not the only one responsible.

Sometimes, when you have a chain of errors leading to a bug, you can't clearly point to one of them and say "now that is the problem!".

I guess the nicest example of this phenomenon is shared mutability. Programmers have been arguing for decades whether it is sharing xor mutability that causes memory safety bugs:

  • "It's threads!" – shouted JavaScript and Python, and JS remained single-threaded, and Python introduced the GIL.
  • "It's mutability!" – screamed Haskell and Erlang, and they made (almost) everything immutable.

And then along came Rust, and said: "you are fools! You can have both sharing and mutability in the same language, as long as you isolate them from each other."

Similarly, I'd say a Copy enum is not a problem in itself. If it's a trivial enum, make it Copy. The root cause is calling a mutating method on a temporary, which is partially caused by the getter returning an owned value. (Returning owned values is not a problem in itself, either.)

If I write the equivalent code on the Playground, Clippy doesn't flag it, and I couldn't find a related lint. I'm not sure what the right solution here is, apart from simply not making a getter.

14 Likes

In terms of designs for hypothetical new lints, maybe an equivalent for #[must_use] but for mutable reference arguments, could be useful.

1 Like

#[must_use] has a problem: “…wait, shouldn't I logically put this on almost every function? How much is too much?”. An analogous attribute for temporary-mutation would be the same way. The things which shouldn't get a warning are cases like std::cell::RefMut::deref_mut (used to mutate the underlying value that outlasts the RefMut) or std::process::Command::spawn (accepts &mut self but the builder is left semantically unchanged and may or may not be used); the majority of &mut self methods are not meaningful to use on a temporary.

Hm, considering those examples, that suggests a pattern: just warn on any call of a function with signature fn(&mut self, ...) -> () on a temporary. All of the examples above are cases where the function returns a non-() value. (It still might be a false positive on things which have a side effect outside of self, like a channel send() function does.)

1 Like

The Copy trait is defined as:

Types whose values can be duplicated simply by copying bits.

I would interpret this to mean that if I copy a Copy type, the bits of the duplicate must remain the same at all times, so &mut self on a Copy type should be illegal.

I don't follow. Why would the duplicate "remain the same at all times"? The duplicate is an independent value from the original.

For example, the following is perfectly fine, legal, compiles and runs as expected, and doesn't lead to any memory safety hazard or logical bug:

fn mutate(value: &mut u64) {
    *value += 1;
}

fn main() {
    let original: u64 = 42;
    let mut duplicate = original;
    mutate(&mut duplicate);
    dbg!(original, duplicate);
}

Hm, I guess you are right.

But then, where to draw the line. If normal &mut is OK for Copy, then why not &mut self for Copy.

I don't understand the distinction. What is a "normal mut"? Is there an abnormal one?

I meant &mut that is not &mut self. But, yeah, the point is that there should be no difference, so that's not a useful distinction.

Another thought: if I'm obtaining &mut T, that would typically be so that I can change it and others see the change, right? Because if I don't want to change it, then &T is suffcient, and if I want to change it but no one will see the change, then I can just take ownership.

In that case, could we consider it wrong by the compiler to crate a temporary to enable a &mut T?

Well, &mut self can be implicit, that's probably why people recommend against it. Non-self &mut is explicit, so it's harder to get it wrong and confuse it with a copy.

I don't understand what you mean by this. Can you elaborate?

I read it is "there should be a warning for calling a &mut method on a temporary" (where a temporary is a value dropped at the end of the current expression, just to be explicit)

I think it would be a bit too noisy as a warning, but it might be a good default clippy. You generally don't want to mutate temporaries, most builders want to at least support moving ownership down the call chain so the final build call can take from the builder for example. Maybe nonstandard lock guards' methods would want this? (The standard ones can directly go to derefmut, which has clear semantics)

In any case, a Copy type very much seems like it has no business with mut methods, semantically. The exclusion implied by the mut has no actual effect and can be defeated by a simple wrapping {}

I think this is overstated. I very much want get_mut to work on [u32; 10] even though it's Copy, for example.

1 Like

True, that's a great counter example, but if I can put up a pale defense of myself, Copy arrays are weird in a few ways.

My point is exactly that it's not clearly/generally the case. Even implicit &mut self needs to be allowed if you want to implement something as trivial as AddAssign on primitive numeric types.

Incidentally, at $JOB, I wrote some generic signal processing code that needed to accumulate values of a generic type (for computing convolutions and related operations), and the best design for that was passing a &mut to the appropriate internal method of the filter data structure.