Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I assign one dereference of a reference of a reference to another when the outer lifetimes differ?

Tags:

rust

lifetime

I want to write the following function:

fn foo<'a, 'b, 'c>(rr1: &'a mut &'c mut u32, rr2: &'b mut &'c mut u32) {
    *rr1 = *rr2;
}

But the compiler complains:

error[E0623]: lifetime mismatch
 --> src/lib.rs:2:12
  |
1 | fn foo<'a, 'b, 'c>(rr1: &'a mut &'c mut u32, rr2: &'b mut &'c mut u32) {
  |                                 -----------       ------------------- these two types are declared with different lifetimes...
2 |     *rr1 = *rr2;
  |            ^^^^ ...but data from `rr2` flows into `rr1` here

My mental model of Rust's lifetimes does not agree that the code is wrong. I read the type of rr2 as "A reference with lifetime 'b to a reference with lifetime 'c to an u32". Thus when I dereference rr2, I get a reference with lifetime 'c to an u32. This should be safe to store in *rr1, which has the same type.

If I require that 'b outlives 'c, it works:

fn foo<'a, 'b: 'c, 'c>(rr1: &'a mut &'c mut u32, rr2: &'b mut &'c mut u32) {
    *rr1 = *rr2;
}

This makes me think that the type &'b mut &'c mut u32 means the u32 at the end of the reference chain is only available during the intersection of 'b and 'c.

What is the right explanation for Rust's behavior here? And why do references of references behave this way instead of the way I thought they do?

like image 352
Lorenz Avatar asked Apr 13 '20 19:04

Lorenz


1 Answers

You cannot dereference a &'b mut &'c mut u32 and get a &'c mut u32 because:

  • &mut references are not trivially copiable, so you can't copy the &'c mut u32; and
  • You cannot move out of a reference, so you also can't move the &'c mut u32 (which would leave the outer reference dangling).

Instead, the compiler reborrows the u32 with the outer lifetime, 'b. This is why you get an error message that data from rr2 flows into rr1.

If foo were allowed to compile, you could use it to get two &mut references to the same u32, which is forbidden by the rules of references:

let (mut x, mut y) = (10, 20);
let mut rx = &mut x;
let mut ry = &mut y;
foo(&mut rx, &mut ry); // rx and ry now both refer to y
std::mem::swap(rx, ry); // undefined behavior!

If I require that 'b outlives 'c, it works

Because 'c must already outlive 'b¹, if you require that 'b also outlives 'c, it follows that 'c = 'b. The updated signature is equivalent to this:

fn foo<'a, 'b>(rr1: &'a mut &'b mut u32, rr2: &'b mut &'b mut u32)

That is, you have unified 'c and 'b, and now there's no problem borrowing a &'b mut u32 from rr2 because the inner and outer lifetimes both live for 'b. However, the compiler now won't let you write the broken code in the example I gave earlier, since ry is already borrowed for its entire lifetime.

Interestingly, if you make the inner reference non-mut, it also works:

fn foo<'a, 'b, 'c>(rr1: &'a mut &'c u32, rr2: &'b mut &'c u32) {
    *rr1 = *rr2;
}

This is because & references are Copy, so *rr2 is not a reborrow, but actually just a copy of the inner value.

For more information, read:

  • Why does linking lifetimes matter only with mutable references?
  • How can I modify a slice that is a function parameter?

¹ It might not be obvious why 'c outlives 'b when there is no explicit 'c: 'b bound. The reason is because the compiler assumes that the type &'b mut &'c mut u32 is well-formed. Well-formedness can become complex (see RFC 1214) but in this case it just means you can't have a reference that's valid for longer ('b) than the thing it references ('c).

like image 178
trent Avatar answered Oct 19 '22 21:10

trent