Rust lifetime problem

I'm learning about lifetime knowledge. I haven't made any progress for a long time.

I think it's very difficult. The difficulty is that official documents and online blogs only explain a specific case.

For example, return the longer of two strings, or explain a little about lifetime in struct.

I can't find a document that explains the specific steps of the compiler.

Of course, I'm not asking for very specific details, after all I'm not looking at compiler implementations.

However, at least there is a relatively strict document that covers all these cases as much as possible.

Many documents found on the Internet explain lifetime in a way that the author thinks he understands.

I am very confused. Programming should be strict and should not be based on self-knowledge.

I think the fundamental reason why lifetime is difficult to master is that the official documentation does not explain in detail how lifetime annotation guides the compiler to perform borrow checks.

For example the following code:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Basically everyone first gets to know lifetime from this example.

But, let me be honest. I still can't understand how the syntax in this example relates to the behavior of the compiler. I don't think it's very simple.

In this case, I have many ways to self-hypnotize myself: I already understand.

For example, I could describe this syntax like this.

Dear rust compiler, when calling this function, please check for me :

  • The reference returned cannot exceed the lifetime of the original object referenced by the first parameter
  • The reference returned cannot exceed the lifetime of the original object referenced by the second parameter

Maybe in this case, I'm right to think this way.

But in the following example, I can't think like this.

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

In this case, the compiler reports an error: error: lifetime may not live long enough

I don't have the syntax and knowledge demonstrated in the first example to know what mistake I made in the second example.

Yes, this is the biggest problem in rust lifetime learning. I cannot face new cases through the existing examples I have learned.

So, first of all, I still want someone to explain in more general terms what information the lifetime annotation tells the compiler. Try to use a stricter, more general statement.

Okay, regarding the second example, why does it not compile, can anyone answer it, thank you very much, I have been stuck here for a long time.

Not sure whether it's what yore looking for, but I found this video very helpful for understanding lifetimes.

1 Like

In this code:

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

you might return x, which has the lifetime 'a.
Or you might return y, which has the lifetime 'b.
However, the return type says you will return with lifetime 'a.
This works if we return x, because it has that lifetime.
But if we return y, we violate the return type: we promised to return something that lives as long as 'a but we're returning something that lives as long as 'b. The compiler doesn't know how long 'b lives - it might be shorter than 'a. This is why it reports an error.

I hope this helps.

2 Likes

They tell the compiler how lifetimes are (or are not) related, and what constraints need to be upheld. It's part of the function API contact, which both the caller and callee must uphold.

In the second example, the two lifetimes have no required relationship besides the base requirement of both being valid for the function body. So maybe 'a is valid everywhere 'b is ('a: 'b), maybe the other way around ('b: 'a), or maybe neither -- they could overlap without one containing the other, like a Venn diagram. The function body has to support all of those possibilities.

The signature also says the return has the same lifetime as the first parameter. To the caller, this means the borrow of first parameter will last at least as long as the return value is in use (but the second parameter, with it's unrelated lifetime, does not). To the callee, it means any returned value must coerce to &'a _.

When the function body tries to return the second parameter, the compiler sees that this would require 'b: 'a, as it's invalid to coerce &'b _ to &'a _ otherwise. But recall that this may not be true! The callee can't assume that holds because it's not part of the API contract.

If you add the bound 'b: 'a like the hint suggests, it compiles. The lifetimes now have a required relationship and the callee can take advantage of it. But this also imposes more constrains on the caller: 'b: 'a means that 'b must remain active everywhere that 'a does, so now the borrow of the second parameter must last at least as long as the borrow of the first parameter (and thus at least as long as the return value is in use).

3 Likes

I think the first important thing to realise is that lifetime annotations go on references but "lifetimes" are a property of the actual data they reference.

So what is the life time of data? As a first approximation it is the scope in which it is declared. Could be the data is a local variable of a function, its lifetime ends when the function ends. Could be the data is declared in some inner scope of a function. Consider the following:

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

pub fn f() {
    let big: &i32;

    let x = 1;
    // x is live here
    {
        let y = 2;
        // both x and y are live here
        big = biggest(&x, &y);
        println!("{big}");   // This is OK both a and y are live.
    }
    // y went out of scope and "died"
    // Only x live here

    // println!("{big}");    // If this were allowed bad things
                             // would happen if y were the biggest.
                             // Then we would be trying to print a
                             // reference to y which no longer exists. 
}

As you see, x and y have different lifetimes determined by where they are declared.

The life time annotations on your biggest() function cannot change that. But having the same life time on the parameters and the return ensures that the returned reference will not get used where it is no longer valid.

2 Likes

The Rustonomicon is not an easy read but I always found useful to graps (I don't dare write "understand") the more complex parts of Rust to better understand the rest. In particular this chapter on lifetimes can provide some insights:

Or maybe can't. :confused:

I would actually advice not conflating these two things which are unfortunately both called "lifetimes":

  • The liveness scope of a value (variable, temporary, ...)
  • Rust lifetimes (those '_ things, e.g. the lifetime of a reference)

The latter is the duration of some borrow of a place (variable, temporary, dereference, ...). The borrow of a place cannot be longer than the the liveness scope of that place, so they are related. But being borrowed is incompatible with other things,[1] so they are not synonymous. And the same data can be borrowed multiple times for different durations.

The borrow checker can be thought of as a proof reader tha checks that all the declared constraints are upheld and that no borrow conflicts are present, but it won't alter the semantics of the source code. For example changing a lifetime annotation can't lengthen the liveness scope of a value.


  1. taking a &mut, being moved, ... ↩︎

1 Like

To the caller, this means the borrow of first parameter will last at least as long as the return value is in use (but the second parameter, with it's unrelated lifetime, does not). To the callee, it means any returned value must coerce to &'a _ .

This passage is very useful to me, thank you.