Why doesn’t matching on a `&mut T` re-borrow?

I recently tried to figure out how to use the reference inside of an Option<&mut T> on the stack, without the variable containing the Option being mut.

(playground containing all the examples from below)

does_not_compile!{
    fn foo() {
        let mut x = 1;
        let y = Some(&mut x);
        if let Some(r) = y {
            *r += 1;
        }
        // error: use of moved value
        if let Some(r) = y {
            *r += 1;
        }

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

At first, I thought it might be impossible. This works, but we need to have mut y:

// workaround using match ergonomics
fn bar() {
    let mut x = 1;
    // needs mut here now
    let mut y = Some(&mut x);
    if let Some(r) = &mut y {
        // and deref twice
        **r += 1;
    }
    if let Some(r) = &mut y {
        **r += 1;
    }

    println!("{}", x); // -> 3
}

Also the **r are not very nice. It took me a while until I had everything figured out. IIRC, I also didn’t find any approach that had me getting rid of the ** (even with the mut y) other than this wordy biest:

// "proper" approach
fn foo() {
    let mut x = 1;
    let y = Some(&mut x);
    // isn’t `&mut ref mut` just ridiculous?
    if let Some(&mut ref mut r) = y {
        *r += 1;
    }
    if let Some(&mut ref mut r) = y {
        *r += 1;
    }

    println!("{}", x); // -> 3
}

This makes me wonder: Why don’t we just have a binder to a &mut T variable re-borrow the reference by default? This would make the first version compile. After all, re-borrows should not make any actual difference in the compiled assembly, right? What would be good reasons against such a change? Of course, taking this approach systematically would mean that even an assignment like let x = y where y: &mut T would re-borrow.

Here’s another example of a problem where the compiler tries to insert a move on matching on/binding a &mut T; this time the Option is behind a reference:

// similar problems behind a reference
does_not_compile!{
    fn baz() {
        let mut x = 1;
        let y = &mut Some(&mut x);
        // error: cannot move out of y.0 which is behind a mutable reference
        if let &mut Some(r) = y {
            *r += 1;
        }

        println!("{}", x);
    }
}
// solution
fn baz() {
    let mut x = 1;
    let y = &mut Some(&mut x);
    // let’s have *mut* every *mut* second *mut* word *mut* be "mut"!
    if let &mut Some(&mut ref mut r) = y {
        *r += 1;
    }

    println!("{}", x); // -> 2
}
2 Likes

My first thought would be that the following should work:

fn foobaz() {
    let mut x = 1;
    let y = Some(&mut x);
    
    if let &Some(r) = &y {
        *r += 1;
    }

    println!("{}", x); // 2
}

But it seems that my understanding about how patterns match up is slightly off since the compiler suggests to remove the & in front of the Some and if I do then I get a mutability error.

Here's a function that would do the reborrowing of the Option. Match ergonomics does some work here making the implementation look deceptively trivial.

fn rb<'a>(m: &'a mut Option<&mut i32>) -> Option<&'a mut i32> {
    match m {
        Some(r) => Some(r),
        None => None,
    }
}

It turns out there is a method for Option that does the same more generally: as_deref_mut. With it you can write

let mut x = 1;
let mut y = Some(&mut x);
// alternatively:
//let y = &mut Some(&mut x);
if let Some(r) = y.as_deref_mut() {
    *r += 1;
}

... although I'm not sure how much sense the function name makes in this example.

Note that you still need the mut y in your example. Also this doesn’t generalize to custom enums/structs or more deeply nested ocurrences. Nonetheless thanks for mentioning this function, I had’t really ever noticed it yet.

1 Like

Wouldn't it be a problem otherwise? Isn't it true you still need to ensure y has not been borrowed immutably by the time you get that &mut i32 out of it?

1 Like

Yes, but it isn’t necessary that y itself is mutably borrowed for this. The borrow checker can directly ensure unique access to the field that you’re re-borrowing with a &mut ref mut r pattern. I’m just going to refer you back to my original post for this:

This code does compile and doesn’t need mut y.

It’s of course also true that this kind of access to a &mut T variable or a SomeStructOrEnumContaining<&mut T> variable cannot be abstracted over (i.e. you can’t write a method that works like this). It only works through field access; and just for the &mut T type. So is not a kind of capability that other smart-reference-like types can offer. A good example is how you usually need to declare a variable containing a MutexGuard as mut to properly use it.

If I had to criticize something, it would rather be the fact that you can reborrow without a mut binding to begin with, rather than the fact that some patterns do require an explicit mut y annotation :smile: But hey, if that was always the case, we'd need to always be writing let mut y = &mut ... for the y to be mut-reborrowable, which goes to show how mut bindings are just a lint, and as with any lint, there may be false positives…


But I have encountered the case where I was mut writing mut every mut other mut word like you showcased, and it was a bit annoying… But if ergonomics are the issue, then match ergonomics can come in handy, for once (since I loathe them, I have sticked to the more quaint &mut ref mut (with a *-deref on the RHS)). So, I agree with your suggestion:

provided it can be done without affecting current code semantics (I am thinking maybe there could be some contrived unsafe code that happens to have a non-dangling CString::new().as_ptr()-like pointer thanks to the current move-by-default semantics that could become unsound if this were to change? :thinking:)

1 Like

Today I learned, assignment expressions already do reborrow. Which can be quite confusing because they often seem so similar to let statements:

let mut x1 = 0;
let y1 = &mut x1;
let mut z1 = &mut 0;
z1 = y1;
*z1 += 1;
*y1 += 1;
println!("{}", x1);


let mut x2 = 0;
let y2 = &mut x2;
let z2 = y2;
*z2 += 1;
// *y2 += 1; // use of moved value: `y2`
println!("{}", x2);

Ah, and for maximal confusion, this rule only seems to apply when the variable was already initialized:

let mut x3 = 0;
let y3 = &mut x3;
let z3;
z3 = y3;
*z3 += 1;
// *y3 += 1; // use of moved value: `y3`
println!("{}", x3);

(playground with all of the above)

Edit: On further inspection, this seems to be related to type-checking. The fact that the compiler already knows that the variable about to be bound is a reference is what triggers the reborrowing. Some more examples:

let mut x4 = 0;
let y4 = &mut x4;
let mut z4;
if false { z4 = unreachable!() }
z4 = y4;
*z4 += 1;
// *y4 += 1; // use of moved value: `y4`
println!("{}", x4);

let mut x5 = 0;
let y5 = &mut x4;
let mut z5;
if false { z5 = &mut 0 }
z5 = y5;
*z5 += 1;
*y5 += 1;
println!("{}", x5);

let mut x6 = 0;
let y6 = &mut x5;
let z6: &mut i32 = y6;
*z6 += 1;
*y6 += 1;
println!("{}", x6);

(playground with the examples 4, 5, 6)

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.