0

I have a function that takes ownership of some data, modifies it destructively, and returns it.

fn transform(s: MyData) -> MyData {
    todo!()
}

In some situations, I have a &mut MyData reference. I would like to apply transform to &mut MyData.

fn transform_mut(data_ref: &mut MyData) {
    *data_ref = transform(*data_ref);
}

Rust Playground

However, this causes a compiler error.

error[E0507]: cannot move out of `*data_ref` which is behind a mutable reference
  --> src/lib.rs:10:27
   |
10 |     *data_ref = transform(*data_ref);
   |                           ^^^^^^^^^ move occurs because `*data_ref` has type `MyData`, which does not implement the `Copy` trait

I considered using mem::swap and mem::replace, but they require that you already have some valid value to put into the reference before taking another one out.

Is there any way to accomplish this? MyData doesn't have a sensible default or dummy value to temporarily stash in the reference. It feels like because I have exclusive access the owner shouldn't care about the transformation, but my intuition might be wrong here.

ddulaney
  • 843
  • 7
  • 19

2 Answers2

4

It feels like because I have exclusive access the owner shouldn't care about the transformation, but my intuition might be wrong here.

The problem with this idea is that if this were allowed, and the function transform panicked, there is no longer a valid value in *data_ref, which is visible if the unwind is caught or inside of Drop handling for the memory data_ref points into.

For example, let's implement this in the obvious fashion, by just copying the data out of the referent and back in:

use std::ptr;

fn naive_modify_in_place<T>(place: &mut T, f: fn(T) -> T) {
    let mut value = unsafe { ptr::read(place) };
    value = f(value);
    unsafe { ptr::write(place, value) };
}

fn main() {
    let mut x = Box::new(1234);
    naive_modify_in_place(&mut x, |x| panic!("oops"));
}

If you run this program (Rust Playground link), it will crash with a “double free” error. This is because unwinding from the panicking function dropped its argument, and then unwinding from main dropped x — which is the same box, already deallocated.

There are a couple of crates designed specifically to solve this problem: take_mut, and replace_with intended to improve on it. (I haven't used either, particularly.) Both of these offer two options for dealing with panics:

  1. Force an abort (program immediately exits with no ability to handle the panic or clean up anything else).
  2. Replace the referent of the reference with a different freshly computed value, since the previous one was lost when the panic started.

Of course, if you have no valid alternative value then you can't take option 2. In that case, you might want to consider bypassing this situation entirely by adding a placeholder: if you can store an Option<MyData> and pass an &mut Option<MyData>, then your code can use Option::take to temporarily remove the value and leave None in its place. The None will only ever be visible if there was a panic, and if your code isn't catching panics then it will never matter. But it does mean that every access to the data requires retrieving it from the Option (e.g. using .as_ref().unwrap()).

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
1

You could derive (or impl) Clone for MyData, then pass the clone into your destructive transform.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e3a4d737a700ef6baa35160545f0e514

Cloning removes the problem of the data being behind a reference.

Jeremy Meadows
  • 2,314
  • 1
  • 6
  • 22