What can I learn from this nop function?

The following code will not compile , if I use the version without lifetime annotation (line A) , it will be ok , so how the lifetime annotation influence the compiler in this case?

fn main() {
  let mut a=0;
  let mut c=1;  
  
  let mut x=&mut a;
  let z=&mut c;
  fn nop<'a>(x:&mut &'a mut u8, z:&'a mut u8) {
//fn nop    (x:&mut &   mut u8, z:&   mut u8) {  // A
  }
  nop(&mut x, z);
  *x = *z;    
}

The lifetime annotations say that both x and z must have exactly the same lifetime. But when you invoke nop(), the &mut c and the &mut a in &mut &mut a have different lifetimes because they come from different variables defined on different lines.

1 Like

When you tied together the lifetimes of the borrow of a in x and the reborrow of z, the compiler can see the possibility that inside of nop, you transfer z to replace the borrow of a with the reborrow of z (a borrow of c):

fn nop<'a>(x: &mut &'a mut u8, z: &'a mut u8) {
    // The lifetimes are the same so this is valid
    *x = z;
}

You didn't do this, but remember, functions are analyzed based on their signatures and not their bodies.

So, as per the function signature, x has "become infected" with z's reborrow, and vice-versa (since the lifetime is equal)(Edit: maybe not due to the nesting, I'd have to think on this one a bit more). Or put differently, any further use of x extends the reborrow of z.

Moreover, types underneath of a &mut are invariant. That means that the length of the borrow of a (in let x = ...) must be 'a, and the length of the reborrow of z must also be 'a. And so 'a must last until the assignment *x = ... completes.

So any use of z before the assignment completes will be an error. This includes reading the value in *z in an attempt to assign it to *x.

If you break things out a bit more, you can see how the assignment to *x is considered a use of the reborrow of z.

As far as I'm aware, this "infectious" property is not well documented; it makes sense, but you have to sort of work out the reasons. (If someone has a citation, I'd welcome it.)


On a different level that doesn't match the error, but is still a valid point I believe:

  • As mentioned, 'a must be the lifetime of the borrow of a due to invariance
  • So again, this must last until the assignment to *x completes
  • And because you named it so, 'a must also be the lifetime of the reborrow of z
  • And thus the original z cannot be used until after the assignment completes

The commented version of nop works because it removes the constraint that some lifetimes are equal or related at all; there would be no way to perform *x = z in the function for example, because the reborrow of z may not be long enough (try it to see the error). And in fact, it must not be long enough for the program to compile. The borrow of a must still be long enough for the assignment, but the reborrow of z can be shorter -- just as short as the call itself.

Rephrasing, having no named lifetimes implies that the lifetimes of all parameters are independent and unrelated (beyond being valid for the call itself).

2 Likes

Revisiting this since it was bugging me -- this statement isn't completely true.

  • There are implied bounds with types like &mut &mut Whatever
    • The inner borrows lasts at least as long as the outer borrow
  • Lifetime elision can indicate a relation between an input lifetime and an output lifetime
  • Various other niche circumstances like &dyn Trait /* + 'lt */

...but in this example specifically, the statement gives the right intuition.