Why the difference in behaviour when overwriting a moved reference?

Hello! I am new to Rust and trying to figure out how its move semantics work with regards to overwriting moved values. In particular, I wanted to know if it is possible to overwrite a moved reference with a reference with a disjoint lifetime. It appears that this is the case, at least as far a local variables are concerned. The following code behaves as one would expect.

fn main(){
   let mut x = 1;
   let mut y = 2;
   let mut z = &mut x;
   let w = z;
   x = 3;
   z = &mut y;
}

However, if we add tuples to the mix, this no longer appears to be true.

fn main(){
   let mut x1 = 1;
   let mut x2 = 2;
   let mut y = 3;
   let mut z = (&mut x1, &mut x2);
   let w = z.1;
   x2 = 4;
   z.1 = &mut y;
}

This produces the error:

error[E0506]: cannot assign to `x2` because it is borrowed
 --> <source>:7:4
  |
5 |    let mut z = (&mut x1, &mut x2);
  |                          ------- `x2` is borrowed here
6 |    let w = z.1;
7 |    x2 = 4;
  |    ^^^^^^ `x2` is assigned to here but it was already borrowed
8 |    z.1 = &mut y;
  |    ------------ borrow later used here

In this case, the use of z.1 appears to cause the lifetime to be extended, even though the value with this lifetime has been moved. I am curious as to why this occurs.

From what I have seen so far, it appears that the tuple is assigned a type which is parameterised by the lifetimes of its two components. Is it the case that this lifetime must then last as long as `z' is still being used? That is, only a single lifetime is inferred for the type of the tuple, where as in the variable case, the type can be changed on overwriting.

Types of variables don't change. However, lifetimes can have gaps. (Such as in the running example of RFC 2094.)

Here:

/*L1*/   let mut x = 1;
/*L2*/   let mut y = 2;
/*L3*/   let mut z = &mut x;
/*L4*/   let w = z;
/*L5*/   x = 3;
/*L6*/   z = &mut y;

The lifetime in z's type needs to be active on L3, L4, and L6. z and w are "dead" (their values are never used) after L4, so the lifetime need not be active on L5. (See this part of RFC 2094.)

Here:

/*L1*/   let mut x1 = 1;
/*L2*/   let mut x2 = 2;
/*L3*/   let mut y = 3;
/*L4*/   let mut z = (&mut x1, &mut x2);
/*L5*/   let w = z.1;
/*L6*/   x2 = 4;
/*L7*/   z.1 = &mut y;

z's type has two lifetimes, call them 'z0 and 'z1. And I think what's going on is that borrow checking is looking at the liveness of the variable, not its fields. So although w and z.1 are dead after L5, the variable z must still be usable on L7 and this is, I conjecture, making 'z1 be active on the left-hand-size of L7 and thus also on L6.

Here's a version which shows that clobbering the entire tuple allows things to compile.

Here's a slight variation where we store z.0 in another variable before modifying x2.

Here's another, where the assignment to the new variable is done after x2. It doesn't compile, and the error points to the read of z.0 as a use of the borrow of x2 (which was stored in z.1). I take this to mean that accessing a field of z is considered a use of all of it's lifetimes, regardless of the liveness of the individual fields.

It's possible the compiler could get smarter about this scenario (though I wouldn't necessarily hold my breath).

2 Likes