What happens when moving a boxed array?

Just to understand fundamentals a bit better... not really a practical question.

Considering this:

fn main() {
    let my_array:Box<[i32;5]> = Box::new([1,2,3,4,5]);
    
    print_vals_move(my_array);
}

fn print_vals_move(vals:Box<[i32]>) {
    for value in vals.iter() {
        println!("{}", value);
    }
}

It seems that everything is allocated on the stack... but then the box is moved, and... pointing at the "previous" stack?

I guess that's really my point of confusion - I'd want to say each scope gets its own amount of memory ("stack") - and then releases it when the scope ends?

I guess that's why the above is okay - even though it's jumping into another function, the parent function is still in scope and so it's okay for the moved Box to point at memory in the parent function?

More generally - any thoughts on this are appreciated. Thanks!

Box stores its content on the heap. The original [1,2,3,4,5] value was indeed on the stack, but Box::new moved it to the heap. Only the box itself (basically a pointer) remains stored on the stack and is moved into the function argument.

1 Like

Thanks! If T in Box<T> is Copy will it copy it instead of move?

When T : Copy the data in the heap can indeed be copied elsewhere instead of moved, meaning that the Box<_> variable holding the source data remains usable.

  • Example
    let my_box: Box<i32> = Box::new(42); // i32 : Copy
    // dereference-read the `my_box` pointer to copy the data in the heap
    // (number 42) into a new stack variable, `forty_two`
    let forty_two: i32 = *my_box;
    dbg!(my_box); // `my_box` has not been moved out.
    

However, even when T : Copy, Box<T> cannot be Copy! Box<T> is a pointer that owns the data it points to. Meaning that it is responsible for its deallocation. When this pointer goes out of scope, it is dropped, deallocating the memory it pointed to. If another such pointer existed, it would lead to a use-after-free bug and vulnerability (for instance, a double-free).

  • Example

    let my_box: Box<i32> = Box::new(42); // i32 : Copy
    // move the `my_box` pointer (that was living in the stack) into a new stack variable, `forty_two`
    let forty_two: Box<i32> = my_box;
    dbg!(my_box); // ERROR: `my_box` has been moved out.
    
  • You can "copy" the pointer under the promise that during the time span where that "copy" is used, the initial Box<T> remains untouched (and undropped), by borrowing (in a shared/aliasable fashion with a & _ reference, or with an unique pointer to the pointed-to data, with a &mut _ reference):

    • shared reference(s)

      let my_box: Box<i32> = Box::new(42);
      println!("Address of heap-allocated 42: {:p}", my_box); // 0x55ed39093a40
      {
          // forty_two points to the same address as `my_box` does.
          let forty_two: &i32 = &*my_box;
          // shared references can be copied
          let another_forty_two: &i32 = forty_two;
          println!("Address forty_two points to: {:p}", forty_two); // 0x55ed39093a40
          println!("Address another_forty_two points to: {:p}", another_forty_two); // 0x55ed39093a40
      }
      // only now can we drop `my_box`
      // ::core::mem::drop(my_box);
      
    • unique reference

      // the binding must be mut-annotated to enable taking unique references out of it.
      let mut my_box: Box<i32> = Box::new(42);
      println!("Address of heap-allocated 42: {:p}", my_box);
      {
          // forty_two points to the same address as `my_box` does.
          let forty_two: &mut i32 = &mut *my_box;
          // a unique reference cannot be copied
          let another_forty_two: &mut i32 = forty_two; // ERROR
          println!("Address forty_two points to: {:p}", forty_two);
          println!("Address another_forty_two points to: {:p}", another_forty_two);
      }
      // only now can we drop `my_box`
      // ::core::mem::drop(my_box);
      

On the other hand, when T : Copy (actually, it suffices that T be Clone), Box<T> : Clone, meaning it can be cloned: a new chunk of heap data of the same size is allocated, where the source pointed-to memory is copied to, and a pointer to that other chunk of data is returned (resulting in two owners of two different heap-allocated chunks of memory):

  • Example
    let my_box: Box<i32> = Box::new(42);
    println!("my_box points to {:p}", my_box); // 0x56232c504a40
    let cloned_box: Box<i32> = my_box.clone();
    println!("cloned_box points to {:p}", cloned_box); // 0x56232c504ba0
    
2 Likes

Great, thanks!

Hmm... but if I create types that are Clone (or Copy, doesn't seem like Box.clone() is cloning it...

#[derive(Clone)]
struct Cloneable {}

#[derive(Copy, Clone)]
struct Copyable {}

fn main()
{
    println!("Cloneable: {}", did_clone(Cloneable{})); //false
    println!("Copyable: {}", did_clone(Copyable{})); //false
    println!("u32: {}", did_clone(42u32)); //true 
    println!("String: {}", did_clone(String::from("hello world"))); //true
    println!("str: {}", did_clone("hello world")); //true
}

fn did_clone<T>(val:T) -> bool 
where T: Clone
{
    let my_box: Box<T> = Box::new(val);
    let cloned_box: Box<T> = my_box.clone();
    
    let s1 = format!("{:p}", my_box);
    let s2 = format!("{:p}", cloned_box);
    
    s1 != s2
}

I've seen it come up a few times... not to think of mut necessarily as "mutable" but also as exclusive... is that the same idea you're conveying here?

2 Likes

Your example involves a special case: zero-sized types, which are not allocated (there is no actual data to allocate), so Box::new(zero_sized_thingy) will actually not allocate (nor deallocate), and will always return a dummy address (currently equal to the alignment of the type, 1 for a zero-sized type unless told otherwise). In that case Box::clone will under the hood just copy the dummy address, but the general semantics remain the same.

Yes, (safe) mutation is earned for free from the uniqueness / non-aliasing guarantee. The very definition of a &mut _ reference is that of a reference that is guaranteed to be unique, vs, a & _ reference, which is that of a (potentially) aliased / shared reference.

In practice, it does boil down to "a read-write reference (allows mutation)" vs "a read-only reference", but it is good to keep the real definitions in mind, when trying to understand some compilation errors.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.