Cannot call clone automatically

At A compiler know it is a copy instead of a move, but does not call my clone implementation. It is anti-intuition.

struct S(u16,u16);

impl Copy for S {}
impl Clone for S {
    fn clone(&self) -> S {
        S(self.1,self.0)
    }
}

fn main() {
    let a = S(1,2);
    let b = a;          // A
    let c = a.clone();
    
    println!("{} {}",a.0,a.1);
    println!("{} {}",b.0,b.1);
    println!("{} {}",c.0,c.1);
}

Copies (or moves, which are the same thing) made by the compiler never call clone(). Copy means exactly one thing: when you move something of a Copy type, the original value is still a valid value of the same type. Implementing Copy does not insert copies or clones automatically or anything of that sort.

Implementing Clone to do something other than a trivial copy is technically possible, but can lead to weird bugs. It's a violation of Clone's contract, as the documentation says:

Types that are Copy should have a trivial implementation of Clone.

("Trivial" means not doing anything other than copying the values from the old struct to the new one. Swapping values is a non-trivial implementation.)

6 Likes

C++ made the mistake of inserting implicit copies and people have been cursing it ever since.

With implicit copies it's too easy to accidentally copy expensive data and kill performance, whereas if you make expensive copies explicit (via a call to the clone() method) then when optimising you just need to grep for clone() to find easy performance wins.

5 Likes

Thanks. I have to update my understanding : compiler do physical copy automatically , leave logical copy to programmer.

2 Likes

When I run your code it does exactly what I might naively expected even if I had never seen Rust before. I'm not sure what is "anti-intuition." about it. Why do you expect it to do something different?

After a year of Rust what I find "anti-intuition." about copy and clone are explanations like that.

When I have "b = a" then it certainly does copy, overwriting the original value.

Oh yeah, could have some fun with that:

#[derive (Debug, PartialEq)]
struct S(u16,u16);

impl Copy for S {
}

impl Clone for S {
    fn clone(&self) -> S {
        println!("Clone");
        S(self.1,self.0)
    }
}
fn main() -> Result<()> {
        let a = S(1, 2);
        let b = a;
        let c = a.clone(); 

        assert!(b == c);   // WTF?
}

In my mind I have the following model of copy and clone.

Some types are small and simple. Just a handful of bytes. Likely created on the stack as local variables in some function. A such when a copy is required, as in "a = b" the "Copy" trait just does that, copy the bits of "a" to "b". This is obviously what one wants for simple integers and the like.

Other types can be much bigger. Structs containing lots of fields. When they get big copying all those bits around becomes an expensive operation that one may not want to do. So they do not have the "Copy" trait by default. This way the programmer does not accidentally start using lots of expensive operations just by writing things like "a = b". However if the programmer knows is structs are small and copying their bits around is not expensive he can put the Copy trait on them.

Things get more interesting for types that contain pointers to data that is outside the actual type itself. For example vectors and strings. Here the type is a small number of bits, likely on the stack of a function, but it contains pointers to possibly large amounts of data in heap memory.

What would it mean to "copy" such a things? A simple copy of the bits of the vector on the stack would create two copies of pointers to the data on the heap. Not good. A "deep" copy of those bits and all the data they point to could e very expensive. Not good. Again the programmer can opt-in to the expensive operations by implementing Clone.

Someone correct me if any of that is misleading.

1 Like

By "original value" I mean a, not b.

a = b; always does the same thing for all types, Copy or not. It copies the bits just like in C.

Again, a = b; is equally expensive whether or not the type is Copy. There may be a second order effect where making a type Copy allows the programmer to do things that inhibit the compiler's ability to optimize away that cost, but implementing Copy itself has no effect on code generation at all.

I actually believe that @yushang's observation that there is a difference between physical copy and logical copy is key to understanding Copy. People (including myself earlier in this thread) tend to use the word "copy" without specifying which they mean. Which one you mean is usually deducible from context, but only to people who already understand the dichotomy, so it's easy to talk past each other. Here's my attempt at explaining it:

  • A physical copy is the process of taking bits and replicating them in a new place. In Rust this is also called move.
  • A logical copy is the process of taking a value and turning it into two new values. In Rust this is also called clone.

Rust does implicit physical copies (moves) all the time. a = b; is a move, which is a copy of the bits from b into a. Returning things and passing things as arguments to functions are also examples of moves, which are copies. Each move, which is a copy, might be compiled to a memcpy, or a single instruction, or no code at all.

At first glance, it seems Rust never does implicit logical copies. You always have to ask for one by calling clone(). But if doing a physical copy results in a logical copy, you can implement Copy to tell Rust to allow you to continue using the moved-from value. That's why I said

When you move something of a non-Copy type, the bits are still copied, but the original copy becomes unusable. The same bits are still there, but they no longer represent a valid value of that type. That's the only difference between a move and a copy, and also the only difference between Copy and non-Copy types.

4 Likes

I do believe you are right. The more we talk about this the more confusing it gets.

In my naive mind, and coming from the physical world as I do, when an object is "move"ed it ends up in a different place and there is still only one of it, when an object is "copy"d there are now two of the same object in different places. "clone" is essentially the same as copy, as in genetics.

Also, when an object is "move"d it may well get a new owner. When an object is "copie"d each copy can have a different owner.

Meanwhile, in computer land (well C), "copy" means duplicating the bits, you end up with the same pattern of bits twice. The is no notion of "move", when a pattern of bits turns up elsewhere the original is still there. "Ownership" of course is unheard of.

Quit so. But here is the thing, if those bits being copied are a struct that contains a pointer to data elsewhere then you end up with two structs and two pointers to that same data. C will not copy that data automatically. Oops, you have multiple read/write references to the same data.

Starting from there, I'm still not sure how Rust-move, Rust-copy, Rust-clone map on to the conventional meanings of the words.

When I write:

    let mut a: Vec<u32> = vec![];
    a.push(1);
    let b = a;
    a.push(2);

I get an error about "borrow after move". Clearly that assignment has not done a copy as I would expect, it has done a move. The compiler says so.

From which I conclude that rust_move = real_world_copy (that does not work)

But I can do this:

    let mut a: Vec<u32> = vec![];
    a.push(1);
    let b = a.clone();
    a.push(2);

From which I conclude that rust_clone = real_world_copy. (that does work)

All this changes for simple types.

    let mut a: u32;
    a = 1;
    let b = a;
    a = 2;

Which works.

From which I conclude that rust_copy = real_world_copy for simple types. It is not a move because I still have the original.

See. Confusing. Somehow I live with it :slight_smile:

This discussion can distinguish between "shallow copy" (memcpy) and "deep copy" (replication of a multi-level rooted tree structure). I don't have time to provide examples, but perhaps this hint may help. Remember that clone() can have a custom definition, whereas copy() cannot.

2 Likes

Your original answer was perfect, don't over-think things :wink:

When you analyse things in loads of detail often you'll find that everyone is actually agreeing and saying the same thing, but you all get confused because different words or analogies are employed.

4 Likes

Yes. Exactly. Shallow vs deep. As I was alluding to above.

Yep. I think I'll stop thinking about it. And cut some code...

It's just that the first reply here, by trentj, which apparently is now the 'solution' says:

Implementing Copy does not insert copies or clones automatically or anything of that sort.

Which I find confusing because that Rust 'Copy' does indeed "copy"/"clone" in the colloquial sense of the words.

This code doesn't follow the requirements that are specified for what it means to be Copy, so bad (but safe) behaviour is to be expected.

There's even a whole RFC saying that the compiler is allowed to remove .clone() calls for types known to be Copy: