Moving value out of the Box by dereferencing (and what it is desugared into)?

60 Views Asked by At

Consider the following code:

    let x = String::from("123");

    let bx = Box::new(x);

    let dbx = *bx;

    println!("{}", dbx);
    //println!("{}", bx); // error due to ownership move

Here x is created, later boxed into bx, and later bx is somehow unboxed in expression *bx.

As far as I understand, ownership of data is moved from x to bx to dbx, and data itself (at least memory occupied by struct String) is copied from stack (x) to heap (pointed by bx) to stack (dbx).

My main question is: how does compiler interpret *bx?

It does not look like Deref for Box, since this would imply claiming ownersip of value, having reference to value (&String).

It looks like compiler somehow desugars this to into_inner, but it is not obvious to me why.

So, What is going on here?

Bonus question: is there a way to see how code gets desugared, or how do I approach demystifying rust magic in general. Auto dereferencing involving various smart pointers is of particular interest to me now. I tried cargo-inspect but it does not seem to be helpful here.

2

There are 2 best solutions below

0
kmdreko On BEST ANSWER

Normally you are right that * goes through Deref or DerefMut and only has access to &T or &mut T. However, Box gets special treatment and is allowed by the compiler to move out of a dereference.

There is currently no trait that allows one to do that for user-defined types (a DerefMove trait has been proposed but nothing accepted AFAIK). So there is no "desugaring" to be done; just *bx is what you see and what is left is handled by the compiler internally. It really is "magic" in this case.

You can see that it doesn't desugar to into_inner because the current implementation of that is to use * (source):

pub fn into_inner(boxed: Self) -> T {
    *boxed
}

See also:

0
drewtato On

kmdreko's answer covers the language side, but in case you want to know the hardware side, I've made a couple functions that compile to the exact same x86_64 assembly as a dereference. These all read the Box pointer into a stack variable, deallocate the Box without running drop on its contents, and return the stack variable.

use core::mem::ManuallyDrop;

pub fn own_box1(b: Box<String>) -> String {
    *b
}

pub fn own_box2(b: Box<String>) -> String {
    // SAFETY: `ManuallyDrop<String>` has the same layout as `String`.
    let mut b: Box<ManuallyDrop<String>> = unsafe { std::mem::transmute(b) };
    // SAFETY: This `ManuallyDrop` is dropped immediately after.
    let own = unsafe { ManuallyDrop::take(&mut b) };
    own
}

pub fn own_box3(b: Box<String>) -> String {
    let p: *mut String = Box::into_raw(b);
    // SAFETY: `p` will only be used to recreate the box as a `ManuallyDrop` and
    // then dropped.
    let own: String = unsafe { p.read() };
    // SAFETY: `ManuallyDrop<String>` has the same layout as `String`. `p` was just
    // created from `Box::into_raw`. It is immediately dropped.
    let _b = unsafe { Box::from_raw(p.cast::<ManuallyDrop<String>>()) };
    own
}

Playground