Why Rust doesn't support move mutable value away then move other back in place

The following code may cause a compiling error:

let mut a="hello".to_string();
fn foo(s:&mut String){
  drop(*s); // Take the ownership
  *s="world".to_string();
}

In fact, Rust has the ability to detect the movement of the ownership, but now we can only using unsafe to do it.

let mut a="hello".to_string();
fn foo(s:&mut String){
  unsafe{
    drop(std::ptr::read(s)); // Take the ownership
    // If we trying read `s` here it should cause an error
    std::ptr::write(s,"world".to_string());
  }
}

So why Rust doesn't support moving the value of mutable references away, then setting new value to the reference afterwards?

Surely std provided std::mem::replace and std::mem::swap, but sometimes they couldn't help.

Please think about the following example:

enum Foo{
  Item1(String),
  Item2(String)
}

fn convert_to_item2(foo:&mut Foo){
  match foo{
    Foo::Item2(_)=>{},
    Foo::Item1(s)=>{
      // How to change `foo` to `Foo::Item2` by taking `s`?
      todo!();
    }
  }
}

And this one:

struct Bar{
  a:String,
  b:String
}

fn swap_a_and_b(bar:&mut Bar){
  // How to swap field `a` and field `b` of `bar`?
  todo!();
}

The swap_a_and_b case is easy.

struct Bar {
    a: String,
    b: String,
}

fn swap_a_and_b(bar: &mut Bar) {
    std::mem::swap(&mut bar.a, &mut bar.b);
}

It's true that the enum case is more annoying, but there are essentially two ways:

// Take ownership of string by replacing it with an empty string.
fn convert_to_item2(foo: &mut Foo) {
    match foo {
        Foo::Item2(_) => {}
        Foo::Item1(s) => {
            let s = std::mem::take(s);
            *foo = Foo::Item2(s);
        }
    }
}
// Replace foo with dummy value temporarily.
fn convert_to_item2(foo: &mut Foo) {
    let dummy_value = Foo::Item1(String::new());
    match std::mem::replace(foo, dummy_value) {
        Foo::Item2(s) => {
            *foo = Foo::Item2(s);
        }
        Foo::Item1(s) => {
            *foo = Foo::Item2(s);
        }
    }
}

The reason it's annoying is that the compiler is worried about what happens if your program panics while you are replacing foo with a new value. If you have taken ownership of something without placing something else there and then panic, then the caller could catch the panic and observe foo having an invalid value.

14 Likes

Here's a blog post about the pattern, including possible future directions for Rust and the challenges they present: Moving from borrowed data and the sentinel pattern

But in this case it is String, what if it is other struct that doesn't implements Default or it is costly to create new one?

The article I mentioned gives some possibilities:

  • Use an Option
  • Wrap an Option in a newtype that implements DerefMut<Target=T> and panics on None
  • Use the take_mut crate
3 Likes

Another possibility is to create an extra case in your enum with no fields for use as the dummy value.

1 Like

This exact same question was asked a week ago. Please use the search.

I don't think it's the exact same question, as this one also asks about transitioning between enum variants.

If so, if I can guarantee it won't panics before setting value, using std::ptr::read and std::ptr::write is not a bad idea :thinking:

1 Like

In Rust unsafe is a perfectly valid tool, but it, basically, says to the compiler trust me, I know what I'm doing.
This makes everyone who reads your code a tiny bit nervous (now we need to walk the tightrope without safety belt), but sometimes it is the best solution.
This being said I would rather look on pure safe solution on playground or godbolt and would only reach out to unsafe if I can see that safe solution is ineffective.

5 Likes

Well, that's basically take_mut without the guard rails self-destruct. (If you're wrong, you have UB. With take_mut, you abort instead.)

5 Likes

There’s also replace_with which has a potentially more efficient implementation and nicer defaults, compared to take_mut. (It’s very explicit about the aborts; by default, it takes a closure that’s used to create a dummy value in case of panic.)

4 Likes

Well, the gist of the question is the same. It doesn't matter much whether you want to move out of a reference because you want to take the value of an enum or that of any other type.

This blog is vivid, but I still cannot get some points.

Firstly, won't a panic abort a thread immediately without doing anything else except printing panic information? If it is, why there is destruct runs after panics?

Secondly, since a panic can roughly understood as the panicked thread stop executing its task at one point permanently, other threads should not be able to access to the uninitialized memory via unreleased sync structs like Mutex, otherwise although there is no panic, there is also data race exists.

In general, I mean, I don't exactly know in what case we need to read that moved value after a panic.

The default behavior on most platforms is to unwind. This calls destructors, and can even be caught.

1 Like

Here's an example of examining some data during an unwinding panic. (If you're wondering why things unwind, it's so (a) destructors can do things like flush and close your files, leave the database in a sane state, etc, and (b) so they can be caught and recovered from in certain situations (e.g. async uses this a lot).)

The stdlib Mutex uses poisoning, so you'll know if a panic happened while it was locked on some other thread. But you can still get at the data using safe code.

Other Mutexes, like in the popular parking_lot, do simply unlock the Mutex on panic.

This happens when the guards are dropped (another use case for unwinding). Because the guard has been dropped, there is no data race if you obtain (guarded) access elsewhere.

The point is that reading the fields of the struct is possible in safe code, e.g. in the destructor or by something that caught the panic, another thread via Mutex, etc. So other safe code can't leave your struct in a partially uninitialized state, as that would lead to UB if the uninitialized field was read.

1 Like

no it won't. resource leaks are considered an error and the language makes a best-effort attempt to prevent them.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.