Description
@idanarye makes an excellent point:
Say I have a MyBox struct - an implementation of Box that uses a raw pointer directly. The fields are private - the module where MyBox is defined is that the only place where you can touch them - and thus the rule is that this module is responsible for maintaining the safety variants of the pointer within (and if it exposes any API that may violate it - it should mark it as unsafe)
Naturally, I should not impl Copy for MyBox because that would break the uniqueness invariant, and more specifically - when I drop one copy and the memory is released I'll still have the other copy with a dangling pointer.
But I can impl Copy for MyBox in a different module of the same crate.
I had never realized this, and I think it can be viewed as a violation of our privacy rules: outside modules in the same crate are not able to access the private field, and yet they are able to make the type copyable!
@rust-lang/types Is there any chance we can fix impl Copy
(over an edition, presumably) so that it is only allowed inside modules where all fields of the type are accessible, i.e., where you could have written the obvious Clone
impl that copies all fields?
Activity
CodesInChaos commentedon Aug 9, 2024
An alternative would be to remove the special rules around implementing
Copy
, and make implementing itunsafe
with a safe derive. This should be possible to do in an edition.This rule would bring it in line with other unsafe marker traits, which can also implemented elsewhere in the same crate.
Though it would unnecessarily require
unsafe
for generic code wherederive
infers incorrect bounds. So probably not a great solution.RalfJung commentedon Aug 9, 2024
I think we definitely want to keep the ability to write a safe
impl Copy
and have the compiler do all the required checks -- that's quite useful to have.Being able to write
unsafe impl Copy
as a way to bypass these checks makes a lot of sense and I'd like to see it happen, but that's a separate discussion from this issue.lcnr commentedon Aug 9, 2024
Pretty sure we can! We already have checks that all fields are actually copy-able, so also checking that they're visible at the location of the impl doesn't feel too challenging. It also feels very reasonable to do so. I think we should future compat lint this everywhere. Don't have strong opinions whether to increase the severity of the lint in editions
@idanarye this is an excellent observation and thanks @RalfJung for moving it into a new issue and pinging t-types/me :3
the8472 commentedon Aug 9, 2024
If we had unsafe fields (e.g. because the pointer is unsafe to access/expose) then could the unsafe in
unsafe impl Copy
be conditional on that?lcnr commentedon Aug 9, 2024
form an impl perspective, sure
traviscross commentedon Aug 9, 2024
Unpin
can also be safely implemented in the same crate but outside of the module that some type is defined, and doing so could create unsoundness. How do we think about that in comparison to the point raised aboutCopy
here?RalfJung commentedon Aug 9, 2024
I think making
Unpin
a safe trait was a mistake for a number of reasons... (see e.g. rust-lang/rfcs#3467)idanarye commentedon Aug 9, 2024
What if instead of the rule being "
Copy
can only be implemented on a type from a module that can see all its fields" it'd be "_Copy
can only be implemented on a type from the same module that declared the type"?This will mean, of course, that even if a struct only has
pub
fields you still won't be able toimpl Copy for
it from a different module. This is a little more strict that what the safety-by-encapsulation principles dictate, but we won't lose much from that because moving theimpl Copy
to the same module as the type is almost always a straightforward fix (and definitely better than making the fieldspub
just so that you won't have to move it)The advantage of this approach is that it does not rely on the fact that
Copy
already looks at the fields, and thus it could be done with an attribute. The same attribute could also be placed onUnpin
- and also other marker traits likeSend
andSync
.If we go that route, it'd be important to remember (not that it can be forgotten - the standard library will break if this neglected) that the attribute should not prevent implementing owned traits on foreign types from. This does raise a question though - should this attribute restrict that kind of
impl
to the module that defined the trait, or should it allow it from any module in the crate that defined the trait?RalfJung commentedon Aug 10, 2024
Send
andSync
are unsafe traits, I see no reason to subject them to constraints like this.traviscross commentedon Aug 10, 2024
I'm a bit torn on whether the trait being safe or unsafe to implement should distinguish these cases or not. Even with
Copy
, you have to be usingunsafe
somewhere for this impl to produce UB, and we accept that in writingunsafe { .. }
you are also making guarantees about what you do and do not do within in the crate outside of theunsafe
block.As another angle, as the concern here is about seeming to violate field privacy, it's not obvious to me that one of the powers of an
unsafe impl
should be to violate field privacy. I.e., it's not obvious that's part of whatunsafe
should mean.RalfJung commentedon Aug 10, 2024
The way I view
Copy
, it is an unsafe trait (it fundamentally has safety-critical promises attached to it) with some compiler magic that can identify cases where the trait is actually safe to implement. However, that magic has a hole since it does not respect field privacy. That's why IMO it makes sense to considerCopy
as quite different fromSend
/Sync
.However, there's probably other ways to view the situation that can lead to different results.
idanarye commentedon Aug 10, 2024
traviscross commentedon Aug 10, 2024
ia0 commentedon Oct 22, 2024
We don't have negative impls yet, but wouldn't a possible alternative (not necessarily better, but for completeness of design space exploration) to instead require of users to
impl !Copy for TypeWithNonCopyInvariants {}
when needed?In this particular example, the module defining
MyBox
would alsoimpl !Copy for MyBox {}
which is required to be present in their proof of soundness. Other modules in the same crate won't be able toimpl Copy for MyBox {}
anymore, and the safe language remains sound.Again this is for completeness only, because it adds some additional burden on unsafe Rust users, now having to explicitly state that their types are not
Copy
, instead of it being implicit. So there's some design trade-off between corner-casingCopy
and unsafe user convenience (I'm assumingnegative impls
to be a general enough idea, that its cost is factorized with other uses, and thus negligible for this particular case).Sky9x commentedon Feb 26, 2025
To implement
Copy
for a type, all of its fields must beCopy
, and it must not implementDrop
. No type can implement bothCopy
andDrop
(ie. they are mutually exclusive:needs_drop<impl Copy>
is always false, andneeds_drop<impl Drop>
is always true).This specific situation has always been impossible. If your type "manages" some resources (RAII etc), it or one of its fields must
impl Drop
, and therefore the compiler will reject any attempt toimpl Copy for MyBox
.This is technically an issue for all traits, but
Copy
is special in that adding a safe impl might cause UB (depending on your API guarantees). I'm not sure how severe it is, as such an impl could only be added within the crate (ie. code that you control, so as long as you don't write such an impl your code is fine).That being said, there are workarounds to ensure that the compiler will reject such an impl:
impl Copy for Foo
can be prevented with an emptyDrop
impl:or by adding a non copy ZST field:
An empty
Drop
impl works almost like animpl !Copy for Foo {}
(however, it does prohibit destructuring and makes the type always!needs_drop
). The!Copy
ZST field avoids these restrictions, but is perhaps less obvious. Maybe we want to add some kind ofimpl !Copy for Foo {}
.RalfJung commentedon Feb 26, 2025
That's a cute hack but does not solve the fundamental problem. I should be able to attach invariants to my fields without having a
Drop
impl.Sky9x commentedon Feb 26, 2025
I think the solution here is negative impls.
For example, if I have some type where I've opted out of
Unpin
via aPhantomPinned
marker field, there is once again nothing stopping animpl Unpin for Foo {}
from being written outside of the defining file (and no cute hacks like forCopy
). Animpl !Unpin for Foo {}
would solve that.ia0 commentedon Feb 26, 2025
There are many "solutions" to this "problem". From the zulip thread:
MyBox
implementsDrop
MyBox
implements!Copy
MyBox
moves to its own crateMyBox
reminds unsafe reviewers that there is only one crate author (not one author per module) and so the scope of unsafe is the crateCopy
can't be derived without visibility on the fieldsCopy
becomes an unsafe traitI'm using quotes because it's not clear whether it's a problem, due to the lack of real examples. There are at least 2 ways to describe the problem from a theoretical point of view:
Copy
, because it's a trait that affects the semantics of code, making it less modular to understand the semantics of code.