I am not good at English. Sorry if there are any funny expressions.
My two types have almost identical formats in rustdoc. However, when I used them in the same way, only one caused compile error. On my investigation, the cause seems to lie in the private fields (that aren't output in rustdoc).
- Question 1
This was surprising based on my experience with other languages. I believed that with just by API documentation, I could determine whether the code passed compilation. But Rust is not? Or is it just that the code below is so unnatural it's not even worth considering? (Especially MyType1 using PhantomData in odd way...)
- Question 2
I researched the specification around lifetime elision, but couldn't find the relevant section for the problematic lifetime inference (though it might just be my lack of English skill...)
Code
In the following, rustdoc outputs of MyType1 and MyType2 is almost identical (There are minor differences around Sync, etc). But MyType2 seems to have inferred T: 'a because it has a field of type &'a T. (I was doing this manually every time.)
error[E0597]: `a` does not live long enough
--> src\lib.rs:6:33
|
4 | let a = 42;
| - binding `a` declared here
5 | test1(MyType1::new(Box::new(&a)));
6 | test2(MyType2::new(Box::new(&a)));
| ----------------------------^^---
| | |
| | borrowed value does not live long enough
| argument requires that `a` is borrowed for `'static`
7 | }
| - `a` dropped here while still borrowed
Here's the section on implied bounds. And here's the RFC about being able to elide the bound on struct definitions.[1] One difference from the RFC, apparently still undocumented, is that 'static bounds are not inferred.
Note that if you have a MyType1<'a, T> where T: 'a does not hold, the type is still not valid for all of 'a. For example, if T is not : 'static, then MyType1<'static, T> is not : 'static either. So allowing the lifetime to be 'static in that case only gets you so far.
Unfortunately there's a challenge for Rustdoc when it comes to these bounds: Whether or not the bound was inferred or explicit can matter semantically. But it would be nice if there was some indication for inferred bounds.
yes, that's the real difference between the two, it is an implied lifetime bound:
and yes, it's a leaky abstraction even when it's a private field, but it's a trade-off for ergonomics, otherwise, you are forced to add T: 'a whenever a generic reference is used.
I think it is useful if rustdoc could generate a section of implied bounds for types, but it isn't doing it currently.
It would be nice to see improvement here -- I would like to see all these documented somehow. But it will be tricky to do so in a way that doesn't overwhelm the reader. Even when the properties are documented, you have to know what to look for as a reader (e.g. auto-traits).
Another challenge is hinted at in the 1-ZST issue I linked: sometimes attributes like repr(transparent) or other properties (like being a 1-ZST) are meant to be public-facing guarantees, and sometimes they are meant to be internal implementation details. How things are handled (both by Rustdoc and by the compiler) is heuristic, based on things like field privacy and non_exhaustive. But the system isn't perfect:
The heuristics change over time, as that issue aims to do
A heuristic cannot be correct for every use case
So there will always be some cases where the crate author and/or the crate user will have to fall back to human-written documentation to know what is or isn't guaranteed. And also there will always the occasional situation where a crate author has unwittingly broken compatibility by changing what they thought was an implementation detail.
I remember encountering the Drop issue before. Oh indeed, it's a similar problem...
There are others too... This seems like a good learning opportunity. Thank you so much.