Confusing std::error::Error source code

In the documentation, I can see three impl dyn Error + 'static blocks, but when I look into their source code, first one is written as impl dyn Error + 'static, and second/third as impl dyn Error.

I know that 'static is the default lifetime of impl dyn Trait, so all of them refer to exactly the same thing, but why is the source code written this way? Is it just a minor inconsistency or maybe it has some meaning I don't know about?

std::error::Error implementations page

It's just a minor inconsistency. Using the explicit version would be better to signal intent IMO.

impl dyn Trait<'_> can mean something different in niche circumstances, but I don't think I've ever seen that in practice and it doesn't apply here as Error has no lifetime parameter.

1 Like

I don't want to create new threads just to post this, so I'm doing it here:

There's an implementation for Error, this one:
impl<'a> From<&str> for Box<dyn Error + Sync + Send + 'a, Global>
Is there any good reason that the trait object inside Box has 'a lifetime, not just 'static? The object is created from String, which should live forever, like the other implementation, e.g.
impl From<&str> for Box<dyn Error + 'static, Global>

Note that the lifetime 'a isn't connected to &str so you can turn any &str into a boxed dyn Error with any trait object lifetime -- including 'static.

So the implementation is more general that way, though granted, not in an amazing way as inference will generally figure out to go through 'static and utilize covariance if it needs to.

Hard to say how conscious the design decisions between this form and other cases that just go for 'static are.


Cases like these where multiple impls on the same type and in the same file differ are generally incidental/accidental. Because of the inference implications I'm not sure if T-libs-api would merge a PR to relax the tighter impl(s) to match the looser impl(s), but personally I think that if someone cares enough, a PR would be warranted to figure out if the breakage is too much to justify just for the impl consistency. And if it isn't going to be changed, a comment indicating why they're different and pointing at the decision around breakage would be a good idea to save the next person who looks some time.

This kind of simple change is a great one to use to get initially familiar with the rustc build system enough to be able to check std changes locally, if you're interested in making code contributions to std. (And if you're not, that's perfectly fine too![1] If you want to assist but don't want to bother with making source changes yourself, actionable issues of "here's an oddity which should ideally get a fix or comment to say why" are generally appreciated and can be used to help onramp other devs who do want to try out code contributions.)

  1. There's a fine line between welcoming contributions and "fix it yourself then" that I never quite know how to walk. ↩︎


That makes sense, thank you.

Also, there's one more thing that's bugging me out in std::error::Error source. I'm talking about:

// This is required to make `impl From<&str> for Box<dyn Error>` and `impl<E> From<E> for Box<dyn Error>` not overlap.
#[stable(feature = "rust1", since = "1.0.0")]
impl !crate::error::Error for &str {}

It seems that std::error::Error isn't an auto-trait, so it shouldn't be possible to opt-out of it. Also, I can't find any actual Error implementations for &str. Is it there just in case (e.g. to make sure nobody ever implements it)?

That's fair point! I'll dive into std contribution (I've never done them before).

This is std making use of an unstable feature: (general) negative trait implementations. It's a promise that implementing Error for &str will be considered a breaking change. [1] You can't have both a negative impl and a normal impl.

Why does the feature exist? Well, it's too fragile to have negative trait bounds like impl<T> Trait for T where T: !Error mean "create an implementation of Trait for all T that happen not to implement Error", because now you've made it a breaking change for anyone else to implement Error for their types if they didn't do so originally -- the implementation for Trait would go away, and someone might have been relying on it existing. Being unable to comfortably add implementations of traits for one's own types due to someone else's implementation is not a desirable quality in the ecosystem. [2]

But if instead it means "the type has explicitly promised not to do this", then everyone is in agreement that adding the implementation is a breaking change. Then it should be okay for everyone to rely on it.

Here's the negative_impls tracking issue, and you can read more in the linked HackMD. My explanation above was a bit back-of-napkin; as the links describe

  • Fixing a soundness hole was part of the initial motivation (and a breaking change)
  • Allowing coherence to recognize negative impls is another level above just having them
    • And that's what's going on in this particular case: coherence recognizes that the two blanket implementations mentioned in the comment don't overlap
    • It's a separate feature -- with_negative_coherence -- but I don't think it has its own tracking issue
  • Allowing negative where clauses is another level above that

Note that coherence can use negative reasoning to some extent today within a single crate. The implementation in question was necessary as part of the move to core because with that move, Box and Error become types in separate crates.

Perhaps some corrections or more details incoming from @CAD97 shortly.... :slight_smile:

  1. which for std means "this will never happen"... unless we get a Rust 2.0 ↩︎

  2. Negative reasoning in coherence makes the ecosystem too brittle in general. ↩︎

This relies on an unstable feature to provide a promise of the lack of a trait implementation. If this weren't present, the coherence checker would conservatively assume that &str might implement Error in the future, and thus that the two implementations would overlap.

But actually, it seems that this isn't necessary with current Rust (trying similar impls in the playground works out fine), but it was at some point in the past. Or perhaps it's still needed as an artifact of the coherence split between std/alloc/core; downstream crates are always conservative about upstream adding new trait impls (when such a negative impl promise doesn't exist).

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.