Changing Rust Enum Variant with Mutable Reference
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.