How does #[derive(Copy)] detect drop handlers?

  1. If something has a drop handler, it can not be #[derive(Copy)] right ?

  2. How does #[derive(Copy)] know if something has a drop handler ?

  3. If it does not check for drop handler directly, what mechanism is it using that ends up implying "if there is a drop handler, we can't Copy" ?

edit: clone -> copy

1 Like
  1. Correct -- Copy and Drop are mutually exclusive, even for indirect Drop in any of its fields.

  2. I don't know the exact mechanism off-hand, but derive(Copy) is a built-in macro, and the compiler already has to know where to insert drop calls in the first place.

3 Likes

For question 3, there are a couple relevant compiler errors, again relying on the fact that the compiler knows where drops are required.

https://doc.rust-lang.org/error-index.html#E0184
https://doc.rust-lang.org/error-index.html#E0204

2 Likes

I don't think it does, because macro expansion happens before type checking. But even if it knew, it wouldn't need to know. Any eventual, manual Copy impls would fail too, since the Drop xor Copy mutual exclusivity is built into the type system. (Playground)

Actually, if you expand #[derive(Copy)] for a Drop type, it doesn't cause an immediate error; it expands to the trivial impl, like this. (Choose the "Expand macros" option in the menu!)

3 Likes

So, the real question here is how does impl Copy detect drop handlers, rather than the derive, as @H2CO3 just mentioned.

In that regard, I findit quite interesting that, in nightly, defining a NoDropGlue marker trait is trivial:

#![feature(auto_traits, negative_impls)]

auto trait NoDropGlue {}
impl<T : ?Sized> !NoDropGlue for T
where
    T : Drop,
{}

Having that, guarding Copy against bad impls would be as simple as adding NoDropGlue as a super trait.


That being said, here is the real implementation of the checks in the compiler:

The other interesting thing here, independent of the above implementation, by the way, but which seems paramount to me as well, is that

in order to implement Copy, all the fields must be Copy

which therefore allows to loosen the "no drop glue" requirement to a "no Drop impl". It does have the side-effect of not allowing Cell<impl Copy> be itself Copy, alas.

5 Likes

Currently checked in coherence here, using the helper @Yandros just posted.

1 Like

Heh, looking at the impl for the "structural check", I noticed that the check ignores lifetimes, so the following is accepted :upside_down_face:

#[derive(Clone)]
struct Foo<'lt>(&'lt ());

impl Copy for Foo<'static> {}

#[derive(Clone)]
struct Bar<'lt>(Foo<'lt>);

impl<'any> Copy for Bar<'any> {}

I would have expected this to be a case of "bad diagnostics" / another check later down the (compilation) line, such as borrowck, would take care of erroring on use of such impl for non-'static types:

fn copy(b: Bar<'_>) -> (Bar<'_>, Bar<'_>) { (b, b) }

To my surprise, it compiled fine!

  • I've managed to come up with a contrived scenario, that of a lib relying on TheirType<'not_static> : !Copy for soundness, where the above surprising impl can be weaponized to break such safety invariant: Rust Playground

  • Filed an issue

8 Likes