Do not understand lifetimes in structs

Well now I'm all sorts of confused... Something like struct Thing { field: u8 } isn't elided to be 'static because that's what the field is?

As far as I know lifetimes are only for references. That field belongs to struct. If we will create struct, then there couldn't be situation, when something goes out of scope, and there is nothing to point for borrow checker. Also field would be freed, when struct would be freed, and 'static, if I understood right, is the biggest lifetime possible, which covers whole program to its very end.

Thing is 'static, but that's different from being parameterized with 'static. There's no lifetime elision in struct Thing { field: u8 } because none of the types involved have lifetime parameters to be elided.

Doesn't literally every allocated bit of memory get a lifetime? And thus wouldn't one be elided in by default? Granted a struct definition doesn't allocate anything, its constructor functions such as new() do. I just assumed a lifetime would be given to the struct that would be the same one that would be elided in to the new() function. But I guess my concept of elision and parameterization are still off...

I think I can begin to see how the struct definition could go without a lifetime now though. The scope in which the new() function is called is the lifetime that the new struct instance is given.

There's a difference between being annotated with a lifetime, i.e. &'a u32 and Foo<'a> and being valid for a lifetime, i.e. T: 'a. Only the former kind can be elided, because only the former is part of the type.

1 Like

Lifetimes are never the lifetimes of values. See my other comment.

lifetime markers never really indicate the lifetime of a value. Rather, when a value owner is borrowed, producing a borrower , the lifetime is a region that lies in between them in the sense that the owner must be valid in the entire region, and borrower may only be valid inside the region. There's no guarantee that either value is equal to the region.

Lifetimes don't come into play until you actually borrow something.

So it's just a shared concept that the 5 in {5;} doesn't outlive the scope? I.e. it has a set lifetime in the sense of "duration of its existence", but not a 'lifetime' in the keyword sense. So it's not that the scope owns the only reference to that value within itself? Very nuanced... Thanks for taking the time to help clarify! I think once I saw lifetime elision I just assumed it was turtles all the way down and happily wandered on from there.

There's no borrowing in {5;}, so lifetimes don't come into play. Lifetime elision means not writing lifetimes in places where you could write them, so it only makes sense to talk about lifetime elision if you could write down the lifetimes.

1 Like

Hopefully this is my last logical hurdle for understanding this, but... There's also no borrowing in:

impl Thing {
    fn new() -> Thing { // Make a thing }
}

But you can write lifetimes in there:

impl<'static> Thing<'static> {
    fn new<'static>() -> Thing<'static> { // Make a thing }
}

Does that not imply that lifetimes go deeper than just & references? Or does it imply that all non-& references are just 'static by default? Though would that give them lifetimes in a sense?

Though I see your point that there are no writeable lifetimes in {5;}. But wouldn't {Thing::new();} produce a 'static Thing within the scope? Maybe my problem is I'm trying to apply the concept of lifetimes to understanding ownership of scopes and I should really view them as two separate concepts?

Not really: (Playground)

   Compiling playground v0.0.1 (/playground)
error[E0262]: invalid lifetime parameter name: `'static`
 --> src/lib.rs:3:6
  |
3 | impl<'static> Thing<'static> {
  |      ^^^^^^^ 'static is a reserved lifetime name

error[E0262]: invalid lifetime parameter name: `'static`
 --> src/lib.rs:4:12
  |
4 |     fn new<'static>() -> Thing<'static> { // Make a thing
  |            ^^^^^^^ 'static is a reserved lifetime name

error[E0496]: lifetime name `'static` shadows a lifetime name that is already in scope
 --> src/lib.rs:4:12
  |
3 | impl<'static> Thing<'static> {
  |      ------- first declared here
4 |     fn new<'static>() -> Thing<'static> { // Make a thing
  |            ^^^^^^^ lifetime 'static already in scope

error[E0107]: wrong number of lifetime arguments: expected 0, found 1
 --> src/lib.rs:3:21
  |
3 | impl<'static> Thing<'static> {
  |                     ^^^^^^^ unexpected lifetime argument

error[E0107]: wrong number of lifetime arguments: expected 0, found 1
 --> src/lib.rs:4:32
  |
4 |     fn new<'static>() -> Thing<'static> { // Make a thing
  |                                ^^^^^^^ unexpected lifetime argument

error: aborting due to 5 previous errors

The things you see in angle brackets are generic parameters, which can be either lifetimes or types. Sometimes, that's defining a new name (like impl<T> or struct MyType<T>), and other times it's filling in the parameters where they're required (like impl MyType<u32>).

'static can only be used in the latter cases, and is the only lifetime name that has any meaning without being introduced in one of the earlier forms. Everything else is a mechanism to transport the lifetime specifier from the scope it was generated (that contains the owned value) to some & or &mut buried in a composite type somewhere.

1 Like

Ohhhhhh... Perhaps that makes things clearer. But, instead of 'static you could make the impl with 'a instead and not try to use the reserved lifetime name. Then doesn't that mean the elided (useless) lifetime 'a is applied into the returned new Thing within the scope?

Apparently I'm trying to untangle difference between 'ownership' and 'lifetime', as I thought the one implied the other.

Just like a function has a fixed parameter list, so too do structs: you can't arbitrarily give it a lifetime that it's not expecting. And if the struct doesn't contain something that uses that parameter (either a reference or another struct), you'll get an unused type parameter error from the compiler.

True... making an impl<'a> for Thing would mean it needed to be struct Thing<'a> to start with, thus an unused lifetime. That clears up a lot.

But, I'm still missing something... I was really expecting Rust to have a whole internal lifetime system for all things that we never see because it just seemed a natural extension of ownership. What DOES Rust do to know the 'duration not keyword' lifetime (duration of existence?) for items in a scope?

Wouldn't it need to give any value stored in memory a base lifetime tied to the scope it's in, because to define a value in memory you need to know where the value is stored - hence a reference. Also, wouldn't that allow new references made to that value in memory to take on that compiler-known duration of existence to use for the true keyword lifetime assigned to the reference?

Not of ownership, I'd say, but of borrowing. If there's no borrowing, there's no need to bother with lifetimes.

If this value is on the stack, you don't need the reference to it, you just encode its address everywhere as a constant. If it's on a heap, but used through the raw pointer, well, the raw pointer has no lifetime (ant that's one of the reasons why it is unsafe to use directly), and the code using this pointer must ensure that it is always valid, effectively encoding the "lifetimes" into the runtime.

I think I'm working my way towards formulating this so I can best state what I feel I'm missing...

When defining a variable the computer can't just set some bits, it has to know which bits it's setting. The statement { let a: u8 = 2; } doesn't just set some unknown bits to 00000010, there is a pointer to those bits within the scope of the {} and that variable (pointer) can't outlive the scope unless it is what is returned.

Thus the & reference in the following would get a known lifetime, and I'd assume it would take it from the extant pointer lifetime in the current scope:

{
let a: u8 = 2;
let b = &a;
}

That lifetime, while unnamed and inaccessible before, must be what is taken on by b, right?

Is there any possible way that thinking of ownership duration as being conceptually similar to lifetimes (though not named or accessible) would be incorrect?

Are lifetimes just the tip of the ownership iceberg that you can actually interact with in code?

Regarding

impl<'a> MyStruct<'a> {
    pub fn new(some_value: &'a str) -> Self {
        ...
    }
}

The 'a lifetime here refers to a borrow that happened somewhere else, e.g. here:

let my_string = "Hello World!".to_string();
let my_struct = MyStruct::new(&my_string);
//                            ^^^^^^^^^^ here

So, generic lifetimes can be thought of placeholders for lifetimes created in other functions. As for 'static, it's a bit of an outlier, because it's sometimes used as a placeholder when no borrow happened, for example with the type Option<MyStruct<'static>>, you can always create a None, even if no borrow happened.

1 Like

This might be where you’re having difficulty, in that example there are no pointers. Not all variables are pointers. A pointer is simplistically a number which represents an offset into memory, in the example there is only a u8 value. The location in memory of that value might be defined as an offset from the stack frame, or it might be stored directly in a CPU register, depending on how it is compiled. But those offsets and register names are not “pointers” in the normal sense.

1 Like

I just now thought, that I don't understand, in which cases we actually need restrict lifetime of references in struct by providing only one lifetime parameter when we have two or more references. If we create such struct and provide to it two references with different lifetimes, then one of them will be shrinked inside struct. Why we may need this?

You don't really ever need to only have one lifetime, since having two allows strictly more things to happen. However in some cases you don't need those extra things, and in that case having just one can simplify the code.

1 Like

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.