Search code examples
rust

replace a value behind a mutable reference by moving and mapping the original


TLDR: I want to replace a T behind &mut T with a new T that I construct from the old T

Note: please forgive me if the solution to this problem is easy to find. I did a lot of googling, but I am not sure how to word the problem correctly.

Sample code (playground):

struct T { s: String }

fn main() {
    let ref mut t = T { s: "hello".to_string() };
    
    *t = T {
        s: t.s + " world"
    }
}

This obviously fails because the add impl on String takes self by value, and therefore would require being able to move out of T, which is however not possible, since T is behind a reference.

From what I was able to find, the usual way to achieve this is to do something like

let old_t = std::mem::replace(t, T { s: Default::default() });

t.s = old_t + " world";

but this requires that it's possible and feasible to create some placeholder T until we can fill it with real data.

Fortunately, in my use-case I can create a placeholder T, but it's still not clear to me why is an api similar to this not possible:

map_in_place(t, |old_t: T| T { s: old_t.s + " world" });

Is there a reason that is not possible or commonly done?


Solution

  • Is there a reason [map_in_place] is not possible or commonly done?

    map_in_place appears possible by combining the low-level read() and write() operations:

    // XXX unsound, don't use
    pub fn map_in_place<T>(place: &mut T, f: impl FnOnce(T) -> T) {
        let place = place as *mut T;
        unsafe {
            let val = std::ptr::read(place);
            let new_val = f(val);
            std::ptr::write(place, new_val);
        }
    }
    

    But unfortunately it's not sound. If f() panics, *place will be dropped twice. First it will be dropped while unwinding the scope of f(), which thinks it owns the value it received. Then it will be dropped a second time by the owner of the value place is borrowed from, which never got the memo that the value it thinks it owns is actually garbage because it was already dropped. This can even be reproduced in the playground where a simple panic!() in the closure results in a double free.

    For this reason an implementation of map_in_place would itself have to be marked unsafe, with a safety contract that f() not panic. But since pretty much anything in Rust can panic (e.g. any slice access), it would be hard to ensure that safety contract and the function would be somewhat of a footgun.

    The replace_with crate does offer such functionality, with several recovery options in case of panic. Judging by the documentation, the authors are keenly aware of the panic issue, so if you really need that functionality, that might be a good place to get it from.