Is it possible to switch variants in the value of

2019-04-24 10:23发布

Is it possible to switch variants in the value of a mutable reference (&mut E<T>) without additional constraints on T, and without resorting to unsafe code?

That is, given an enum:

enum E<T> {
    VariantA(T),
    VariantB(T)
}

What is the correct way of writing this:

let x: E<???> = E::VariantA(??);
change_to_variant_b(&mut x);
assert_eq!(x, E::VariantB(??));

标签: enums rust
3条回答
疯言疯语
2楼-- · 2019-04-24 10:47

As an alternative for this particular case, you could consider changing to a struct with a T and a plain enum:

struct Outer<T> {
    val: T,
    kind: Inner,
}

impl<T> Outer<T> {
    fn promote_to_b(&mut self) {
        self.kind.promote_to_b()
    }
}

enum Inner {
    VariantA,
    VariantB,
}

impl Inner {
    fn promote_to_b(&mut self) {
        if let Inner::VariantA = *self {
            *self = Inner::VariantB;
        }
    }
}
查看更多
Summer. ? 凉城
3楼-- · 2019-04-24 10:54

is it possible to switch variants in the value of a mutable reference

As Matthieu M. said, the general answer is "no". The reason is that doing so would leave the enum in an indeterminate state, which would allow accessing undefined memory, which would allow breaking Rust's safety guarantees. As an example, let's pretend that this code compiled without error:

impl<T> E<T> {
    fn promote_to_b(&mut self)  {
        if let E::VariantA(val) = *self {
            // Things happen
            *self = E::VariantB(val);
        }
    }
}

The problem is once you've moved the value from self into val, what should happen to the memory representing T inside self?

If we copied the bits and then a panic occurred in "Things happen", the destructor for both val and the T inside self would run, but since these point at the same data, this would lead to a double free.

If we didn't copy the bits, then you couldn't safely access the val inside "Things happen", which would be of marginal usefulness.


The by-value solution works because the compiler can track who is supposed to call the destructor: the function itself. Once you are inside the function, the compiler knows which specific lines might need to free the value and properly calls them in case of panic.

The Clone or Default solution works because you never move the value out of the original enum. Instead, you can replace the original enum with a dummy value and take ownership of the original (using Default) or duplicate the entire original value (using Clone).


The replace_with RFC (#1736) proposed to add a method that would allow this to work while ensuring that proper memory semantics were upheld, but that RFC was not accepted.

查看更多
老娘就宠你
4楼-- · 2019-04-24 11:10

I am going to go on a limb here and say No.


It is possible with just a minor change to the signature though:

fn change_to_variant_b<T>(e: E<T>) -> E<T> {
    match e {
        E::VariantA(t) => E::VariantB(t),
        E::VariantB(t) => E::VariantB(t),
    }
}

It is possible using unsafe:

fn change_to_variant_b<T>(e: &mut E<T>) {
    use std::ptr;

    unsafe {
        match ptr::read(e as *const _) {
            E::VariantA(t) => ptr::write(e as *mut _, E::VariantB(t)),
            E::VariantB(t) => ptr::write(e as *mut _, E::VariantB(t)),
        }
    }
}

It is possible with additional bounds (Default, or Clone):

fn change_to_variant_b<T: Default>(e: &mut E<T>) {
    match std::mem::replace(e, E::VariantA(T::default())) {
        E::VariantA(t) => e = E::VariantB(t),
        E::VariantB(t) => e = E::VariantB(t),
    }
}
查看更多
登录 后发表回答