Curious case of destructure on reference

Hey, fellas. I have a problem with rust that I cannot convince it to move a value out of mutable reference and then rewrite it to point to a fresh object. Basically, it's this snippet that troubles me.

struct Smth(u32);
fn mutate(val: &mut Smth) {
  let loc@Smth(thing) = val;
  let _ = thing;
  *loc = Smth(0);
}

Compiler keep nudging me that I use moved value. How do you deal with this? Certainly there is a way, right?

Binding modes are making this painful. If you want thing to be a u32, you could do

    let ref mut loc @ Smth(thing) = *val;

But in general I think this binding is confusing and that you should do one of

    let loc = val; // Assuming you want the new name for some specific reason
    let thing /* : u32 */ = loc.0;

Same thing basically:

    let thing /* : u32 */ = val.0;
    let loc = val; // Assuming you want the new name for some specific reason

Or if you want thing to be a &mut u32:

    let loc = val; // Assuming you want the new name for some specific reason
    let thing /* : &mut u32 */ = &mut loc.0;

In this last case, once you use loc again, you can't use thing anymore.

For any of these, you could do

    let loc = &mut *val;

In order to reborrow val instead of moving it.


On a higher level, this is an example of "match ergonomics" being unergonomic / confusing. Another name for the behavior is "binding modes". You can read some documentation on binding modes here.

In your original, your non-reference pattern loc made the default binding mode ref mut, so on the inside, thing was taken to be a &mut u32. However, it couldn't reconcile the borrow on the inside with moving val into loc (where is above, the borrow on the inside is taken after the move). One fix is to first move val and then mutably borrow the contents from loc.

This non-working version is similar, but it acts on a reborrow of val instead of a move. It still doesn't work because both bindings are trying to borrow from val, as opposed to thing borrowing from loc, say.

    let ref mut loc @ Smth(ref mut thing) = *val;

Similar to before, one possible fix is to first make loc a reborrow, and then to borrow thing from inside loc.

And finally, the working version where thing is a u32 works because the outermost ref mut disables the resetting of the default binding mode; it remains "move", so thing is taken to be a u32.

1 Like

Thinking about it a minute more (naturally), you can also do this:

    let loc @ &mut Smth(thing) = val;

thing is again a u32 and loc is a &mut Smth, but val has been moved and not reborrowed. Similar to before, the use of &mut prevents resetting the default binding mode.

Here's a good article which may make this version more clear.

1 Like