Unexpected <Option<&mut ...>> behavior

I have a number of inter-related questions I'd like to ask around this topic, which I hope is ok.

I started with the following code:

use std::cell::RefCell;

struct SomeStruct {
    x: u64,
    y: u64,
}

fn process_ss(ss: Option<&mut SomeStruct>) {
    let x = match ss {
        Some(ss) => Some(&mut ss.x),
        None => None,
    };

    //do mutable processing on x

    let y = match ss {
        Some(ss) => Some(&mut ss.y),
        None => None,
    };

    //do mutable processing on y
}

fn main() {
    let mut ss = SomeStruct {x: 1, y: 2};
    process_ss(Some(&mut ss));
}

If the option is in fact Some, I was hoping to get a mutable ref for x and y out of it, and pass it further down for processing.

This however produces an error:

Some(ss) => Some(&mut ss.x),
   |              -- value moved here
...
17 |         Some(ss) => Some(&mut ss.y),
   |              ^^ value used here after move
   |
   = note: move occurs because value has type `&mut SomeStruct`, which does not implement the `Copy` trait
help: borrow this field in the pattern to avoid moving `ss.0`
   |

First question - why is rust complaining I'm moving the value? Surely what's inside the option is a reference, not a value?

After a bunch of tinkering, I arrived at:

fn process_ss(mut ss: Option<&mut SomeStruct>) {
    let x = match ss {
        Some(ref mut ss) => Some(&mut ss.x),
        None => None,
    };

    //do mutable processing on x

    let y = match ss {
        Some(ref mut ss) => Some(&mut ss.y),
        None => None,
    };

    //do mutable processing on y
}
  • which works.

Second question - I don't fully understand what the mut in front of ss is doing. It seems we now have a mutable function parameter, which contains an option, which may contain a mutable reference? Why is this necessary?

Third and final question - is the way I solved it considered appropriate, or is there a better, more elegant way?

One other option I tried was this:

    let (x, y) = match ss {
        Some(ss) => (Some(&mut ss.x), Some(&mut ss.y)),
        None => (None, None),
    };

which leads to cannot borrow *ss as mutable more than once at a time

Nope, it works.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=942b61e02a3718d7a7df0d256565e17c

Yes it works in the simplified problem I presented, but it throws the error I showed in the actual code (too complicated to post here). I suspect it has to do with the downstream processing functions for x and y. I honestly tried my best to replicate the code in the simplified problem.

Still keen to get some insights on the questions!

It's not about references versus (non-reference) values, it's about move semantics. In Rust, unless a type implements Copy, it exhibits move semantics (or are "affine") -- this means that once you move them (assign to a new variable, pass to a function by value, ...), you can no longer use them.

let s = String::new();
let t = s;
println!("{}", s); // error[E0382]: borrow of moved value: `s`

As the note says, &mut references are not Copy, so they move. If they could be copied, you could trivially end up with two mutable references to the same memory, which is undefined behavior in Rust in a very core way. (Shared references (&) do implement Copy, though.)

In this case, you moved the contents of the Option out when you matched Some(ss), held onto a reference to the x parameter, and discarded access to the rest.


By using ref mut, in the match arm, you've now taken a mutable reference to the inside of the Option without moving it. Inside the match arm, you have a &mut &mut SomeStruct. When you make a field access, it sees through both outer references, and you reborrow the x field. But you haven't moved anything -- the x is still inside the ss is still inside the Option. Since you're creating a mutable reference to something inside the Option, the Option itself must be mutable. That's why you need to mark ss as mut in the parameter list.

It's similar to why you need mut on ss here:

let mut ss = SomeStruct { x: 0, y: 0 };
let rx = &mut ss.x;

If I could do everything in this, I would:

if let Some(ss) = ss {
    // Work with `SomeStruct` directly
}

If not, you can rewrite your match-based solution like so:

fn process_ss(mut ss: Option<&mut SomeStruct>) {
    let x = ss.as_mut().map(|ss| &mut ss.x);
    //do mutable processing on x

    let y = ss.as_mut().map(|ss| &mut ss.y); 
    //do mutable processing on y
}

It works in pretty much the same way: ss.as_mut() results in an Option<&mut &mut SomeStruct>, then the map turns that into an Option<&mut u64>, and nothing gets moved.

2 Likes

Thanks for the elaborate answer! Really appreciate it :pray:

how about using RefCell? Is that a good/bad solution? I understand it's less safe, but could probably make the code simpler. Or is it advised not to use unsafe features unless you have to?

What about RefCell? I don't see any problem here that requires interior mutability, and if you shoehorn it in somewhere it's just going to make things more complicated unnecessarily - not simpler.

Note also that RefCell is not unsafe to use; it is a safe abstraction around UnsafeCell (as are Cell, Mutex, and all other interior mutability types in Rust).

1 Like

Where I was thinking of using it was this part:

let (x, y) = match ss {
        Some(ss) => (Some(&mut ss.x), Some(&mut ss.y)),
        None => (None, None),
    };

changing it to this:

let (x, y) = match ss {
        Some(ss) => (Some(ss.x.borrow_mut()), Some(ss.y.borrow_mut())),
        None => (None, None),
    };

this would solve this error that I'm getting - cannot borrow *ss as mutable more than once at a time.

You're right about unsafety. My bad. It's safe, just does checking at runtime, not compile time.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.