Docs example for manual implementation of Copy is circular?

In the docs for Copy, they give following example of a manual implementation:

struct MyStruct;

impl Copy for MyStruct { }

impl Clone for MyStruct {
    fn clone(&self) -> MyStruct {
        *self
    }
}

But this definition is circular. The implementation of Clone requires Copy, because it moves out of a shared reference. But the definition of Copy requires Clone, because it is a super trait.

It doesn't seem like this should be allowed. I also don't know how it makes sense in the general case, especially if the type stores allocations. Is the copy just a bit-wise memcpy? Couldn't that cause safety issues? I must be missing something.

Yes, Copy is akin to memcpy.
It requires clone, but it doesn't call it or need it.

In order for a type to be Copy, all of its members also have to be Copy, so you won't have a problem with allocated heap types (since they're not Copy)

I guess that makes sense.

I had assumed Copy existed purely at the language level and simply desugared to object.clone(). If that were true, then the example would be circular and invalid. But I guess Copy must be handled as a special case by the compiler and is always a bitwise copy.

Doesn't that mean the output of Copy and Clone could be different? There's no mechanism to prevent that, right? That just seems a little counter-intuitive to me.

That's right.

That's not right.

The only thing Copy does is changing ownership semantics. It doesn't change codegen in any way, since moves are compiled down to memcpy too.

Yes, and this is explicitly stated in documentation:

(emphasis mine)

5 Likes

Ah that's in the Clone documentation. I was reading the Copy documentation.

Why not reverse the dependency then? Couldn't you have Copy as a marker trait and a blanket implementation of Clone for types that are Copy? That would still allow users to implement Clone without Copy, but it would prevent any breakage of the invariant.

Essentially, in what case would it ever make sense to have a non-trivial implementation of Clone for T: Copy? I can't seem to think of one.

Yes, this would be desirable, but due to limitations in Rust's trait system, it would prevent things like other generic implementations of Clone. For example, we would now have two conflicting generic impls that both apply to Option<u8>:

impl<T: Copy> Clone for T { ... }

impl<T: Clone> Clone for Option<T> { ... }

Edit: These would conflict because of this other existing impl which we would still want to have:

impl<T: Copy> Copy for Option<T> {}

This was discussed a bit in this old blog post: Intersection Impls · baby steps

7 Likes

@mbrubeck, I've run into the competing generic impl problem before, but your example doesn't seem to be one.

This compiles just fine on the playground:

trait Copy2 {}
trait Clone2 {}
impl<T: Copy2> Clone2 for T {}
impl<T: Clone2> Clone2 for Option<T> {}

Am I missing something?

You're missing impl<T: Copy2> Copy2 for Option<T>.

1 Like

Also Copy is a subclass of Clone. But I don't know if it changes anything here.

We're discussing an alternate world where Clone is not a super trait of Copy.

1 Like

Ah ok, yes. But in this world, that would only be a problem for types that are Clone but not Copy though, right?

It's a problem for Option, because it means that Option<i32> can't be Copy2, only Clone2.

If you change the impls you can make it so instead Option<i32> is Copy2 and Clone2, but then Option<String> can't be Clone2 because String isn't Copy2.

Yes, that's what I was trying to say. You can't use all of the blanket impls that make sense simultaneously.

@mbrubeck, it might be helpful to edit your post to include the missing impl that makes it fail to compile. It wasn't immediately obvious to me, so I think it might not be to others who find this in the future. Thanks for explaining. And thanks to @trentj for clarifying.

1 Like

The circularity is okay not just because Copy is special. You can use the implementation of a subtrait to implement the supertrait in general. Another typical example of this is Ord which requires PartialOrd, where usually PartialOrd is implemented using Ord:

impl PartialOrd for Foo {
    fn partial_cmp(&self, other: &Foo) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

playground

2 Likes

Interesting. I didn't realize you could do that generally. I'll keep that in mind. It might come in handy.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.