Do not understand lifetimes in structs

I finished Chapter 10 in the book (Generic Types, Traits, and Lifetimes). I still do not understand how lifetimes works with structs. I tried to find, if somebody already asked about this on this forum. And it is. But the more I read, the less I understood.

In the book there only one example (apart from impl examples), and it looks like this:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

This annotation means an instance of ImportantExcerpt can’t outlive the reference it holds in its part field.

Okay. I'm understand how syntax looks like. But what it's possible to describe with it, apart this case? And how to read such syntax, when it's describing something, that is not this case? And what problem is being solved at all? Usage of this struct in example is straightforward:

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

When I thought about this, I realized that I'm also not sure about something more basic about structs, outside of theme with lifetimes. Is the following statement correct?

It is possible to declare and initialize struct variable separately, but when initializing such variable, it's necessary to initialize all its fields right away. It's impossible to delay initializing of some fields.

Consider this example:

#[derive(Debug)]
struct HasStrSlice<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let i = HasStrSlice {
        part: &novel,
    };
    drop(novel);
    println!("{:?}", i);
}
error[E0505]: cannot move out of `novel` because it is borrowed
  --> src/main.rs:11:10
   |
9  |         part: &novel,
   |               ------ borrow of `novel` occurs here
10 |     };
11 |     drop(novel);
   |          ^^^^^ move out of `novel` occurs here
12 |     println!("{:?}", i);
   |                      - borrow later used here

playground

This example fails to compile because the struct contains a reference into novel, but is used after the novel is destroyed. Since references are only borrows into data stored somewhere else, a struct with such a reference cannot be used after that somewhere else is destroyed.

This is particularly common when writing constructors, since the new value cannot point into local variables defined in the constructor.

impl<'a> HasStrSlice<'a> {
    pub fn new() -> HasStrSlice<'a> {
        let novel = String::from("Call me Ishmael. Some years ago...");
        HasStrSlice {
            part: &novel,
        }
    }
}
error[E0515]: cannot return value referencing local variable `novel`
  --> src/lib.rs:8:9
   |
8  | /         HasStrSlice {
9  | |             part: &novel,
   | |                   ------ `novel` is borrowed here
10 | |         }
   | |_________^ returns a value referencing data owned by the current function

playground

One common point of confusion is that this compiles:

impl<'a> HasStrSlice<'a> {
    pub fn new() -> HasStrSlice<'a> {
        HasStrSlice {
            part: "Call me Ishmael. Some years ago...",
        }
    }
}
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.64s

playground

This is because a hard-coded string constant is actually shorthand for declaring an immutable global variable containing the string data and storing a reference to the global. Since the global lives forever and is not destroyed at the end of new, it is ok to return the struct.

Of course this does not work once you try to use a value that was not hard-coded at compile time.

Yes.

7 Likes

Your example is similar to one from the book. Lifetime annotation in HasStrSlice struct description means that an instance of HasStrSlice can’t outlive the reference it holds in its part field. You intentionally broke this rule in your example, and it didn't compile.

But is it even possible to describe such struct, that can outlive its fields with references? If yes, then how? If no, then why all this? In functions lifetimes needed to describe relationship between input lifetimes and output lifetimes. Do we need lifetimes in structs to describe relationship between different lifetimes? If yes, then can you or somebody to give an example of this with usage, please?

Not using references, but you can use String to take ownership of the string.

Yes, but very rarely. You can find a good example here.

1 Like

Whoa. Thanks. It's hour and a half, but I hope it will resolve my confusion about lifetimes. I will watch it tomorrow. If I won't have more questions, I will mark this as a solution.

1 Like

How about a simple analogy:

  1. You have a phone number. Which you might tell me.

  2. I have a contacts list in my phone so I enter it there so that I can easily call you later.

  3. When I select you number in my contacts I get a call to you. Everything works nicely.

So, your phone number is a reference to you. It's analogous to a reference in Rust. My contacts list contains that reference to you by phone. It's analogous to a Struct in Rust.

  1. You close your phone account. You have moved, or god forbid died.

  2. Later I select your, now old number. There is no call. Panic! The system has failed.

In this situation you closing your phone account is like a variable being referenced in Rust to a variable that has now be dropped. If Rust allowed that dead reference to be used the program would fail/crash. As it would in C or C++ and other language. This is not good.

Note: Conversely I could delete by contacts list. That's likely OK. I did not want to call you anyway and you don't care if I no longer have your number, your phone still works. This situation is analogous to a Struct in Rust being dropped when any references it contains still exist some place. That is OK.

What to do about the out of date phone number problem? Normally you might notify me of a phone number change and I would update my contacts list. But what if you are dead? I have no way to know. A sad failed call could result.

One solution, which we don't use in phone contact lists, would be to have an expiration date with each phone number. If knew ahead of time when your phone number would go off line I would then know not to call you after that time. Effectively my contacts list is no longer correct and valid at that time.

These expiration times phone numbers are analogous to Rust's lifetime annotation. They are a statement of when a reference will become unusable, and hence the whole Struct becomes invalid. Not in actual times of course, but in terms of variable lifetimes as they go out of scope, get dropped, etc.

We can extend this analogy to the situation when I have many phone numbers in my contacts list. Each with an expiry date. My contacts list become out of date, invalid, when any one of those expiration dates is passed.

This situation is analogous to a Rust struct containing many references each with a different expiry time, and hence use of many different lifetime annotations.

This analogy also works for functions. Lifetime annotations on function parameters are like the expiry dates I might attach to your phone number if I gave it to someone else.

3 Likes

Oh, that's nothing. Just wait until you get hooked to Jon's content and you decide to sit through his 5+ hour videos. You've never really understood how uncomfortable your chair is until you've done that.

But you will learn a lot about Rust.

2 Likes

That's detailed explanation. Thanks, but I'm already understand dangle reference problem. Sorry. My problem, that I don't understand, what else possible to describe with such syntax.

We can extend this analogy to the situation when I have many phone numbers in my contacts list. Each with an expiry date. My contacts list become out of date, invalid, when any one of those expiration dates is passed.

That's similar. Whole struct must be freed before lifetime of any of its references will end. We still can have only one lifetime parameter, which would represent shortest lifetime of all references. And I cannot imagine it be other way, with my current knowledge. If struct is valid, then each of its fields is valid. But existence itself of this syntax hints, that it could be other way. alice wrote, that there could be more lifetime parameters, but I'm still going to watch video alice suggested.

While I'm reading the book and understand everything, I have no urge to lookout for external material. Of course I can just think, that I understand, but I won't be ready to be distracted from the book without proper suspicion.

Having multiple lifetime parameters lets you keep track of which parts of the struct are tied to different external objects. This is most useful when you want to eventually decompose the struct into its component parts. For example:

struct Builder<'init,'run> {
    config: &'init InitializationData,
    data: &'run RuntimeData
}

impl<'init,'run> Builder<'init, 'run> {
    pub fn start(self)->Runtime<'run> {
        Runtime::new(self.data)
    }
}

While the Builder is alive, both the InitializationData and RuntimeData must be kept alive. Once start is called, though, all references to the InitializationData are gone and it can be dropped. The compiler will still enforce, though, that the RuntimeData needs to be kept alive until the Runtime itself is destroyed.

This works even if the reference given to Runtime::new is only to a piece of the original RuntimeData— the existence of any object tagged with the 'run lifetime will do that. The compiler then enforces that you can’t borrow from a &'run reference without also tagging your type as 'run, thus preventing use-after-free bugs while still allowing complicated reference manipulations.

3 Likes

Continuing off this example, the key point is: using a single lifetime parameter would unnecessarily restrict usage of the struct.

Suspect this may be part of the OP's question: Some cases require multiple lifetime parameters to express the non-worst-case linkage of all reference fields in a struct to the shortest lifetime.

In the example above, replacing 'init and 'run with a single lifetime parameter 'a could unnecessarily restrict the inputs the caller can provide. This is the same idea as a function with many lifetime parameters, but with the possibility of using the struct lifetimes for future function calls on the struct (start(..) function in the example)

2 Likes

I think I understand. In short, if we use multiple lifetime parameters, before destruction of struct we can copy specific reference from it, and lifetime of this reference won't be shrinked to shortest lifetime of all other references of struct, right?

1 Like

Yes, this is exactly what multiple lifetime markers allow you to do.

2 Likes

Thanks. Now I got it. Still going to watch video, just in case. Then I'll mark this thread as solved.

Worse:

  1. Later I select your now old number. The call goes through. I tell the other person some private information intended for you. Oops! It wasn't you. Security breach.

Or

  1. The call goes through. I ask the other person to pick up my takeout order for me. Oops! It wasn't you. I go hungry.

Or

  1. The call does not go through, but I don't realize it. I tell the dead line some information. Oops! Later still you and I are having a conversation in which I assume you know the thing I told "you" earlier. We are all confusion.

Etc. This is why panicking is a best case scenario for UB. If the code panics or crashes it is immediately obvious that something has gone wrong.

2 Likes

It might also be worth pointing out that the original example with the single lifetime 'a is equivalent to what Rust automatically elides in if no lifetime parameter is given. Thus it's just a way of showing that the lifetime exists, but offers no additional control over it (besides getting to give it a name).

But in reading some of the other responses I now worry that I've been thinking of lifetimes of structs in the reverse sense - that fields can't outlive their struct, as opposed to a struct can't outlive its fields... Or is that just semantics? Or is there an explanation of operation at the memory level that defines which controls which?

As a final note, the concept that really threw me was that 'static didn't mean something needed to last forever, just that it could. And that it didn't automatically confer 'static demands on anything containing it.

Indeed. This analogy goes quite a long way.

Even to describing shared access to some degree. I we both have a persons phone number we cannot call them at the same time. If we could the conversation would likely be gibberish.

I was dreaming up a more exciting analogy. Where references are like time bombs set to explode at the slightest touch/vibration after some timeout.

They might all be labeled according to their timeout value, a', b', c' etc.

A Struct is then like a truck full of such time bombs, all ticking away down to their different timeouts.

You, as the driver of that truck want to be out of there and far away before anyone of them reaches it's timeout.

:slight_smile:

1 Like

Regarding what thing outlives what thing, a detail is that 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.

As for 'static, it has to do with whether you are looking at the owner or borrower. The owner must live forever because it must exist inside the entire region, but the borrower has no such requirement.

Note that whenever a lifetime appears on a type in any way, that type is in the borrower position.

2 Likes

There is lifetime elision on structs?

No, not in structs.

1 Like