Lifetime constrain not work?

// Here, Rust infers a lifetime that is as short as possible.
// The two references are then coerced to that lifetime.
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}

// `<'a: 'b, 'b>` reads as lifetime `'a` is at least as long as `'b`.
// Here, we take in an `&'a i32` and return a `&'b i32` as a result of coercion.
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // Longer lifetime

    {
        let second = 3; // Shorter lifetime

        println!("The product is {}", multiply(&first, &second));
        println!("{} is the first", choose_first(&second, &first));
    };

    println!("{}", first);
}

the signature of choose_first says the first arg must outlive the second arg, I call choose_first with args not satisfying the constrain. However, this compiles.

Why does the lifetime constrain not work?

The lifetimes in the function signatures are the lifetimes of the references, not of the values passed in. You created some references within the block, passed them to choose_first, and then printed the result -- and didn't use the returned value pass that. All the references were no longer needed at the end of the inside block (where second gets dropped).

So the compiler chose some lifetimes (like from println to the end of the inner block) which worked with your signature. Generally, the compiler tries pretty hard to find some lifetimes that will work (to the point where some cases it doesn't handle yet are considered bugs).

Here's an update of your code where I force the return value to stick around until after the inner block ends. As you can see, it doesn't compile because the reference would have to last longer than second.

What a wake-up call! Thx!

// `<'a: 'b, 'b>` reads as lifetime `'a` is at least as long as `'b`.
// Here, we take in an `&'a i32` and return a `&'b i32` as a result of coercion.
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // Longer lifetime

    let cf = &first; // lifetime --------------------------------------⬇️

    {
        let second = 3; // Shorter lifetime

        let cs = &second; // lifetime ------------------------⬇️

        println!("{} is the first", choose_first(cs, cf));
        // ---------------------------------------------------⬆️
    };
    // ---------------------------------------------------------------⬆️
}

wait... How about this?

This has to do with subtyping and variance, I gave a detailed walkthrough of variance in an example here: Lifetime shortening and turbofish ascription - #2 by RustyYato

2 Likes

The short version is that when you make a call like choose_first(cs, cf), and cs and cf are references, it's sort of like you just freshly borrowed those references again and they can have shorter lifetimes than before -- so short they end on the same line as the println for example.

Let's look a little closer:

// `<'a: 'b, 'b>` reads as lifetime `'a` is at least as long as `'b`.
// Here, we take in an `&'a i32` and return a `&'b i32` as a result of coercion.
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // Let's say first: i32 + 'f

    let cf = &first; // And cf: &'cf i32
    {
        let second = 3; // And s: i32 + 's

        let cs = &second; // cs: &'cs i32

        // See Below
        println!("{} is the first", choose_first(cs, cf));
        
        // 'cs ends here
        // 's ends here
        // 'cf and 'f could have ended here too since you didn't use them any more...
    };
    
    // But you can force them to live all the way to here by using them
    println!("{}", cf);
    // 'cf ends here
    // 'f ends here
}

So in this version, 'cf definitely lasts longer than 'cs as I have written them.

And the signature of choose_first says the first reference must last as long as (or longer than) the second reference.

So why did the call to choose_first work? It works because a & reference with a shorter lifetime is considered a sub-type of a & reference with a longer lifetime. It works because a & reference with a longer lifetime is considered a sub-type of a & reference with a shorter lifetime. So when you call choose_first, the shortest pair of lifetimes possible are chosen. It's sort of like this happened:

        {
            let temp_cf = &*cf; // Some new shorter borrow
            let temp_cs = &*cs; // Same
            println!("{} is the first", choose_first(cf, cs));
            // The borrows end here
        }

This system is usually presented formally by explaining variance, which unfortunately, can be complicated and confusing. A shorter version is "the compiler can shrink the lifetimes of your & references at the call site." That's what "covariant" means in the link.


Bonus example: If you look on that variance page, it says:

    b: &'b mut B, // covariant over 'b and invariant over B

When a type is invariant, the lifetime can't be shrunk (or grown). So this says that if we used something with a lifetime that was itself behind a &mut, the inner lifetime can't be shrunk. Let's see:

// I only care about enforcing the lifetime bounds here
fn zero<'a: 'b, 'b>(_: &'a str, _: &mut &'b i32) -> u32 {
    0
}

fn main() {
    let first = 2;

    let mut cf = &first;
    let ccf = &mut cf;
    {
        let second = "foo".to_string();
        let cs = &second;

        println!("{} is the zero", zero(cs, ccf));
    };

    println!("{}", cf);
}

And yep, this one fails to compile. The compiler couldn't shrink the borrow because it was behind a &mut. (The phrasing of the error is a bit odd.)

If you remove the last println, the lifetime can be short enough from the beginning and that also works. (This is what I meant before when I said the compiler tries really hard to find some lifetimes that allow the code to compile.)

1 Like

typo?

fn bar<'a>() {
    let s: &'static str = "hello";
    let a: &'a str = s;
}

above case, s(long) is a subtype of a(short)

1 Like

D'oh, indeed. Thank you. Corrected.

another detail:

// I only care about enforcing the lifetime bounds here
fn zero<'a: 'b, 'b>(_: &'a str, _: &mut &'b i32) -> u32 {
    0
}

fn main() {
    let first = 2;

    let mut cf = &first; // lifetime 'b start
    let ccf = &mut cf;

    {
        let second = "foo".to_string();
        let cs = &second; // lifetime 'a start

        println!("{} is the zero", zero(cs, ccf));
        // lifetime 'a end, lifetime 'b end
    };

    // println!("{}", cf);
}

In this case(the last line is removed), the lifetime start points aren't same, end points can be same. 'b is still longer than 'a? And you say 'b can't be shorten because of the mut

In the version with a final println!, the lifetime of cf has to extend past that final println! -- and it can't be shortened in the call to zero. The lifetime of cs must end before this (at the end of the inner block at the latest), and so the constraints cannot be satisfied.

In the version without a final println! the lifetime of cf can end at the same time the lifetime of cs ends. And thus even though it can't be shortened in the call to zero, it can still satisfy the constraints.

Or in other words, if you don't force the lifetime of cf to be longer than that of cs (e.g. by adding the final println!), the compiler will choose a lifetime of cf that allows the constraints to be satisfied by making it as short or shorter than that of cs.

you mean the start point of lifetimes can be ignored?

Yes -- lifetime constraints are "forward looking". The history of the lifetimes don't matter, just their future. Therefore it doesn't matter if one lifetime started before another -- by the time they both exist so that you can constrain the relationship between them, all that matters is where they end.


Even more technically, the lifetimes aren't even a continuous span of source code -- they're a set of points where the borrow rules must be enforced. But it's generally easier to reason about them as a forward looking region constraint. If you want to explore the gritty details some more, though, here are some references:

It is all quite dense reading, but may lend some insight.

2 Likes

I'm struggling in nll section :joy:
One question:

// let mut foo: i32;
// let mut bar: i32;
// let mut p: &i32;

A
[ p = &foo     ]
[ if condition ] ----\ (true)
       |             |
       |     B       v
       |     [ print(*p)     ]
       |     [ ...           ]
       |     [ p = &bar      ]
       |     [ ...           ]
       |     [ goto C        ]
       |             |
       +-------------/
       |
C      v
[ print(*p)    ]
[ return       ]

what does the [... ] mean?

Just a place-holder; code that isn't relevant, or maybe it's there if they need to refer to the point in execution between the two statements.

1 Like

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.