11

I have some non-copyable type and a function that consumes and (maybe) produces it:

type Foo = Vec<u8>;

fn quux(_: Foo) -> Option<Foo> {
    Some(Vec::new())
}

Now consider a type that is somehow conceptually very similar to Box:

struct NotBox<T> {
    contents: T
}

We can write a function that temporarily moves out contents of the NotBox and puts something back in before returning it:

fn bar(mut notbox: NotBox<Foo>) -> Option<NotBox<Foo>> {
    let foo = notbox.contents; // now `notbox` is "empty"
    match quux(foo) {
        Some(new_foo) => {
            notbox.contents = new_foo; // we put something back in
            Some(notbox)
        }
        None => None
    }
}

I want to write an analogous function that works with Boxes but the compiler does not like it:

fn baz(mut abox: Box<Foo>) -> Option<Box<Foo>> {
    let foo = *abox; // now `abox` is "empty"
    match quux(foo) {
        Some(new_foo) => {
            *abox = new_foo; // error: use of moved value: `abox`
            Some(abox)
        }
        None => None
    }
}

I could return Some(Box::new(new_foo)) instead but that performs unnecessary allocation - I already have some memory at my disposal! Is it possible to avoid that?

I would also like to get rid of the match statements but again the compiler is not happy with it (even for the NotBox version):

fn bar(mut notbox: NotBox<Foo>) -> Option<NotBox<Foo>> {
    let foo = notbox.contents;
    quux(foo).map(|new_foo| {
        notbox.contents = new_foo; // error: capture of partially moved value: `notbox`
        notbox
    })
}

Is it possible to work around that?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Pan Hania
  • 468
  • 4
  • 11
  • 4
    You seem to be asking more than one question here. The match vs map one looks like it should be move to a new question. – Chris Emerson Jul 15 '16 at 13:39
  • I was going to answer the first part but realised that I don't yet understand how moving out of `Box` works; it doesn't seem to be related to `Deref` or `DerefMut` traits. So looking forward to a good answer too! – Chris Emerson Jul 15 '16 at 13:42
  • @ChrisEmerson The match part was something that popped up when I was trying to create the minimal example of my problem so I haven't put much research into it. I just thought that this is probably related to the general question and the fact that I don't get how "partial" moves work so I left it here. – Pan Hania Jul 16 '16 at 11:59

3 Answers3

13

So, moving out of a Box is a special case... now what?

The std::mem module presents a number of safe functions to move values around, without poking holes (!) into the memory safety of Rust. Of interest here are swap and replace:

pub fn replace<T>(dest: &mut T, src: T) -> T

Which we can use like so:

fn baz(mut abox: Box<Foo>) -> Option<Box<Foo>> {
    let foo = std::mem::replace(&mut *abox, Foo::default());

    match quux(foo) {
        Some(new_foo) => {
            *abox = new_foo;
            Some(abox)
        }
        None => None
    }
}

It also helps in the map case, because it does not borrow the Box:

fn baz(mut abox: Box<Foo>) -> Option<Box<Foo>> {
    let foo = std::mem::replace(&mut *abox, Foo::default());

    quux(foo).map(|new_foo| { *abox = new_foo; abox })
}
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 2
    The downside is that `Foo:default()` needs to exist and hopefully be cheap to execute. If neither is true, then the suggestion to switch to a `Box – Shepmaster Jul 15 '16 at 16:27
  • @Shepmaster: Agreed :) However I'd rather stick to the presented scenario and only elaborate if the OP has more specific concerns, otherwise I am afraid the answer might drown anyone attempting to read it. – Matthieu M. Jul 15 '16 at 16:29
5

Moving out of boxes is special-cased in the compiler. You can move something out of them, but you can't move something back in, because the act of moving out also deallocates. You can do something silly with std::ptr::write, std::ptr::read, and std::ptr::replace, but it's hard to get it right, because something valid should be inside a Box when it is dropped. I would recommend just accepting the allocation, or switching to a Box<Option<Foo>> instead.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Thiez
  • 177
  • 1
  • 9
  • 1
    My biggest annoyance with Rust is that some details like this apparently aren't documented outside of discussion threads or sometimes in passing in RFCs. :-( FWIW, it's mentioned [in this postponed RFC](https://github.com/rust-lang/rfcs/pull/178) . – Chris Emerson Jul 15 '16 at 15:55
  • 2
    @ChrisEmerson i think in this case, the special casing of `Box` is something that wants to be removed and transformed to the proposed `DerefMove` trait. Don't want to show too much dirty laundry and have it be widley internalized if it can become more reusable and generic. Note that the [RFC has been restarted](https://github.com/rust-lang/rfcs/pull/1646) as well. – Shepmaster Jul 15 '16 at 16:00
0

We can write a function that temporarily moves out contents of the NotBox and puts something back in before returning it

That's because you can partially move out from the struct that you take by value. It behaves as if all fields were separate variables. That is not possible though if the struct implements Drop, because drop needs the whole struct to be valid, always (in case of panic).

As for providing workaround, you haven't provided enough information – especially, why baz needs to take Box as an argument and why quux can't? Which functions are yours and which are part of an API you can't change? What is the real type of Foo? Is it big?

The best workaround would be not to use Box at all.

krdln
  • 1,009
  • 8
  • 12