Move out of mutable reference of variant and immediately reinitialize mutable reference

struct D; // !Copy + !Clone + !Default

enum E { // Can't modify this enum to add dummy variant

fn test(a: &mut E) {
    match a {
        E::A(v) => {
            *a = E::B(*v)
        E::B(v) => {
            *a = E::C(*v)
        E::C(v) => {
            *a = E::A(*v)

Compiler output:

error[E0507]: cannot move out of `*v` which is behind a mutable reference
  --> src/
12 |             *a = E::B(*v)
   |                       ^^ move occurs because `*v` has type `D`, which does not implement the `Copy` trait

I am trying to implement some kind of finite-state-machine using pattern-matching and an enum, however I am not really sure how to do the transition efficiently and completely with safe code.

The thing is, I've though about using std::mem::replace, but this is not possible because I won't be able to create a D object, or have a special dummy variant which I can use as replacement, and swap the new variant back.

Isn't Rust able to tell (through some kind of dataflow analysis) that moving D out of v is valid, as long as I assign a new variant to a?

1 Like

You can use the take_mut crate, which uses the unsafe std::ptr::read and std::ptr::write functions to take the old value and replace it with the new one.

The tricky part is ensuring memory safety if there is a panic in between these two operations. (take_mut guards against this by aborting on panic.) If you don't run any code that can panic in between the read and write, then you can use them directly without such a guard:

use std::ptr::{read, write};

fn test(a: &mut E) {
    unsafe { // safe as long as nothing can panic between `read` and `write`
        let val = match read(a) {
            E::A(v) => E::B(v),
            E::B(v) => E::C(v),
            E::C(v) => E::A(v),
        write(a, val);

Regarding take_mut, it seems that it uses the same underlying idea as your example (plus catch_unwind to catch panics).

Mhhh, imho that doesn't look like an efficient or safe solution at all. Every "transition" would result in an additional write (I would imagine, that I would be able to "break out" of a transition and keep the variant state) and maintaining that safety invariant is really a burden.

I was really hoping the Rust compiler could help here somehow :confused:.

@mbrubeck Well, that's weird, I thought that I've just seen a response from you. :smiley:

I wonder whether it would be possible for the compiler to actually reason about code between a read and a possible write and decide if the code in between can panic or not, in the latter case it would generate code as take_mut would generate without the catch_panic part? and in the former case issue the old compiler error. @mbrubeck?

I deleted my previous response because I had second thoughts about some unsafe code in it. :slight_smile:

It would indeed be interesting if the compiler could allow moving out of &mut if nothing could panic before a new value is moved in. This would be extremely restrictive, however. No calls to any other functions, either within the crate or from a library, would be possible. (This includes operations like indexing, comparison, and arithmetic, which are sugar for trait methods.) Even if one of these can never panic currently, there's no way for the compiler to guarantee that the implementation of that external function won't change to a panicking one in the future.

That's interesting. Excuse my ignorance and curiosity but doesn't something similar already exist in Rust, like the marker traits Send and Sync? I am imagining something like a PanicSafe marker-trait which can be used to "mark" functions or types as "panic-safe".

What marker-trait can guarantee that a hardware fault while the thread is executing won't trigger a panic? (Added note: Hardware faults ultimately traceable to cosmic rays are inevitable.)

Well, a hardware fault is a hardware fault, which would result in an undefined program state, there is nothing Rust can do here. But aren't we specifically talking about actual panics, which were issued by code? (think of panic!(), ...). Such code can be "marked" with a marker trait "just like" Sync and Send. So we uphold at least the invariant that the take_mut part "should" always succeed as long as the code does not panic which the compiler "could" prove.

If this is a state machine and you always have that piece of data, why don't you just store it separately? Could you create a struct with two fields of type D and E, respectively, where variants of E no longer contain a D?

Of course that would be possible but it's not an elegant solution and doesn't really always work. I've seen this pattern several times now and it's pretty annoying to always have to "workaround" the actual problem (With unsafe code, take_mut, mem::replace or even just a re-design by for example using an Option).

1 Like

I'm afraid you'll need to elaborate on that. In my opinion, if you have the same piece of associated data, then it's much more elegant to factor it out in a single field than making each variant carry the same redundant baggage. I have encountered several examples of finite automata implemented in Rust and the question of state transitions doesn't seem to be a hard or unsuitable problem. So to sum up, why is factoring out the common field not elegant or infeasible in your use case? What are you trying to do, more specifically?

But what if they aren't the same? My examples above were simplified to explain this issue. The original (and more complex?) case contains several "conditional" transformations which depend on the data stored in the variant. Also sometimes the lifetime of the data stored in the variant is only bound to several enum variants which would make it "impossible" to factor out the data (Another way would be a factored Option, however that would unnecessarily increase the size of the enum and code complexity).

Do you need to use a mutable reference to transition the state? fn test(a: E) -> E may or may not be efficient, depending on the actual types used.

At least in the case of the contrived example, both mbrubeck's function based on take_mut and the move-only implementation generates roughly the same code:

Uncomment the struct fields to add a little flavor, and: The safe version is actually smaller and contains no calls (vs one call in the take_mut version).

YMMV, but there you have it.

1 Like

Then I don't see how you can expect it to function with zero work, because you'd have to perform some sort of conversion anyway. TBH this all seems like premature optimization here.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.