How to take the ownership of an enum variant and quickly put the converted variant back via `&mut`

#[derive(Debug)]
struct A(Inner, ());
impl A {
    fn into_b(self) -> B {
        B(self.0)
    }
}

#[derive(Debug)]
struct B(Inner);
#[derive(Debug)]
struct Inner; // Non-copy

#[derive(Debug)] // Non-copy
enum C {
    A(A),
    B(B)
}

impl C {
    fn a_to_b(&mut self) {
        // error: cannot move out of `self`
        *self = match *self {
            C::A(a) => C::B(a.into_b()),
            _ => *self
        };
    }
    
    fn into_b(self) -> C {
        match self {
            C::A(a) => C::B(a.into_b()),
            _ => self
        }
    }
}

fn main() {
    let mut c = C::A(A(Inner, ()));
    dbg!(&c);
    
    // works
    c = match c {
        C::A(a) => C::B(a.into_b()),
        _ => c
    };
    dbg!(&c);
    
    // works
    c = c.into_b();
    dbg!(&c);
}

The obvious fix as shown above is to consume Self and return Self.
But consider if you have a sorted Vec<C>, and when you want to mutate the state from A to B, by consuming Self, you have to

  • remove the specified C from Vec to get the owership
  • consume and convert it
  • append the result
  • sort the Vec to retain previous order in elements

If the conversion can be done via &mut self, we reduce the above steps to one step...

Oh, I think Vec::swap_remove is the solution? Steps become

  • swap the specified element to last position
  • pop off from Vec to get the owership
  • consume and convert it
  • append the result
  • swap back

The swap_remove trick is a good one if you have a Vec. If you don't, you can do something similar with mem::take or mem::replace, storing a temporary value while you modify the original:

impl Default for C { /* somehow... */ }

impl C {
    fn a_to_b(&mut self) {
        *self = match mem::take(self) {
            C::A(a) => C::B(a.into_b()),
            c => c
        }
    }
}

If the conversion starts to get a bit complicated, I usually add a Poison variant to the enum to serve as the temporary value. That way, if the conversion code panics there's an obviously-wrong value stored instead of one that looks valid.

4 Likes

Thanks for the suggestions.

  • Default for C is like Option + C, because
    • I need each variant in C to return its valid PkgKey, if there is a default variant value, the key points to an empty (or nonexistent) pkg name in PkgKey which is an invalid state, so this would make C::get_pkg_key returns Option<&PkgKey>
    • so I have to be careful to generate a default value if I keep get_pkg_key returning &PkgKey ...
  • an extra Poison variant is likewise: it'll return a None for PkgKey...

Well, I think for my code, I can use replace + C::empty_state() instead of take + Default to do it and express the intent right.

1 Like

The only way to observe a Poison variant should be if a panic has already occurred, either in another thread or stopped by catch_unwind. Given that, it's reasonable to still produce an unwrapped &PkgKey and re-panic on poison (though an additional try_* method that returns Result never hurts).

1 Like

The other place you could possibly see Poison is in Drop, but if you don't need to impl Drop then it doesn't matter, and if you do, seeing Poison would probably mean immediately returning.

It's important to understand that whatever placeholder value you use, it will only exist between the mem::take or mem::replace, and the following *self = assignment. For simple things, the compiler should be able to make it disappear entirely. It's only there so the compiler can be sure that &mut self always has a valid value.

3 Likes

There's a crate for this: replace_with - Rust with important pros and cons in the crate docs.

4 Likes