5 ms·
struct Sheep<'c> { age: &'c i32, } I don't get why this is needed at all and isn't implicit? What are other possible values for a lifetime in a struct?
by jhasse 11y ago
struct Sheep<'c> {
age: &'c i32,
}
I don't get why this is needed at all and isn't implicit? What are other possible values for a lifetime in a struct?
- Ygg2 11y agoIn theory, Rust definitely could use extra lifetime elision. Question is how often is such elision useful. You could have struct Example<'a, 'b>{ ref1: &'a i32, ref2: &'b i32}
- groovy2shoes 11y agoIn this case, how is the lifetime 'a different from the lifetime 'b?
- Manishearth 11y agoMore accurately, it allows the lifetimes to _be_ different. Think of it like generics. What's the difference between: struct Foo<T> {x: T, y: T} and struct Bar<T,U> {x: T, y: U} "How's T different from U"? That's not the right question to ask -- over here Bar lets T be different from U, whereas in Foo both x and y must have the same type. With the example above struct Example<'a, 'b>{ ref1: &'a i32, ref2: &'b i32} this means that there are two references of independent lifetimes. It could also be: struct Example<'a>{ ref1: &'a i32, ref2: &'a i32} which means that the lifetimes must match, or even struct Example<'a, 'b: 'a>{ ref1: &'a i32, ref2: &'b i32} which means that 'b is a subset of 'a. (This becomes useful if a struct stores a borrow to a thing and then a borrow of something extracted from that thing) While the first case is the most common (and theoretically could be elided), it's still often a tossup and it's clearer when you know that a struct is borrowing things.
- groovy2shoes 11y agoThank you. The article made it sound to me like struct Foo<'c>, 'c is the lifetime of the struct. Am I correct in thinking that, in reality, 'c must live as long or outlive the struct?
- dbaupp 11y agoYeah, the lifetimes on a struct are the lifetimes of things that the struct points to, not an instance of the struct itself. For an instance to be valid, everything it points to must be valid, so if `foo: Foo<'c>`, `foo` cannot be stored for longer than `'c`. E.g. this code is invalid, because the `Foo` instance would live longer than the thing it points to: let foo = { let age = 0; Foo { age: &age } }; The `age` variable goes out of scope at the } (i.e. the maximum possible lifetime for &age ends there), and hence letting the `Foo` escape from that scope would be a bad idea. (NB. this applies to the "core" lifetime'd types like `&'c T` too: that is really just a nicer way to write a parameterised type like `Ref<'c, T>`. So the rule "cannot return a reference to a local variable" is the same rule that stops a `Foo<'c>` living too long.)
- groovy2shoes 11y agoMakes perfect sense. Thank you!
- Manishearth 11y agoThe article is wrong/misguided. See my comment above :) When you say `Foo<'c>`, all you're saying is that each `Foo` instance has an associated lifetime `'c`, similar to a type parameter. When you add the `x: &'c u8` field, then you're saying that "this lifetime is the lifetime of the inner u8". > Am I correct in thinking that, in reality, 'c must live as long or outlive the struct For the specific example, yes. It's possible to have an example where 'c must be shorter lived, and cannot outlive the struct. If you had an &mut instead of & there, then 'c must live exactly as long as the struct.
- groovy2shoes 11y agoThank you. > It's possible to have an example where 'c must be shorter lived, and cannot outlive the struct. I'm interested in such an example. Could you provide one?
- Manishearth 11y ago
- tatterdemalion 11y agoAren't there subtype coercions that make this syntax useful only rarely? (something like: if 'b: 'a, then &'a and &'b will both be treated as 'b for this instance, or maybe the other way around? And with only lexical lifetimes, for two references visible in the same scope, one is necessarily a subtype of the other, no?) The only time I've ever used multiple explicit lifetimes, I was accidentally trying to trick the compiler into accepting shared mutability. The compiler was not fooled.
- Manishearth 11y agoYes, you're correct. You need this syntax when the borrow checker forces you to add this dependency, i.e. when you're stuffing one reference inside another, or swapping references, or whatever. This is why elision is so awesome, it takes care of almost all the cases where you need a "regular" lifetime specification on a function. When elision doesn't works usually there is something you need to specify. The subtyping works for callers; lifetimes get stretched and sqeezed to fit the function signature. But the function body itself won't compile when you're doing things like stuffing references into other references unless the lifetimes are right.
- whyever 11y agoI think it is intentional. Rust wants you to be explicit about lifetimes in type definitions.
- dbaupp 11y agoThere's a few other configurations of lifetimes, e.g. (not meant to be exhaustive) - only allowing storing references that are valid forever struct Sheep { age: &'static i32 } - storing multiple references, with the same lifetime struct Sheep<'a> { age: &'a i32, name: &'a str } or different ones struct Sheep<'a, 'n> { age: &'a i32, name: &'n str } (This is usually most important for mutable references (&mut), rather than shared ones (&).) - lifetimes that have a non-trivial relationship to other lifetimes/types, e.g. via trait bounds: struct Sheep<'a, N: SomeTrait<'a>> { age: &'a i32, name: N } --- There could be some elision rule that allows omitting the explicit annotation in some cases, but I think it would be weird: allowing complete elision for the equivalent thing with types (e.g. writing `struct Foo<T> { x: T }` as just `struct Foo { x: T }`) would not be great at all IMO. These types are generic over lifetimes in essentially the same way as types that are generic over types (i.e. they can be instantiated with a lifetime (resp. type) of a downstream user's choosing), and being generic over a lifetime is part of its signature/behaviour.
- wycats 11y agoAs the person who originally pushed extremely hard for all of the lifetime elision that we have today, I agree with you that this case is still too verbose. That said, the reason it wasn't included in the original elision RFC (https://github.com/rust-lang/rfcs/pull/141 https://github.com/rust-lang/rfcs/pull/141) is that the design space here is a little bit more subtle than some of the cases that did make it. In particular, I believe that it's important to know that a particular struct contains a borrowed value somewhere. So let's say you could leave off the `'c` on the Sheep struct, and I wrote a struct containing `Sheep`: struct Flock { sheep: Vec<Sheep> } Now, we have visually lost the fact that a borrowed value is stored inside the flock. Since data structures can get very recursive, this rapidly causes useful information about ownership to get lost. What I'd like to see personally is: struct Sheep<&> { age: &i32 } struct Flock<&> { sheep: Vec<Sheep> } What this would mean is that you can always see, by looking at a struct definition, whether it contains borrowed values (anywhere recursively), but without the grotty need to define and wire up lifetime names. I like the fact that this maintains the strong mental notion of ownership (the meaning of the & "operator" in Rust), but, as with normal elision, it eliminates the wiring that a good compiler should be able to do for you.
- dan00 11y agoThere're even people that don't like the automatic moving in Rust, because they can't see where something is moved. I can understand why people feel that way, but it's a bit irrational, because if automatic behaviour can't result into incorrect code, more ressource usage or higher work load, why should I care about? I have pretty much the same feeling about explicit lifetimes in this example. Again, I understand that people want the whole control, want to see every relevant information, but everything has its costs, and the relevant information in each use case isn't always the same.
- steveklabnik 11y agoWe did try making moves and copies into distinct syntaxes. let x = foo; // <- copies let x = move foo; // <- moves It was worse, or at least, was by the majority of people at the time. See http://smallcultfollowing.com/babysteps/blog/2012/10/01/moves-based-on-type/ http://smallcultfollowing.com/babysteps/blog/2012/10/01/move... for more of this ancient history.
- pcwalton 11y agoEarly Rust did attempt to elide here. The trouble is that if you left off <'c> then it wasn't obvious in those kinds of cases that Sheep is bound to a stack frame, which led to surprising errors. So lifetimes were made mandatory here.