Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When is it useful to define multiple lifetimes in a struct?

Tags:

rust

lifetime

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)?

like image 896
Kai Avatar asked Apr 25 '15 05:04

Kai


2 Answers

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.

like image 162
Kai Avatar answered Nov 18 '22 20:11

Kai


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.

like image 35
Xiao-Feng Li Avatar answered Nov 18 '22 18:11

Xiao-Feng Li