Why is the value expression with the type implemented Copy not desugared to `e.clone()`?


#[derive(Debug)]
struct A(i32);

impl Clone for A{
    fn clone(&self)->Self{
      let r = self.0+1;
      A(r)
   }
}
impl Copy for A{}

fn main(){
    let x = A(0);
    let b = x;  // #1
    println!("{b:?}");
}

We know that the type that implements Copy trait must first implement Clone trait, So, why is the value expression x at #1 not expanded to x.clone()? What's the designed theory that let b = x; trivially copies x even though x does not have a trivial clone?

Moreover, in this context, let b = x; have a different effect from that of let b = x.clone();, which looks like inconsistent.

In C++, if we user-defined copy constructor, then let b = x; is desugared to A::A(b, x);, which means the side effect in the copy constructor will apply to the object b.

Why should it be? When you write an expression with x, x is moved. Copy means the original x is still valid.

Clone can be expensive so implicitly calling it could be dangerous.

1 Like

Why should it be?

Because implementing Copy requires Clone, which sounds like the behavior of Copy is established on what you want for Clone. However, we may want some side effects in the clone, as we do for the copy constructor in C++, in Rust as indicated in the above example, the copy semantic will have a different meaning from that of Clone.

The meaning of Copy is a promise that the type can be moved without invalidating the original. And a move is always a bitwise copy. Copy implies Clone because any Copy type can also be cloned; the clone implementation is simply a trivial assignment. You should practically never manually implement Clone for a Copy type, and if you do, the implementation of clone() should not do anything besides the simple assignment. Copy does not exist for making implicit clone() calls happen; Rust simply doesn't do C++-like implicit function calls on assignments.

7 Likes

Copy doesn't have any behavior though. I think this is a good thing. Even though Copy: Clone this really is a constraint on how Clone should behave.

C++ constructors are very complicated and mostly exist to solve problems that C++ or backwards compatibility with C created. It's hard to look at a fragment of C++ code and know what is actually being executed due to features such as perfect forwarding, all the different value categories, etc. Of course there are many things you can do in C++ (placement new!) that you can't do in Rust as easily.

By this logic, the Copy trait shouldn't require the implementation first implements Clone because Copy merely means the bit-wise copy and the source's ownership is not moved to the destination while the Clone can have arbitrary complicated "copy". What I mean is I don't know why the Copy trait requires Clone if they do not have an established relationship.

What I actually meant is, since Copy trait merely means bit-wise copy while Clone can do arbitrary complicated "copy", why does Copy trait requires implementations first to implement Clone? The Copy trait should be complete as a single mark trait without any super trait because the behavior of Copy and Clone does not have an established relationship.

Clone is a supertrait of Copy, so everything which is Copy must also implement Clone. If a type is Copy then its Clone implementation only needs to return *self

If you wonder the rationals behind, it's because Clone is a generalization of Copy which allows user defined duplication logic(like copy constrictor) so it's natural that every Copy types should also impl Clone. Ideally compiler may autogenerate impl Clone for every Copy types, but it requires complex language features and it was decided to not worth it to delay to release Rust 1.0 for about a decade just for it.

2 Likes

By this logic, I am more sure that Copy should be as a single marker-trait without requiring implementations first implement Clone. The language totally can do something like this for these types that implement Copy

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

This default implementation for types that implement Copy can prevent user-defined incorrect Clone for types that implement Copy.

In other words, Copy should be detached from Clone. Semantically, the implementation of Clone for Copy types should be restricted as a bit-wise "copy".

The current design for Copy can make an error-prone for implementing Clone

That's exactly what was said before:

And now it would be breaking change to add this.

It won't, if one just does the most natural thing and implements them both with derive - if the type can be Copy in the first place, there's no profit in making the implementation manually (barring some very niche cases like Wrapper<T>(fn () -> T), where derive would be more restrictive then the manual impl).

What if we manually implement Clone for T: Copy and have a complicated "copy" in clone? Is it make the program UB? Or, is it just the right implementation?

It's the same kind of error as e.g. PartialEq not being an "order" in mathematical sense - i.e. no UB, but possible unexpected behaviour due to logic errors. Point is, you almost always don't want to implement Clone for Copy type at all - it's the unnecessary complication of code (even with the expected logic), given the existence of derives for both.

1 Like

So, Clone is clone and Copy is copy, detaching Copy from Clone seems to make sense, all T that implement Copy must have a boilerplate implementation of Clone, in other words, we are even not necessary to implement Clone for a type that is Copy. A type that is Copy has a bit-wise copy in any context.

Again, that's exactly what was already said before - yes, it is desirable, no, it's not as easy as it seems to be.

Why no? Seems to me exactly like what Rust epoch approach may fix: make compiler magically implement Clone for Rust 2024 module if Copy is derived and forbid to derive Clone for such types.

For all practical intents and purposes it would be like that blanket implementation @xmh0511 is talking about.

1 Like

I even think Clone shouldn't be used as the super trait of Copy because the type that implements Copy just does bit-wise copy in any context regardless of how the user implements Clone for that type. By this logic, the Copy trait should be an individual marker trait because the behavior of Copy does not depend on Clone.

This is obviously unworkable since there's code relies on T: Copy implies T: Clone. Even if we can make this breaking change, we probably still won't do it. This just create noise at trait bound site without any practical benefit. i.e. You have to write T: Copy + Clone instead of T: Copy.

I kind of doubt saving 7 character (", Clone") worth the introduction of more magic.

There are self-imposed hard constraints on what editions can break which would make this very hard if not impossible. For example it's required that warning free code in an edition must still compile with the same behaviour in the next edition, and your proposal would definitely break this. Moreover even such hard errors are expected to hit only a very small percentage of code, which is very far from the amount of code that derive Copy and Clone.

And you can't use T: Clone at all without excluding Copy types.

Because everything that is copiable is also, by definition, clone-able (trivially). It doesn't ever make sense to have a type that is Copy but not Clone, hence Clone must be a supertrait of Copy.

This is the exact same relationship as there is e.g. between Ord and PartialOrd. Every type that induces a total order also, by definition, induces a partial order. So it doesn't ever make sense for a type to be Ord but not PartialOrd. Hence, PartialOrd is a supertrait of Ord.

4 Likes