Simple usage of lifetime constraint ('a: 'b) seems to be ignored?

#![allow(unused)]
fn main() {
    let outer_ok = "outer".to_owned();
    let outer_ref_ok: &str = &outer_ok;
    let my_ref: &str;
    {
        let inner_ok = "inner".to_owned();
        let inner_ref_ok: &str = &inner_ok;
        test_lifetime1(inner_ref_ok, outer_ref_ok);  // OK! But why?  ****
        test_lifetime2(outer_ref_ok, inner_ref_ok);  // OK
        my_ref = test_lifetime3(inner_ref_ok, outer_ref_ok);
    }
    println!("{outer_ref_ok}");  // OK, print: "outer"
    // println!("{my_ref}");   // ERROR if uncommented: `inner_ok` dropped after the bracket while still borrowed
}
fn test_lifetime1<'a, 'b, T>(outer: &'a T, inner: &'b T) where T: ?Sized, 'a: 'b {}
fn test_lifetime2<'a, T>(outer: &'a T, inner: &'a T) where T: ?Sized {}
fn test_lifetime3<'a, 'b, T>(outer: &'a T, inner: &'b T) -> &'a T where T: ?Sized, 'a: 'b {outer}

In this code snippet, I wonder why is test_lifetime1 allowed by the borrow checker and works well, for I specified that 'a is longer - or at least as long as 'b in official terms. I think the borrow checker should first annotate all the variables their lifetime and then it will notice that outer_ok has a larger scope than inner_ok and that inner_ok definitely isn't as long as outer_ok but is strictly shorter than it, which violates the constraint. I have one speculation. All the usages of this syntax is related to some struct and their reference field after my looking up some references. So maybe using it in this trivial circumstance is ignored by the borrow checker. But I don't know if this is the case.

  • Whenever you pass a reference, the lifetime it has can be shortened to satisfy requirements. So, 'a in the call to test_lifetime1 can be shorter than the lifetime which would cover all of inner_ref_ok's borrowing of inner_ok.
  • test_lifetime1 requires that 'a be at least as long as 'b, but it doesn't require that either of a or 'b be any longer than the duration of the call to test_lifetime1.

Therefore, no lifetime constraint is violated.

That’s because the call to test_lifetime1 does not receive the two references necessarily at their full lifetime. Lifetimes can be implicitly shortened (through so-called “subtyping coercion”). For instance, any 2 references could both be shortened to a common lifetime shorter than either of the original lifetime. The shortened lifetimes match, and in the function that receives them any constraint like 'a: 'b would be trivially met because the two shortened lifetimes are the same; this works no matter how the relationship between the original lifetimes was.

For comparison, something that prevents this automatic shortening is “invariant” usage. One example for this is for lifetimes that appear in types behind mutable references.

Indeed, if you were to change up your test functions a little bit, receiving parameters like

(outer: &mut &'a T, inner: &mut &'b T)

you can achieve this effect. Now, test_lifetime1 is no longer accepted, precisely because of the 'a: 'b constraint:

#![allow(unused)]

fn main() {
    let outer_ok = "outer".to_owned();
    let mut outer_ref_ok: &str = &outer_ok;
    let my_ref: &str;
    {
        let inner_ok = "inner".to_owned();
        let mut inner_ref_ok: &str = &inner_ok;
        // test_lifetime1(&mut inner_ref_ok, &mut outer_ref_ok); // ERROR if uncommented
        // test_lifetime2(&mut outer_ref_ok, &mut inner_ref_ok); // ERROR if uncommented
        // my_ref = test_lifetime3(&mut inner_ref_ok, &mut outer_ref_ok); // ERROR if uncommented
        test_lifetime_no_constraint(&mut inner_ref_ok, &mut outer_ref_ok); // OK
    }
    println!("{outer_ref_ok}"); // OK, print: "outer"
                                // println!("{my_ref}");   // ERROR if uncommented: `inner_ok` dropped after the bracket while still borrowed
}

fn test_lifetime1<'a, 'b, T>(outer: &mut &'a T, inner: &mut &'b T)
where
    T: ?Sized,
    'a: 'b,
{
}

fn test_lifetime2<'a, T>(outer: &mut &'a T, inner: &mut &'a T)
where
    T: ?Sized,
{
}

fn test_lifetime3<'a, 'b, T>(outer: &mut &'a T, inner: &mut &'b T) -> &'a T
where
    T: ?Sized,
    'a: 'b,
{
    outer
}

fn test_lifetime_no_constraint<'a, 'b, T>(outer: &mut &'a T, inner: &mut &'b T)
where
    T: ?Sized,
{
}
1 Like

Your reply and the example is amazing. I have been reading the nomicon for a short time and your reply provides me with some new connection. That's awesome!

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.