54

In Rust, when we want a struct to contain references, we typically define their lifetimes as such:

struct Foo<'a> {
    x: &'a i32,
    y: &'a i32,
}

But it's also possible to define multiple lifetimes for different references in the same struct:

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

When is it ever useful to do this? Can someone provide some example code that doesn't compile when both lifetimes are 'a but does compile when the lifetimes are 'a and 'b (or vice versa)?

Kai
  • 1,912
  • 1
  • 17
  • 19

3 Answers3

29

After staying up way too late, I was able to come up with an example case where the lifetimes matter. Here is the code:

static ZERO: i32 = 0;

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

fn get_x_or_zero_ref<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {
    if *x > *y {
        return x
    } else {
        return &ZERO
    }
}

fn main() {
    let x = 1;
    let v;
    {
        let y = 2;
        let f = Foo { x: &x, y: &y };
        v = get_x_or_zero_ref(&f.x, &f.y);
    }
    println!("{}", *v);
}

If you were to change the definition of Foo to this:

struct Foo<'a> {
    x: &'a i32,
    y: &'a i32,
}

Then the code won't compile.

Basically, if you want to use the fields of the struct on any function that requires it's parameters to have different lifetimes, then the fields of the struct must have different lifetimes as well.

Kai
  • 1,912
  • 1
  • 17
  • 19
  • 8
    Hahahaha! I was writing more or less the exact same thing, then had a power failure 15-ish minutes ago. I was *just* about to post it. Yes, about the only case I can think of is when you want to be able to take an aggregate value and split off parts of it after using it, without losing lifetime information. Think of building up a bundle of values (which might involve lifetimes), using it, then recovering the original values afterwards. – DK. Apr 25 '15 at 07:13
  • 3
    The 'b in get_x_or_zero_ref can of course be omitted since it's implied by the default lifetime elision rules. – bluss Apr 26 '15 at 13:11
  • 4
    It doesn't make sense to say that a function "requires" its parameters to have different lifetimes. The purpose of lifetime parameters is to prevent the function or struct from *unifying* those parameters into a single (inferred) lifetime, so the borrow checker can distinguish between them – trent Sep 17 '16 at 22:26
9

Here is another simple example where the struct definition has to use two lifetimes in order to operate as expected. It does not split the aggregate into fields of different lifetimes, but nests the struct with another struct.

struct X<'a>(&'a i32);

struct Y<'a, 'b>(&'a X<'b>);

fn main() {
    let z = 100;
    //taking the inner field out of a temporary
    let z1 = ((Y(&X(&z))).0).0;  
    assert!(*z1 == z);
}

The struct Y has two lifetime parameters, one for its contained field &X, and one for X's contained field &z.

In the operation ((Y(&X(&z))).0).0, X(&z) is created as a temporary and is borrowed. Its lifetime is only in the scope of this operation, expiring at the statement end. But since X(&z)'s lifetime is different from the its contained field &z, the operation is fine to return &z, whose value can be accessed later in the function.

If using single lifetime for Y struct. This operation won't work, because the lifetime of &z is the same as its containing struct X(&z), expiring at the statement end; therefore the returned &z is no longer valid to be accessed afterwards.

See code in the playground.

Xiao-Feng Li
  • 600
  • 6
  • 11
  • The additional lifetime to Y can be removed if the expression `X(&z)` is lifted into its own variable. i.e. `let x = X(&z)`. https://play.rust-lang.org/?version=nightly&mode=debug&edition=2015&gist=e5defc06e21d487986cba4d2969b132d Is there another way to force the need for additional lifetime parameters? I'm currently trying to understand why functions might require >1 lifetime parameter. – Steven Shaw Jan 25 '20 at 04:05
  • @StevenShaw Yes. A separate variable x will lift X(&z) to the same scope level as z, instead of a temporary within z's constructor. On the other hand, the case in my answer is not a game of concepts, but happened in my actual project. I just reduced it into the given code. For functions, it is even more common to have more than one lifetime parameter. E.g., you have two input borrows, but the return value's lifetime only relies on one of the inputs lifetimes. – Xiao-Feng Li Jan 26 '20 at 04:03
  • Thanks, I thought it might be that I'd only see it in a wider context. I've tried hard to come up with a small example that requires multiple lifetime parameters on a function. For example, the accepted answer can simply have the second parameter to the function removed. It can even have the second parameter to the struct removed if you also remove the unnecessary scope in `main`. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f8c63e5f51df08323749230931883bf2 I've tucked away your nice phrase "game of concepts" and added your book to my wish list. – Steven Shaw Jan 26 '20 at 22:07
  • @StevenShaw Being able to remove the lifetime parameter of the second input (while keeping the first one) already means they have two different lifetime arguments. It is just that one is elided according to "lifetime elision" rule. Secondly, the inner scope for `v` in `main()` in the accepted answer can be a function call (or call chain), hence cannot be simply removed. – Xiao-Feng Li Jan 27 '20 at 18:07
  • Got it. My deletion does rely on lifetime elision (all variables have lifetime tracking in Rust if I'm not mistaken). I'm looking for an example where it's necessary to annotate multiple lifetimes on a function (where elision does not work). – Steven Shaw Jan 27 '20 at 22:38
  • @StevenShaw Lifetime elision does not work when the compiler cannot infer its lifetime. A simple example is to let the function to return a struct that has two different lifetimes such as Foo in the accepted answer. See https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=31247245d3472547571d99dd02103737 . – Xiao-Feng Li Jan 28 '20 at 02:07
  • Thanks for your help and perseverance! So, functions require multiple lifetimes when structs require multiple lifetimes. This brings us back to this very SO question! However, I find the answers here pretty contrived as I can remove the lifetimes from the structs without any trouble. For instance, I change your lastest playground to have a single lifetime parameter (and also the function). https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=237c2fecbde105bd65ec563b2b3beee7. Again, I a imagine that in a larger project, multiple lifetimes on structs must become necessary. – Steven Shaw Jan 28 '20 at 23:22
9

I want to re-answer my question here since it's still showing up high in search results and I feel I can explain better. Consider this code:

Rust Playground

struct Foo<'a> {
    x: &'a i32,
    y: &'a i32,
}

fn main() {
    let x = 1;
    let v;
    {
        let y = 2;
        let f = Foo { x: &x, y: &y };
        v = f.x;
    }
    println!("{}", *v);
}

And the error:

error[E0597]: `y` does not live long enough
--> src/main.rs:11:33
|
11 |         let f = Foo { x: &x, y: &y };
|                                 ^^ borrowed value does not live long enough
12 |         v = f.x;
13 |     }
|     - `y` dropped here while still borrowed
14 |     println!("{}", *v);
|                    -- borrow later used here

What's going on here?

  1. The lifetime of f.x has the requirement of being at least large enough to encompass the scope of x up until the println! statement (since it's initialized with &x and then assigned to v).
  2. The definition of Foo specifies that both f.x and f.y use the same generic lifetime 'a, so the lifetime of f.y must be at least as large as f.x.
  3. But, that can't work, because we assign &y to f.y, and y goes out of scope before the println!. Error!

The solution here is to allow Foo to use separate lifetimes for f.x and f.y, which we do using multiple generic lifetime parameters:

Rust Playground

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

Now the lifetimes of f.x and f.y aren't tied together. The compiler will still use a lifetime that's valid until the println! statement for f.x. But there's no longer a requirement that f.y uses the same lifetime, so the compiler is free to choose a smaller lifetime for f.y, such as one that is valid only for the scope of y.

Kai
  • 1,912
  • 1
  • 17
  • 19