How do `where` clauses constrain lifetimes?

The following code runs fine:

fn set<'a, 'b>(dest: &'b mut &'a i32, src: &'a i32) where 'a: 'b {
    *dest = src;
}


fn main() {
    let i : i32 = 0;
    {
        let mut r : &i32 = &5;
        
        set(&mut r, &i);
    }
}

and that makes sense to me: 'a : 'b means "'a encloses 'b".

Or so I thought. If I replace where 'a : 'b with the reverse, where 'b : 'a, the program still compiles. Clearly the lifetime of i encloses that of r, but 'b does not enclose `'a'.

I assumed that the appearance of the type &'b mut &'a i32 in the argument list would cause Rust to infer that 'a must enclose 'b, and then my contrary constraint in the where clause would essentially make 'a and 'b identical: if a < b and b < a, then a == b. But that's not what's going on either.

So I'm obviously missing something pretty fundamental here.

[edit: I'm re-reading the appropriate section from the Rustonomicon so I'll be a little more likely to be able to understand whatever explanations are offered]

When you reverse the constraint, Rust must then infer the same lifetime for both 'a and 'b. Evidence:

  • You can change the where clause to where 'a: 'b, 'b: 'a and it still works.

  • You can change <'a, 'b> to a single lifetime parameter <'a> (changing 'b to 'a throughout) and it still works.

It seems like &mut &mut is a special case (similar to &&mut) where the lifetime bounds are handled for you by the compiler. I don't have any further info on this, but rust-internals should be able to help.

@Gankra, should this go into the Nomicon?

Nothing special's happening here. Remember: the borrowchecker is an optimizer. It will pick the smallest solution to your problem. It found the best solution it could for this problem: shrink the lifetime of the &i down to live as long as r. Here is a modified and annotated version:

fn set<'a, 'b>(dest: &'b mut &'a i32, src: &'a i32) 
    where 'a: 'b,    // b nested in a
          'b: 'a,    // a nested in b
{
    *dest = src;
}


fn main() 
{
    '5: {
        let i : i32 = 0;
        '4: {
            let x = 0;
            '3: {
                // r is a reference to something that lives for its scope
                let mut r: &'3 i32;
                // x lives for r's scope: OK!
                r = &'3 x;
                // i lives for r's scope: OK!
                set(&'3 mut r, &'3 i);
                // NOTE: this won't compile because the &mut was forced to
                // live as long as `r` does. `r` is now borrowed for its whole
                // life after calling `set` :(
                println!("{}", r);
            }
        }
        
    }
}

Actual Rust code:

fn set<'a, 'b>(dest: &'b mut &'a i32, src: &'a i32) 
    where 'a: 'b,    // b nested in a
          'b: 'a,    // a nested in b
{
    *dest = src;
}


fn main() 
{
    let i : i32 = 0;
    {
        let x = 0;
        {
            // r is a reference to something that lives for its scope
            let mut r: &i32;
            // x lives for r's scope: OK!
            r = &x;
            // i lives for r's scope: OK!
            set(&mut r, &i);
            // NOTE: this won't compile because the &mut was forced to
            // live as long as `r` does. `r` is now borrowed for its whole
            // life after calling `set` :(
            println!("{}", r);
        }
    }
}

Note that hoisting r's definition to live strictly longer than x makes this code correctly fail to compile, as there is no longer a correct solution:

fn set<'a, 'b>(dest: &'b mut &'a i32, src: &'a i32) 
    where 'a: 'b,    // b nested in a
          'b: 'a,    // a nested in b
{
    *dest = src;
}


fn main() 
{
    let i : i32 = 0;
    // r is a reference to something that lives for its scope
    let mut r: &i32;
    {
        let x = 0;
        {
            // x doesn't live for r's scope: OH NO!
            r = &x;
            // i lives for r's scope: OK!
            set(&mut r, &i);
        }
    }
}
2 Likes

I believe my last example could start compiling with non-lexical lifetimes, as the compiler would correctly identify that r is allowed to dangle after set is called, because it's never used.

Okay, so let me walk through why r isn't "live" (in the sense used in the Rustonomicon) at the println! a bit more carefully...

Normally, when I pass a reference to a function:

fn f(r: &mut i32) -> i32 { ... }

let mut x = 42;
let y = f(&mut x);
g(x);

the borrow checker will give that reference a lifetime confined to the call itself, as if I'd written:

fn f(r: &mut i32) { ... }

let mut x = 42;
let y = '3: { let temp = &'3 mut x; f(temp) };
g(x);

and that's why this x is live again when we call g: the reference's lifetime, and hence the extent of the borrow, is only '3.

That's the healthy normal arrangement. However, in my example, because of the where clause, set has a signature equivalent to:

fn set<'a>(&'a mut &'a i32, &'a i32);
//                 ^~~ inner
//         ^~~ outer

The same lifetime variable 'a appears for both "inner" and "outer". Since r itself is &'3 i32, that is unified with "inner", and the instance of 'a for this call expression is bound to '3.

However, both "inner" and "outer" have lifetimes of 'a, so our temporary &mut r is given a lifetime of '3. The borrow checker treats its borrow as extending to the end of '3, which is what leaves r still borrowed at the println!.

To remove extraneous factors, we could do the same with a single-argument alternative to set. The following compiles:

#![allow(unused_variables)]
fn main() {
    fn s<'a, 'b>(r: &'a mut &'b i32)
    where 'b: 'a //, 'a: 'b
    { }

    let i = 42;
    let mut r = &i;
    s(&mut r);

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

but if we uncomment that 'a: 'b constraint, we've forced the inner reference (i.e. r itself) and the outer reference (i.e. the &mut r) to have the same lifetime, which forces the borrow to extend to the end of r's scope, and we get an error complaining that r is still borrowed at the println!.

Yes, this seems like an accurate analysis.

Ah, great. Your explanation was very helpful, thanks a lot.