Is unboxed value recreated on the stack?

The following code is copied from Rust by Example

use std::mem;

#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Point {
    x: f64,
    y: f64,
}

fn origin() -> Point {
    Point { x: 0.0, y: 0.0 }
}

fn boxed_origin() -> Box<Point> {
    // Allocate this point on the heap, and return a pointer to it
    Box::new(Point { x: 0.0, y: 0.0 })
}

fn main() {
    // The output of functions can be boxed
    let boxed_point: Box<Point> = Box::new(origin());

    // Double indirection
    let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());

    // box size == pointer size
    println!("Boxed point occupies {} bytes on the stack",
             mem::size_of_val(&boxed_point));
    let unboxed_point: Point = *boxed_point;
    println!("Unboxed point occupies {} bytes on the stack",
             mem::size_of_val(&unboxed_point));
}

I deleted the derived Copy trait for Point so that let unboxed_point: Point = *boxed_point; will move the point out of the box instead of copy it. I wonder what's the behaviour of this moving? Will the destructor be called on the point while a new point is allocated on the stack or will the heap point stay there and there is a pointer maintained which points to the heap point? Based on the output, I think the first one is true and I just need a confirmation.

A value is only dropped once, so moving will not cause a call to Drop::drop on the previous owner, it is simply invalidated so you can't use it after ownership has been transferred. Only after the new owner goes out of scope will the value be dropped (if not moved again). Moves are just shallow memcopies that potentially are optimized away.

1 Like

So it will just remain in the heap, won't it? But why is the size 16 bytes, not that of a pointer, considering it now should be a pointer pointing to the heap point? I thought a let statement in rust means creating a stack variable, if no Box or something like that is involved

a Point is a value containing two 8-byte floats, hence, 16 bytes. There are no implicit pointers involved.

1 Like

Note that Box’s behavior is slightly magical, in that

let unboxed_point: Point = *boxed_point;

will move the Point out of the Box<Point>, so that a sort-of empty Box remains. The compiler emits code that makes sure that, at the end of the scope, only the heap memory of the Box is deallocated, but no destructor call (of Point) happens anymore.

So you need to differentiates two parts of all the “destructor” of a Box<Point>, one part is any Drop implementation of Point or its fields, the other is the deallocation of the memory of the Box that was holding the Point struct.

Your example features a struct that doesn’t implement Drop, nor do any of its fields. So really all that happens is the deallocation of the Box.

Types without Box’s “magical” abilities would offer a method in the style of Box::into_inner, which has the signature fn(Box<T>) -> T. If one works with such a method, the heap allocation will be deallocated immediately upon calling boxed_point.into_inner() instead of at the end of the scope.

For instance a type like tropmphej::UniqueArc is very similar to a Box, but can only offer an into_inner method, because the compiler doesn’t support moving out of custom types via dereferencing operator.


In any case, the resulting Point was moved to a new location on the stack. This Point on the stack has nothing to do with the heap location of the original Box<Point>.

For types with some nontrivial ownership & nontrivial destructors, the moved value does retain all owned resources though; e.g. foo: Box<String> and let bar = *foo; will create bar: String that still points to the same heap location for the String’s data.

1 Like

In case you might be wondering what’s the point of this happening only at the end of the scope, two benefits:

  • it’s consistent behavior with how Copy types are treated
  • you are able to still use the Box if you wanted to, by moving back in a new value

Here’s some code demonstrating the second aspect:

let mut boxed_point: Box<Point> = Box::new(origin());
println!("{boxed_point:?}");

let unboxed_point: Point = *boxed_point;
println!("{unboxed_point:?}");

// can't use `boxed_point` here
// println!("{boxed_point:?}"); // error[E0382]: borrow of moved value: `boxed_point`
// note that the error message was slightly lying/oversimplifying:
//     `boxed_point` has not been moved, but `*boxed_point` has been

*boxed_point = unboxed_point;
// but now, it's back!
println!("{boxed_point:?}");

// can't use `unboxed_point` here
// println!("{unboxed_point:?}"); // error[E0382]: borrow of moved value: `unboxed_point`

Rust Playground

For my understanding, do you know why this was done for deref of Box (and only Box) -- why the inconsistent behavior was deemed worth the benefit of not having to call an into_inner fn?

Before Rust reached 1.0, it did not take the approach it does today, where all pointer types except for raw pointers and references were imported from the standard library. Instead, Box<T> and (approximately) Rc<T> had their own language syntax, ~T and @T, and there were some other special rules for them, like allowing @T to be Copy with implicit reference count updates (strange as that sounds today). In that context, it’s not so strange that ~T had special features.

For another example of such specific features, &mut has its own special dereference power: if you have a variable let x: &mut T, then you can mutate *x even though the variable is not declared as mut x. You can't do that with any type that merely implements DerefMut, because DerefMut requires &mut self, i.e. taking a full mutable reference to x.

2 Likes

Thanks! Good to know.

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.