27

I want to update an enum variant while moving a field of the old variant to the new one without any cloning:

enum X {
    X1(String),
    X2(String),
}

fn increment_x(x: &mut X) {
    *x = match *x {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    }
}

This doesn't work because we can't move s from &mut X:

error[E0507]: cannot move out of borrowed content
 --> src/lib.rs:7:16
  |
7 |     *x = match *x {
  |                ^^
  |                |
  |                cannot move out of borrowed content
  |                help: consider removing the `*`: `x`
8 |         X::X1(s) => X::X2(s),
  |               - data moved here
9 |         X::X2(s) => X::X1(s),
  |               - ...and here

Please don't suggest things like implementing an enum X { X1, X2 } and using struct S { variant: X, str: String } etc. This is a simplified example, imagine having lots of other fields in variants, and wanting to move one field from one variant to another.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
sinan
  • 6,809
  • 6
  • 38
  • 67
  • 3
    In the case of `String`, you can `mem::replace` an empty string into the field, and use the result to form the new variant. Just a few moves. But this only works for types that have a cheap form like empty strings. – Sebastian Redl Apr 11 '16 at 19:28
  • 1
    FYI, I have [proposed `std::mem::replace_with`](https://github.com/rust-lang/rfcs/pull/2490#issuecomment-459448752), which should help in situations like this. – Vlad Frolov Jan 31 '19 at 18:29

4 Answers4

28

This doesn't work because we can't move s from &mut X.

Then don't do that... take the struct by value and return a new one:

enum X {
    X1(String),
    X2(String),
}

fn increment_x(x: X) -> X {
    match x {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    }
}

Ultimately, the compiler is protecting you because if you could move the string out of the enumeration, then it would be in some half-constructed state. Who would be responsible for freeing the string if the function were to panic at that exact moment? Should it free the string in the enum or the string in the local variable? It can't be both as a double-free is a memory-safety issue.

If you had to implement it on a mutable reference, you could store a dummy value in there temporarily:

use std::mem;

fn increment_x_inline(x: &mut X) {
    let old = mem::replace(x, X::X1(String::new()));
    *x = increment_x(old);
}

Creating an empty String isn't too bad (it's just a few pointers, no heap allocation), but it's not always possible. In that case, you can use Option:

fn increment_x_inline(x: &mut Option<X>) {
    let old = x.take();
    *x = old.map(increment_x);
}

See also:

Alex Petrosyan
  • 473
  • 2
  • 20
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 2
    This answer is really comprehensive ! I would add this link though: https://rust-unofficial.github.io/patterns/idioms/mem-replace.html . It is a common design pattern I think, similar to using Option::take for your custom enum. – rambi Nov 23 '21 at 19:33
5

If you want to do this without moving out of the value in a zero-cost way, you have to resort to a bit of unsafe code (AFAIK):

use std::mem;

#[derive(Debug)]
enum X {
    X1(String),
    X2(String),
}

fn increment_x(x: &mut X) {
    let interim = unsafe { mem::uninitialized() };
    let prev = mem::replace(x, interim);
    let next = match prev {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    };
    let interim = mem::replace(x, next);
    mem::forget(interim); // Important! interim was never initialized
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
kralyk
  • 4,249
  • 1
  • 32
  • 34
  • 1
    You have to be **very careful** here. Any panic that happens between `mem::uninitialized()` and `mem::forget` will allow the uninitialized value to leak out to the rest of the program. In this example, *I* don't see such a possibility. However, it's very likely that someone modifies the example to be able to call code that can panic. FWIW, I'd have [extracted the second `mem::replace`](https://play.integer32.com/?gist=46b4233e57b243819bf0ef2bd1cda799&version=stable). – Shepmaster Jul 15 '17 at 15:00
  • Well obviously you need to be very careful with `unsafe` code no matter what. I needed to do this in a performance-critical path so `unsafe` was the way to go. If someone wants to insert some non-trivial / potentially panicking code in there, they should make a "forgetting object", ie. one that stores the interim object and forgets it on `drop()`. – kralyk Jul 17 '17 at 11:10
  • 2
    While I completely agree that one should always be very careful with unsafe, this specific use of `uninitialized` gets compiled out in release mode and the "toggle" operation gets applied in-place. Yet, I would love to see a safe implementation for this intent, which is why I [proposed `std::mem::replace_with`](https://github.com/rust-lang/rfcs/pull/2490#issuecomment-459448752). – Vlad Frolov Jan 31 '19 at 18:34
  • I would be very careful with this. See the warnings on `mem::uninitialized` on why this causes **immediate undefined behavior** in pretty much all cases. Future LLVM optimization passes might destroy your program. – WorldSEnder Jan 05 '22 at 17:42
  • 1
    [An updated version](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=93741a9d9e3018e862e25e57f4c07f78), using `MaybeUninit`. Note that this version passes miri, while the one in the answer doesn't. – WorldSEnder Jan 05 '22 at 17:52
  • A better way which does not involve using uninitialized values is to use `mem::ManuallyDrop` for fields which then you can `unsafe { ManuallyDrop::take(field) }` as long as you won't use that reference before reassignment. – Amin Sameti Aug 21 '23 at 17:52
2

In some particular cases, what you want is in fact std::rc

enum X {
    X1(Rc<String>),
    X2(Rc<String>),
}

fn increment_x(x: &mut X) -> X {
    match x {
        X::X1(s) => {x = X::X2(s.clone())},
        X::X2(s) => {x = X::X1(s.clone())},
    }
}
rambi
  • 1,013
  • 1
  • 7
  • 25
0

As @VladFrolov has commented, there was an RFC proposed that would've added a method to the standard library, std::mem::replace_with, that would allow you to temporarily take ownership of the value behind a mutable reference. However, it was not accepted.

There are third-party crates that provide similar functionality: take-mut and replace-with being notable ones I'm aware of. By reading the documentation though, you might be able to see why this was not accepted into the standard library. There are potentially dire consequences of aborting the program if the function that was given ownership panics, since it is required to put some value back into the mutable reference before unwinding can continue. Mechanisms other than panicking are available to "put back" a value.

Here is an example using replace-with:

enum X {
    X1(String),
    X2(String),
}

fn increment_x(x: &mut X) {
    replace_with::replace_with_or_abort(x, |x| match x {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    });
}
kmdreko
  • 42,554
  • 6
  • 57
  • 106