Should Option<&mut T> implement Copy?

Prepare for some borrow-workaround-gymnastics…

So, I can make a nice little count-with-mutable-pointer program as follows.

fn incr(p: &mut i32) { *p += 1; }
fn main() {
    let mut x = 0;
    for _ in 0..10 {
        incr(&mut x);
        println!("x: {}", x);
    }
}

Rust is quite happy to work out the lifetimes if we introduce an extra mut pointer, p. From a data point-of-view, it gets copied; from the lifetime point-of-view maybe p gets mutably borrowed and this dereferenced (this is the key point to the rest of the problem, and I don’t fully understand it).

fn incr(p: &mut i32) { *p += 1; }
fn main() {
    let mut x = 0;
    let mut p = &mut x;
    for _ in 0..10 {
        incr(p);
        println!("x: {}", *p);
    }
}

Lets make things more interesting by making p optional:

fn incr(p: Option<&mut i32>) {
    if let Some(q) = p {
        *q += 1;
    }
}
fn main() {
    let mut x = 0;
    let mut p = Some(&mut x);
    for _ in 0..10 {
        // uh oh... how do we pass p?
        incr(p);  // doesn't work
        println!("x: {}", *p.as_ref().unwrap_or(&&mut 0));
    }
}

It turns out we can make this work, by unwrapping and rebuilding the option:

        if let &mut Some(ref mut q) = &mut p {
            incr(Some(q));
        }

or like this:

        incr(p.as_mut().map_or(None, |p| Some(p)));

But this stuff is nasty to get your head around. Why doesn’t incr(p) work? Should incr(p.clone()) work? Or should Option have some other method, supporting incr(p.foo())?

Why doesn't incr(p) work? Should incr(p.clone()) work?

Because p ( the Option<&mut T> ) is attempted to be moved into incr() more than once (thanks to the loop). Option<&mut T> is not copyable or clonable, because if it was, it'd allow a user to create more than one mutable borrow to the same data.

This breaks rusts current borrowing axioms.

Ultimately you are passing ownership the Option, and once passed, it can't be passed again.

So why does the work around work?

    if let &mut Some(ref mut q) = &mut p {
        incr(Some(q));
    }

So here you borrow the original option as mutable, so that you can then mutably borrow the mutable borrow contained within, you end up with q being &mut &mut p. You then construct a new Option, whose life time is just the method call to incr() and is moved immediately in and thus only ever moved once, and (I assume) thanks to Rust's type inference and auto deref stuff, it knows it needs to create an Option<&mut i32> and not an Option<&mut &mut i32> and thus auto derefs q from &mut &mut i32 to an &mut i32.

You can see that if you dereference once, manually, it also works showing that it was at least a double reference:

if let &mut Some(ref mut q) = &mut p {
    // notice the deref operator next to q
    incr(Some(*q));
}

So why does this work:

incr(p.as_mut().map_or(None, |p| Some(p)));

and not this:

incr(p.as_mut());

Well, calling as_mut() on Option<&mut T> gives you a new Option<&mut &mut T>, you might mistakenly think this is a repeat of using the &mut &mut above. However, due to this return type being set, the deref can't apply.

However, with what you do with the mapping method, it does cause the same case as above:

|p| Some(p)

You are creating a new option which lifetime is the length of incr() function call and moved straight into it, and as with above, the type inference knows that it it needs to create a Option<&mut i32> despite the fact that you would actually be creating a new Option<&mut &mut i32> if type inference never kicked in. So, because it can auto deref p to match the type your are trying to assign the result to, it does, and thus it works.

Or should Option have some other method, supporting incr(p.foo())

With all this being the case though, I'm not sure how you can. Any method that would return an a new Option<&mut T> from an existing Option<&mut T> without consuming the original would give you the same issue as clone and copy.

Disclaimer: I'm not a rust expert, so if I'm wrong, please do let me know.

I missed that q was a double ref, thanks.

Option<&mut T> can't implement Clone or Copy because &mut T doesn't, for lifetime reasons. If the compiler supported some special trait, say BorrowCopyOfPointer, then Option<&mut T> could implement that, as could other wrapper types (&mut T would implicitly implement it).

More complexity, but for most users passing an Option<&mut T> or similar wrappers into functions would just magically work.

https://github.com/rust-lang/rfcs/issues/1403

2 Likes

You are quite right, this could be achieved as a method… I think… assuming my code here makes sense.

This I believe is just to solve you specific case above though:


enum MyOption<T> {
    Some(T),
    None
}

impl<'a, T> MyOption<&'a mut T> {
    fn my_reborrow<'b>(&'b mut self) -> MyOption<&'b mut T> {
        match self {
            &mut MyOption::Some(ref mut v) => MyOption::Some(&mut *v),
            &mut MyOption::None => MyOption::None
        }
    }
}

impl <T> MyOption<T> {
    fn unwrap(self) -> T {
        match self {
            MyOption::Some(v) => v,
            MyOption::None => panic!("None!")
        }   
    }
}

fn incr(v: MyOption<&mut i32>) {
    if let MyOption::Some(v) = v {
        *v += 1;
    }
}

fn main() {
    let mut x = 9;
    let mut y = MyOption::Some(&mut x);
        
    for i in 0..10 {
        incr(y.my_reborrow());
        println!("{}: {}", i, y.my_reborrow().unwrap());
    }
}

This was just me fiddling in the playground (link to code).

I can’t figure out how you’d achieve this as a generic trait so it could be usable for other types though.

1 Like

For anyone trying to follow what the map_or in the original example is doing, you could also write the following, which is a bit more straightforward:

incr(p.as_mut().map(|x| &mut **x));
1 Like

@eefriedman: your map version works with this example, but not with another I have. Not sure why (I get “does not live long enough” errors). Maybe my map_or version is more confusing than necessary because it should be .map_or(None, |p| Some(*p)) but Rust derefrences automatically when the * is omitted.

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