Help understanding multiple lifetimes in structs

I have problems with understanding the behavior and availability of structs with multiple lifetime parameters. Consider the following:

struct  My<'a,'b> {
    first: &'a String,
    second: &'b String
}

fn main() {
    let my;
    let first = "first".to_string();
    {
        let second = "second".to_string();
        my = My{
            first: &first,
            second: &second
        }
    }
    println!("{}", my.first)
}

The error message says that

   |
13 |             second: &second
   |                     ^^^^^^^ borrowed value does not live long enough
14 |         }
15 |     }
   |     - `second` dropped here while still borrowed
16 |     println!("{}", my.first)
   |                    -------- borrow later used here

First, I do not access the .second element of the struct. So, I do not see the problem.

Second, the struct has two life time parameters. I assume that compiler tracks the fields of struct seperately. For example the following compiles fine:

struct  Own {
    first:  String,
    second: String
}

fn main() {
    let my;
    let first = "first".to_string();
    {
        let second = "second".to_string();
        my = Own{
            first: first,
            second: second
        }
    }
    std::mem::drop(my.second);
    println!("{}", my.first)
}

Which means that even though, .second of the struct is dropped that does not invalidate the whole struct. I can still access the non-dropped elements.

Why doesn't the same the same work for structs with references?

The struct has two independent lifetime parameters. Just like a struct with two type parameters are independent of each other, I would expect that these two lifetimes are independent as well. But the error message suggest that in the case of lifetimes these are not independent. The resultant struct does not have two lifetime parameters but only one that is the smaller of the two.

If the validity of struct containing two references limited to the lifetime of reference with the smallest lifetime, then my question is what is the difference between

struct My1<'a,'b>{
 f: &'a X,
 s: &'b Y,
}

and

struct My2<'a>{
 f: &'a X,
 s: &'a Y
}

I would expect that structs with multiple lifetime parameters to behave similar to functions with multiple lifetime parameters. Consider these two functions

fn fun_single<'a>(x:&'a str, y: &'a str) -> &'a str {
    if x.len() <= y.len() {&x[0..1]} else {&y[0..1]}
}

fn fun_double<'a,'b>(x: &'a str, y:&'b str) -> &'a str {
    &x[0..1]
}

fn main() {
    let first = "first".to_string();
    let second = "second".to_string();
    
    let ref_first = &first;
    let ref_second = &second;
    
    let result_ref = fun_single(ref_first, ref_second);
    
    std::mem::drop(second);
    
    println!("{result_ref}")
}

In this version we get the result from a function with single life time parameter. Compiler thinks that two function parameters are related so it picks the smallest lifetime for the reference we return from the function. So it does not compile this version.

But if we just replace the line

let result_ref = fun_single(ref_first, ref_second);

with

let result_ref = fun_double(ref_first, ref_second);

the compiler sees that two lifetimes are independent so even when you drop second result_ref is still valid, the lifetime of the return reference is not the smallest but independent from second parameter and it compiles.

I would expect that structs with multiple lifetimes and functions with multiple lifetimes to behave similarly. But they don't.

What am I missing here?

I asked the same question at Rust multiple lifetimes in structs - Stack Overflow hoping to get better answers here.

1 Like

Yes. But note that struct, itself, also have a lifetime.

It doesn't work like this. Lifetime of any field of the struct must not be shorter than lifetime of struct itself.

This becomes important when you pass struct into some function. And since lifetimes are there mostly to help with that usecase language developers decided not to invent special rule for structs which are passed anywhere.

Most likely the answer is: “it wasn't considered important enough to implement”. The situation where you “pull” something from the struct and leave it half-empty before eventual full destruction happens common enough that special support was added for it.

Situation with lifetimes is different: usually these are only used when functions are called and partial borrows are not a thing (although there are some discussions about them).

No. It actually have two parameters, but three lifetimes. You can see that lifetimes are tracked individually on the following example:

struct  My<'a,'b> {
    first: &'a str,
    second: &'b str
}

fn get_first<'a, 'b>(my: My<'a, 'b>) -> &'a str {
    my.first
}

fn get_second<'a, 'b>(my: My<'a, 'b>) -> &'b str {
    my.second
}

fn foo() -> &'static str {
    let first = "first";
    let second = "second".to_string();
    let my = My {
        first: first,
        second: second.as_str()
    };
    get_first(my)
}
1 Like

You point that the struct itself has a lifetime is helpful.
Just like a Hashmap<V,K> has two type parameters the instance will have type neither V nor K but its own type (lets say) Hashmap<i32,i32>.

So, then my question becomes what is the relationship between lifetime parameters of a lifetime generic struct and the lifetime of the instance of that struct.

Your answer suggests that for any struct S<'a,'b,...,'n> the lifetime of the instance is the smallest of a..n. Is that correct?

But if that is correct the following two have no difference

struct My1<'a,'b>{
 f: &'a X,
 s: &'b Y,
}

and

struct My2<'a>{
 f: &'a X,
 s: &'a Y
}

Either 'a is smaller or 'b. if it is 'a, by the suggestion that the lifetime of the struct is the smallest one first struct is exactly like the second struct. If it is 'b, they are just notational variants.

In simple cases you can usually replace multiple lifetimes on a struct with a single one, yes. It depends on what you're trying to do with the lifetimes, and the variance they end up with.

In more complicated scenarios you may not be able to do that without causing problems. Generally though you should avoid the complicated situations inside a struct anyway.

Almost. Lifetime of struct data type is equal to intersections of all these lifetimes (not that liefetimes only have partial order, not full order).

Variable can live for even shorter time than lifetime of one of its arguments. Look at the example which I have already shown: I am putting reference to two str s in my struct. One is &'static str (it references literal with static lifetime), other references str created from a temporary string (thus cannot outlive function foo). That's why the call to get_first succeeds (you get &'static str back from My struct), but call to get_second would fail.

It would also fail if you would try to use one lifetime. The error message would look a bit silly, on the first glance (you are returning first yet error message points to second), but logical: you said that both fields should reference objects with the same filetimes, but second references local object thus lifetime of first target is reduced to match…

I don't know what you mean by “notational variants”. Just compare these two examples, it exactly deals with the situation where 'b have shorter lifetime. One version compiles (correctly), one doesn't (also correctly).

2 Likes

You can read the rules for nominal types and others here. Or translated,

S<'a, ... 'n>: 'r if 'a: 'r, ..., 'n: 'r.

Or translated even more, "S<...> is a valid type for the region where all of its lifetimes are also valid."

The distinction between "smallest of a..n" and "intersection of a..n" is relevant as inferred lifetimes are flow-sensitive, e.g. they may be discontiguous within a function body (thanks to NLL).

2 Likes

Thank you so much for the link!

Is there any other place which describes rust's type system?

I would appreciate two kinds of resources a lot.

  1. It completely describes the type system so that if I have any problems with types I can use it as a reliable references.
  2. If possible, it is not for compiler implementation but describes the type system in abstract.

But I would settle for less, if you can point me towards the right direction.

Unfortunately there is no spec and little normative documentation on the language itself (in contrast with the standard library), and the documentation that does exist is spread out all over the place, so you might have to search and play around a bit depending on the question at hand and how nuanced you want to get.

The main resources I can point you to are

  • The reference is generally your first stop
  • Though aimed at understanding unsafe, the nomicon fills some gaps of the reference and has alternative presentations of some of the same topics (like variance)
  • The accepted RFCs have a lot of information
    • But are still technically non-normative (the implementation can deviate from the RFCs and RFCs can be overridden later, and the RFC docs are not kept up-to-date) and you have to at least check the tracking issue to see if a given RFC is even implemented yet
    • There are also a lot of them and no sure-fire way to guess which are relevant to your question
  • Ask in this forum
  • Search on GitHub for issues related to your question
    • In UCG for questions around undefined behavior and unsafe specifically
  • On rare occasion I've found more information in the rustc dev guide

"Rust's type system" is a broad topic, but if but if you can narrow it down any maybe I can highlight what I consider the relevant RFCs to be.

Here's an earlier post where I linked to a lot of RFCs and other resources for learning topics beyond the basics.

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.