How can I reuse a box that I have moved the value out of?
Asked Answered
N

3

12

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?

Nameplate answered 15/7, 2016 at 13:17 Comment(3)
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.Imbecile
I was going to answer the first part but realised that I don't yet understand how moving out of Box<T> works; it doesn't seem to be related to Deref or DerefMut traits. So looking forward to a good answer too!Imbecile
@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.Nameplate
N
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 })
}
Naomanaomi answered 15/7, 2016 at 16:24 Comment(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<Option<Foo>> would be useful as then you can call take and None is the cheap-to-create default.Shop
@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.Naomanaomi
S
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.

Stoic answered 15/7, 2016 at 14:23 Comment(2)
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 .Imbecile
@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 as well.Shop
Z
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.

Zed answered 15/7, 2016 at 14:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.