Just another problem with borrows

I'm "playing" (but not having fun) with lifetimes and references and I can't understand
because the following code gives error:

fn reassign<'a>(p1: &'a mut &'a mut i32, p2: &'a mut &'a mut i32) -> &'a mut i32 {
     *p1 = *p2;

     *p1
}


fn main() {
     let mut debug = 0;
     {
         let mut _a = 11;
         let mut _b = 33;
         println!("Before: &_a = {:p}, _b = {:p}", &_a, &_b);
        
         let mut ref_a = &mut _a;
         let mut ref_b = &mut _b;

         let result = reassign(&mut ref_a, &mut ref_b);

         //*ref_a = 55;

         *result = 77;
     }

Uncommenting the line: //*ref_a = 55;
the compiler reports the following:

"cannot assign to `*ref_a` because it is borrowed
`*ref_a` is assigned to here but it was already borrowed"

to be more precise he adds:

*ref_a` is borrowed here: let result = reassign(&mut ref_a, &mut ref_b);
borrow later used here: *result = 77

;

Why ?
I also tried to analyze it step by step also with ChatGPT but in the end it itself and sent me here

I guess you understand the intent of this code and it doesn't matter that it's not the best way
to obtain it, I repeat: I'm playing/experimenting with Rust to try to better understand some of its peculiarities (and I'm definitely also influenced/deviated by the legacy of other languages) but especially with Rust, more which in other languages, I find myself in this situation:
https://www.youtube.com/watch?v=yqp3KXDu9qE.
And I'm not Feynman, for sure.

Thanks everyone, anyway

Let's consider some lifetimes in main:

  1. The expression &mut _a creates a borrow that lasts for lifetime 'l1.
  2. The expression &mut ref_a creates a borrow that lasts for lifetime 'l2.

Now, we get 'l2 ≤ 'l1 because otherwise &mut ref_a would be valid when ref_a is no longer valid, which is clearly problematic.

However, when you call reassign, since it reuses the same lifetime 'a for both 'l1 and 'l2, and this introduces the constraint that 'l1 ≤ 'l2. Combined with the previous constraint, we get that both lifetimes must end at the same time.

Now, you have a line containing *ref_a = 55. This assignment is only possible if 'l2 has ended when you get to this line, since 'l2 is the duration in which ref_a is borrowed, and ref_a must not be borrowed for you to assign to it.

However, the line *ref_a = 55 also requires that 'l1 has not yet ended. This is because ref_a originates from a borrow of _a, and if _a is no longer borrowed, then we can't use the borrow anymore.

So you now have a line where 'l2 must already have ended, but 'l1 can't have ended yet. Since you also require that they end at the same time, you get a compilation failure.

So it's indeed getting smarter :smile: (j/k).

In your code, result is basically an alias for ref_a, so you can't reborrow it as long as result exists. That's what the compiler is telling you.

1 Like

The previous replies have discussed the code in main, but this function signature is also doomed to fail:

fn reassign<'a>(
    p1: &'a mut &'a mut i32,
    p2: &'a mut &'a mut i32,
) -> &'a mut i32 {

You should not ever use the same lifetime parameter for a mutable reference (p1: &'a mut ...) and any part of its referent (... &'a mut i32), because that ends up implying that the thing is borrowed for the rest of its own existence.

You need to let the inside and outside be separate lifetimes:

fn reassign<'a, 'b>(
    p1: &'b mut &'a mut i32,
    p2: &'b mut &'a mut i32,
) -> &'b mut i32 {

But then you'll find that the function doesn't compile. This is not because the lifetime annotations are wrong — they accurately reflect what it's trying to do — but because you may not duplicate a mutable reference (ending with both *p1 and *p2 containing the mutable reference that was initially contained by *p2), nor leave *p2 moved-out-of.

If you're OK with swapping the two references, then you can do that with a standard library helper function:

fn reassign<'a, 'b>(
    p1: &'b mut &'a mut i32,
    p2: &'b mut &'a mut i32,
) -> &'b mut i32 {
     std::mem::swap(p1, p2);
     *p1
}

Note that the returned lifetime is &'b mut i32, not &'a mut i32. This is again because of the rule that you can't duplicate a mutable reference: if it was &'a mut i32 then you could use it along with *p1 (that is, ref_b) once the function returned, but the 'b lifetime from the borrows means that (from the outside view) ref_b will be borrowed-and-therefore-unusable until you drop result.

3 Likes

Why returning the longer lifetime 'a(instead of 'b) doesn't work?

fn reassign<'a, 'b>(
    p1: &'b mut &'a mut i32,
    p2: &'b mut &'a mut i32,
) -> &'a mut i32 {}

Their last paragraph explains why.

Here's a use after free if you force it. (Run Miri under Tools, top-right.)

1 Like