Kai
Kai

Reputation: 2832

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

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

Upvotes: 78

Views: 12285

Answers (4)

Laplace
Laplace

Reputation: 33

Multiple lifetime parameters because input is nested lifetime parameter

struct Container<'a> {
    data: &'a str,
}

struct Processor<'a, 'b> {
    container: &'a Container<'b>,
}

fn process<'a, 'b>(processor: &'a Processor<'a, 'b>) -> &'b str
where
    'a: 'b,
{
    processor.container.data
}

fn main() {
    let data = "Hello, world!";
    let container = Container { data: &data };
    let processor = Processor { container: &container };
    let result = process(&processor);
    println!("Result: {}", result);
}

Return type with shorter lifetime

I think it is worth mentioning that lifetime parameters in Rust are closely tied to the scopes in which they are defined. By assigning appropriate lifetime parameters and adding constraints, we ensure that references have valid lifetimes and avoid issues with dangling references or references outliving their intended scopes.

Each item or reference has its own lifetime, which determines how long it is valid and can be used. When we specify different lifetime parameters for different items, we are giving each of them a "ticket" that represents the duration for which the reference is valid.

Consider the following code structure:

{
    // Scope of item A
    {
        // Scope of item B
    }
}

Item B can only live within its scope, while item A can live in both the scope of item A and the scope of item B.

In the code above, the get_x_or_zero_ref function has two input references with different lifetimes: 'a for x and 'b for y. By giving each of them a lifetime parameter, we are essentially giving them their own "tickets" that specify how long the reference is valid to avoid dangling references.

Now, when we specify the return type of the get_x_or_zero_ref function as 'a i32, we are saying that the lifetime parameter for the return type should be the same as the lifetime parameter for the first input reference, which is 'a. This ensures that the returned reference will not outlive the scope of x.

But what if we want to specify the lifetime parameter for the return type as 'b i32 or -> &'b i32 or that has a shorter lifetime instead?

In this case, we are trying to assign the lifetime parameter of 'b to the return type, which corresponds to the scope of item B. However, there is a potential problem if we don't handle it correctly. The lifetime of item B will end within its scope, but the lifetime of item A, by default, has a greater scope than that of item B.

This is problematic because we specified the return type to have the lifetime 'b, which corresponds to item B's scope. However, the lifetime of item A, represented by 'a, outlives the needs of the return type. We are essentially giving the return type a ticket that is valid for a shorter duration than the actual lifetime of item A.

To resolve this issue, we need to add a constraint to the 'a lifetime of item A, indicating that the tickets associated with 'a are only valid within the scope of item B. By adding this constraint, we ensure that the returned reference does not outlive its intended scope and maintains the correct relationships between lifetimes.

So if we want to return an item that has a shorter lifetime, we need to determine if there is another input with a greater lifetime. Then we add a constraint on the input with a greater lifetime so its tickets are only valid within the shorter lifetime and don't outlive another.

Here is the code if we want to write the return type to have a shorter lifetime.

static ZERO: i32 = 0;

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

// returning lifetime 'b
fn get_x_or_zero_ref<'a, 'b>(x: &'a i32, y: &'b i32) -> &'b i32
where
    // Adding constrain so a will not outlive b
    'a: 'b,
{
    if *x > *y {
        x
    } else {
        &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);
    }
}

Upvotes: 2

Kai
Kai

Reputation: 2832

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.

Upvotes: 24

Xiao-Feng Li
Xiao-Feng Li

Reputation: 770

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.

Upvotes: 13

Kai
Kai

Reputation: 2832

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.

Upvotes: 47

Related Questions