Why can Box<T> move the ownership of the inner data?

struct Data{
    i:i32
 }
 struct Wapper{
    data:Data
 }
 fn main(){
    let ptr = Box::new(Wapper{data:Data{i:10}});
    let d = ptr.data;  // #1 ok move the ownership
    let ptr1 = Box::new(Wapper{data:Data{i:10}});
    let d = (*ptr1).data;  // #2 ok move the ownership
    let mut ptr2 = Box::new(Wapper{data:Data{i:10}});
    let d = ptr2.deref_mut().data;  // #3 error
 }

Either #1 or #2 is ok, and they can move the ownership of the inner data data. #3 should have the same meaning as that of #2, that is, *ptr1 is equivalent to ptr2.deref_mut(), however, #3 is an error. For me, there are two issues here.

issue 1:

why does the Box can move the ownership of the inner data?

issue2:

Either #1, #2, or #3, they seem to have the same effect that they can access the inner data since they have implemented the DerefMut trait such that they have the same effect as if they were the reference of Wrapper. Why can two former cases move the ownership but the last one cannot?

Common sense is that any borrowing cannot move the ownership even though it is a mutable borrowing. I don't know what's the magic of the Box such that it can move the ownership of the inner data.

The Box type is special and the reason this is allowed is compiler magic.

2 Likes

Is that to say, we cannot implement ourself smart pointers, which can move the ownership of the inner data as Box can?

Correct.

There have been a couple RFCs to remove the special case (e.g. via a DerefMove trait or &move T reference type), but I don't believe any of them gained enough traction or were able to cover all the edge cases in a backwards compatible way.

Normally, library authors will introduce some sort of fn into_inner(self) -> T method which manually moves the wrapped value out of a smart pointer (e.g. Pin::into_inner()).

1 Like

It appears to me that std::ptr::read is the only way that can move the ownership of the pointed-to value. The same is true for the smart pointer since we always have a raw pointer behind a smart pointer.

Ehh... Kinda, but not really. The std::ptr::read() function doesn't actually "move" anything. It's more like a low-level primitive for reading from and writing to the value behind a pointer.

From further down the std:ptr::read() docs:

Ownership of the Returned Value

read creates a bitwise copy of T, regardless of whether T is Copy. If T is not Copy, using both the returned value and the value at *src can violate memory safety. Note that assigning to *src counts as a use because it will attempt to drop the value at *src.

write() can be used to overwrite data without causing it to be dropped.

You can implement into_inner() on top of std::ptr::read(), but it's up to the author to actually obey move semantics... Which is non-trivial.

use std::{alloc::Layout, mem::ManuallyDrop};

struct MyBox<T>(*mut T);

impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        unsafe {
            let ptr: *mut T = std::alloc::alloc(Layout::new::<T>()).cast();
            ptr.write(value);
            MyBox(ptr)
        }
    }

    fn into_inner(self) -> T {
        // Safety: We put the pointer behind a ManuallyDrop to avoid double
        // frees. This operation logically moves ownership of the T from MyPtr
        // to the caller, so we can't touch it after the read() call.
        unsafe {
            let ptr = ManuallyDrop::new(self);
            let value = std::ptr::read(ptr.0);
            // don't forget to clean up the initial allocation
            std::alloc::dealloc(ptr.0 as *mut u8, Layout::new::<T>());

            value
        }
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        // Safety: We've got unique access to the *mut T and we'll treat the
        // value as uninitialized once it has been dropped in place.
        unsafe {
            std::ptr::drop_in_place(self.0);
            std::alloc::dealloc(self.0 as *mut u8, Layout::new::<T>());
        }
    }
}

fn main() {
    let boxed = MyBox::new("Hello, World!".to_string());

    let s = boxed.into_inner();
    println!("{s}");
    let _another_box = MyBox::new("Hello, World!".to_string());
}

(playground)

3 Likes

Thanks. Does std::ptr::drop_in_place always invoke the drop method(if any) for the pointed-to value?

Yes.

It will also run the type's drop glue, so even if it doesn't explicitly have a destructor, the compiler will recursively call drop_in_place() on each of the type's fields (which may in turn implement Drop).

3 Likes
A minor nitpick

std::alloc::alloc() is allowed to return a null pointer on allocation failure, and callers are required to handle that. To illustrate, this produces a segfault (Rust Playground):

use std::{
    alloc::{GlobalAlloc, Layout, System},
    mem::ManuallyDrop,
    ptr,
    sync::atomic::{AtomicBool, Ordering},
};

struct MyBox<T>(*mut T);

impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        unsafe {
            let ptr: *mut T = std::alloc::alloc(Layout::new::<T>()).cast();
            ptr.write(value);
            MyBox(ptr)
        }
    }

    fn into_inner(self) -> T {
        // Safety: We put the pointer behind a ManuallyDrop to avoid double
        // frees. This operation logically moves ownership of the T from MyPtr
        // to the caller, so we can't touch it after the read() call.
        unsafe {
            let ptr = ManuallyDrop::new(self);
            let value = std::ptr::read(ptr.0);
            // don't forget to clean up the initial allocation
            std::alloc::dealloc(ptr.0 as *mut u8, Layout::new::<T>());

            value
        }
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        // Safety: We've got unique access to the *mut T and we'll treat the
        // value as uninitialized once it has been dropped in place.
        unsafe {
            std::ptr::drop_in_place(self.0);
            std::alloc::dealloc(self.0 as *mut u8, Layout::new::<T>());
        }
    }
}

//////////

struct DummyAlloc(AtomicBool);

// SAFETY: Neither `alloc()` nor `dealloc()` can unwind, and `alloc()` returns
// either an allocated pointer or a null pointer.
unsafe impl GlobalAlloc for DummyAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        if self.0.load(Ordering::Relaxed) {
            System.alloc(layout)
        } else {
            ptr::null_mut()
        }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        System.dealloc(ptr, layout);
    }
}

#[global_allocator]
static ALLOC: DummyAlloc = DummyAlloc(AtomicBool::new(true));

fn main() {
    ALLOC.0.store(false, Ordering::Relaxed);
    MyBox::new(0);
}

The recommended fix is to call handle_alloc_error():

     fn new(value: T) -> Self {
         unsafe {
             let ptr: *mut T = std::alloc::alloc(Layout::new::<T>()).cast();
+            if ptr.is_null() {
+                std::alloc::handle_alloc_error(Layout::new::<T>());
+            }
             ptr.write(value);
             MyBox(ptr)
         }
     }