wrs
2 months ago
My only complaint with this excellent list is that it treats "generics" and "lifetimes" as separate things. There's a reason the lifetime is inside the generic brackets. The code is generic over some lifetimes just as it can be generic over some types.
As a Rust beginner I read lifetimes backwards, thinking <'a> means I'm "declaring a lifetime" which I then use. What that actually declares is a placeholder for a lifetime the compiler will attempt to find wherever that struct or function is used, just as it would attempt to find a valid type for a type generic <T> at the points of usage.
Once I fixed that misconception everything made much more sense. Reminding myself that only the function signature matters, not the actual code, was the other thing I needed to really internalize.
The compiler messages hinder this sometimes, as when the compiler says "X doesn't live long enough" it actually means "using my limited and ever-evolving ability to infer possible lifetimes from your code, I can't find one that I can use here".
This is also (for me, anyway) a common "it's fine but it won't compile" case, where you don't have enough lifetime parameters. In other words, you're accidentally giving two things the same lifetime parameter when it's not actually necessary to require that the compiler come up with a single lifetime that works for both. The compiler error for that does not typically lead you to a solution directly.
estebank
2 months ago
If you have a good repro case, I'd appreciate a bug report. Bad diagnostics are considered bugs.
wrs
2 months ago
It's actually a classic and much-repeated case in Rust education:
fn pick_first<'a>(x: &'a str, y: &'a str) -> &'a str {
x // We only actually return x, never y
}
fn main() {
let s1 = String::from("long-lived");
let result;
{
let s2 = String::from("short-lived");
result = pick_first(&s1, &s2);
} // s2 dropped here
println!("{}", result);
}
The error here is "borrowed value [pointing to &s2] does not live long enough". Of course it does live long enough, it's just that the constraints in the function signature don't say this usage is valid.Thinking as a beginner, I think part of the problem here is the compiler is overstating its case. With experience, one learns to read this message as "borrowed value could not be proved to live as long as required by the function declaration", but that's not what it says! It asserts that the value in fact does not live long enough, which is clearly not true.
(Edit: having said this, I now realize the short version confuses beginners because of the definition of “enough”. They read it as “does not live long enough to be safe”, which the compiler is not—and cannot be—definitively saying.)
When this happens in a more complex situation (say, involving a deeper call tree and struct member lifetimes as well), you just get this same basic message, and finding the place where you've unnecessarily tied two lifetimes together can be a bit of a hunt.
My impression is that it's difficult or impossible for the compiler to "explain its reasoning" in a more complex case (I made an example at [0] [1]), which is understandable, but it does mean you always get this bare assertion "does not live long enough" and have to work through the tree of definitions yourself to find the bad constraint.
[0] https://play.rust-lang.org/?version=stable&mode=debug&editio...
[1] https://play.rust-lang.org/?version=stable&mode=debug&editio...
estebank
2 months ago
Would output along the following lines be an improvement?
error[E0597]: `buffer` does not live long enough
--> src/main.rs:63:21
|
59 | let report = {
| ------ borrow later stored here
60 | let mut buffer = Vec::new();
| ---------- binding `buffer` declared here
61 | let ctx = Context {
62 | config: &config,
| ------ this field and `buffer` are required by `Context` to have the same lifetime
63 | buffer: &mut buffer,
| ^^^^^^^^^^^ borrowed value does not live long enough
...
68 | };
| - `buffer` dropped here while still borrowed
help: consider making different fields in `Context` have independent lifetimes
|
4 | struct Context<'a> {
| ^^
5 | config: &'a Config,
| ^^
6 | buffer: &'a mut Vec<u8>,
| ^^
7 | }wrs
2 months ago
Wow, yes, that would be great in this case. What would it do for pick_first?
estebank
2 months ago
It would be similar, pointing at the other argument. Neither case would have an applicable suggestion, just point in the right direction. I don't know if at the time we emit the error for `pick_first` we're able to evaluate the function's body to see if changing the lifetimes is feasible or not.
penguin_booze
2 months ago
I'm fairly out of touch with Rust. I think generics and lifetimes are also separate in the sense that only the generics get monomorphised, while lifetimes don't. I.e., you get distinct structs Foo<u32> and Foo<i32>, depending on the (type) argument with which Foo was instantiated (just like it is in C++), but only one Bar<'a> no matter what (lifetime) argument it was "instantiated" with.
steveklabnik
2 months ago
You're slightly incorrect. Lifetimes do get "monomorphized" in the sense that you can have multiple concrete lifetimes be filled in for a given lifetime parameter (that's why they're called parameters) but also, lifetimes are fully erased far before you get to codegen monomorphization, which is what happens with generics.
vacuity
2 months ago
Lifetimes and types are different, but the part where they are generic is the same. I think of it as "who controls/decides the value of this parameter". It's a crucial part of understanding lifetimes, not just a misconception.
wrs
2 months ago
Exactly, so it seems like a category error to say (as the article does) “generics and lifetimes are tightly intertwined in Rust”. I mean, would you say “generics and types are tightly intertwined in Rust”? If anything, types and lifetimes are intertwined — by generics!
Rust tutorials introduce lifetimes and stick them into the brackets, but I don’t remember one that explains genericity in a general way first, then applies it to both types and lifetimes. Lifetimes come across as a special thing that happens to be in the same brackets. (Or maybe that was just me!)
I believe the model in the compiler isn’t quite so pure in reality (IIRC, struct lifetimes can affect type unification), but most of the time this is the easiest way to understand it.