Can we avoid the orphan rule for empty traits?

I recently had a situation where I wanted to use bytemuck::bytes_of to convert a reference to an object to a reference to the underlying bytes, but it requires the type implement the NoUninit trait, and the type came from a crate that doesn't implement the trait even though the type meets the requirements.

Generally, for coherence purposes, the orphan rule is necessary to avoid having multiple implementations of the same trait for the same type, which could lead to ambiguity as to which implementation to use. But, in the case of this trait, there are no items in the implementation (no functions, constants, or associated types), so all implementations must be identical[1]. And semantically, these traits typically represent a property of the underlying type, such that implementing the trait is asserting the property and so it's idempotent to assert the same property multiple times.

I expect we'd probably want a lint to help people avoid accidentally implementing traits on external types when they don't mean to, but I see no reason why Rust couldn't allow something like this:

#[allow(impl_empty_external_trait_for_external_type)]
impl ExternalTrait for ExternalType {}

I think this would complicate negative trait bounds, since you can specify a type and trait and not locally know if the type implements that trait, and also specialization means you might have a concrete type and not know if it implements traits for one specialization or another. But as far as I can think of, nothing presently stable could cause a problem if this change happened.


  1. NoUninit requires Copy, but if the crate implements Copy for a type, then downstream implementations of NoUninit must be identical ā†©ļøŽ

2 Likes

It was discussed and rejected for marker traits here:

5 Likes

Sounds like you might be asking for this?


EDIT: Oh, you're talking about Orphan, not Overlap?

Removing overlap is easy (with the trait opt-in to avoid semver hazards); removing orphan seems way more questionable.

The orphan rule mechanically prevents multiple implementations, but it also protects a library author’s freedom to change how their type functions. If the type foo::Bar you were using is currently NoUninit-compatible in foo 1.0, well, that’s not normally an exposed property of a type, and so it might not be anymore in foo 1.1. Of course, that might be overly conservative—for example, if Bar’s members are all public and it satisfies all the bytemuck::NoUninit requirements in practice—so I’m not saying it can never work, just pointing out that the orphan rule is theoretically justified as well as practically.

Auto traits are already an exception to this—occasionally a problematic one, if foo’s author didn’t realize a client was depending on Bar being Sync in 1.0, for example. Extending this to arbitrary traits would be…well, you can see there were previous discussions on it.

10 Likes

Yeah, that is a potential concern, which is why I thought about having a lint you'd have to #[allow] to do the implementation. The error message on writing such an orphaned implementation should probably say something along those lines.

In my particular case that inspired this post, the type came from libc and is guaranteed to not have any uninitialized bytes, but libc doesn't take bytemuck as a dependency to implement its traits (which I think is the right choice for libc), nor does bytemuck take libc as a dependency to implement its traits on libc types (it could, but among other arguments against it, adding a big dependency would likely slow down compile times for dependents). So I'm stuck with a type that can't implement NoUninit for reasons of crate boundaries, but which otherwise could implement the trait, which is exactly when this would be useful.

2 Likes

I've been thinking for a while that there should be some way for crates to say "compile this code only if this other crate exists in the crate graph within this version range", without being an actual dependency – that would allow that sort of interoperation between crates. (This is better than an optional dependency because if the other crate exists twice in the crate graph due to being imported with two different versions, you would get two copies of the code that requires both crates, in order to support both versions.)

Global features addresses this problem in some capacity.