Changing Rust Enum Variant with Mutable Reference

Feb 14, 2024·
Zhiyao Ma
· 3 min read

Consider the following Coin type that has two variants Front and Back, each attached with a variable of type T.

enum Coin<T> {
    Front(T),
    Back(T),
}

Given a mut reference to Coin, how can we change it from Front to Back or vice versa? Let us begin with a naive attempt to implement turn_to_front, which changes Back to Front if it is not already Front.

impl<T> Coin<T> {
    fn turn_to_front(&mut self) {
        match self {
            Self::Front(_) => return,
            Self::Back(val) => {
                *self = Self::Front(*val);
            }
        }
    }
}

The compiler, however, produces an error when we try to compile the code above:

error[E0507]: cannot move out of `*val` which is behind a mutable reference

More specifically, the naive code above attempts to create an invalid state of Coin through the *val expression. Let us zoom in the non-trivial code branch

Self::Back(val) => {
    *self = Self::Front(*val);
}

The single-line statement is equivalent to the following two-line statements. If the code compiles, then the Coin variable referenced by self would be left in an invalid state for some time. Specifically, the invalidity comes from the fact that self continues to be the Back variant, yet the variable attached to Back is no longer valid because of the move.

let tmp = *val;
// If the above statement compiles, then `self` will be invalid until the
// next statement is executed.
*self = Self::Front(tmp);

A reddit post attempts to circumvent the compiler’s check with unsafe code, however, as other replies have pointed out, it is impossible to do it safely.

A straightforward correct solution is to wrap T with Option.

enum Coin<T> {
    Front(Option<T>),
    Back(Option<T>),
}

impl<T> Coin<T> {
    fn turn_to_front(&mut self) {
        match self {
            Self::Front(_) => return,
            Self::Back(opt) => {
                let val = opt.take();
                *self = Self::Front(val);
            }
        }
    }
}

However, wrapping T with Option might be suboptimal because every access to T must go through an .unwrap() of the Option, degrading the runtime performance. Also, the size of the resulting Coin type can be larger if niche optimization is not applicable to Option<T>.

An alternative better approach is based on the following insight: We should convey an explicit variant to the compiler, which represents the state when enum Coin is not owning the T during the transition from Back to Front.

enum Coin<T> {
    Front(T),
    Back(T),
    Undef,
}

impl<T> Coin<T> {
    fn turn_to_front(&mut self) {
        match self {
            Self::Front(_) => return,
            Self::Back(_) => {
                let mut tmp = Self::Undef;

                // Make `self` to be `Undef`.
                // `tmp` holds the previous value.
                core::mem::swap(self, &mut tmp);

                // `tmp` must be the `Back` variant.
                if let Self::Back(val) = tmp {
                    *self = Self::Front(val);
                }
            }
            Self::Undef => panic!(),
        }
    }
}

This approach avoids wrapping T, nor does it increase the size of Coin, since the Front or Back variant has larger size than Undef.

The idea has also been discovered on the Rust forum.