Rust lifetimes question

Can someone explain why the compiler allows code like this? If the lifetime label is the same no matter which ref is returned how does the compiler know which actual lifetime to select? Does it simply select the longest one? For example if parameter a's lifetime is longer than b's lifetime, but the function returns a.

fn main() {
    let a = "A";

    {
        let b = "B";
        let result = pick(&a, &b);        
    }
    
    println!("{}", a);
}

fn pick<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() {
        a
    } else {
        b
    }
}

No, it coerces the longer one(s) to the shortest one.

The compiler looked at the lifetime of the two variables you passed it (&a and &b) and chose 'a to be the smallest of the two so your result variable won't be able to outlive b. If you try to make that println!() statement print result you'll get a lifetime error.

The proper term for this "find the shortest lifetime" process is Variance, and the nomicon discusses it in more detail.

2 Likes

So then if that's the case shouldn't the println("{}", a); fail since a's lifetime was set to be the same as the shorter b's lifetime and b's lifetime ended in the inner scope?

Lifetimes can be shortened when calling a function, so that works the same as if it were written like this:

fn pick<'ret, 'a: 'ret, 'b: 'ret>(a: &'a str, b: &'b str) -> &'ret str {

AKA so long as the arguments live longer than the return value needs to, you're fine.

That lifetime selection only affected the &a reference in pick(&a, &b).

There's a couple of reasons you're not seeing what you expect in your example:

  • String literals have the type &'static str, and so effectively live forever.
  • You only use the reference returned by pick inside the block, where both a and b are still valid.

If you change your example to use String and allow the reference from pick to escape b's scope, then you can see the lifetime shortening behaviour mentioned in the other replies:

fn main() {
    let a = String::from("A");

    let foo = {
        let b = String::from("B");
        pick(&a, &b)
    };
    
    println!("{}", a);
}

fn pick<'a>(a: &'a str, _b: &'a str) -> &'a str {
    a
}
error[E0597]: `b` does not live long enough
 --> src/main.rs:6:18
  |
4 |     let foo = {
  |         --- borrow later stored here
5 |         let b = String::from("B");
6 |         pick(&a, &b)
  |                  ^^ borrowed value does not live long enough
7 |     };
  |     - `b` dropped here while still borrowed

Notice that this error occurs even though the code never actually returns b!

5 Likes

The scope of a variable is not affected by lifetime declarations of references to it. This is a common misconception: the scope of a variable is determined once and for all by the place where it is declared. Lifetime annotations don't modify its scope; instead they are checked to be consistent with the declaration.

5 Likes

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.