How/Why Box<T> is Unpin regardless of whether T is Unpin or !Unpin

Is there any secret implementation that do the magic and let the Boxed Pin to safely update the refrences?
I'm confused sorry :confused:

Because moving a Box<T> does not change where the T is stored. This isn't magic — it's just how heap allocations work.

6 Likes

@alice, no, the fact that Box<T> implements Unpin unconditionally has got nothing to do with the fact that Box introduces an indirection. You can build a wrapper without any indirection such that pinning is not structural for the field, e.g. with pin-project

use pin_project::pin_project;

#[pin_project]
struct UnpinWrapper<T>(T);

fn assert_unpin<T: Unpin>() {}
fn test<T>() {
    // this compiles because UnpinWrapper<T>: Unpin
    // is true even if T: !Unpin
    assert_unpin::<UnpinWrapper<T>>();
}

Edit: Well, actually, Unpin is a safe trait, so that wrapper doesn’t need pin-project. That crate with its safe macros is only really needed if you do want some fields to do structural pinning.

struct UnpinWrapper<T>(T);
impl<T> Unpin for UnpinWrapper<T> {}
2 Likes

Okay, maybe you can be a bit more precise in the wording. Anything can be Unpin if it wants to. In the case of Box, it is Unpin because it makes sense for heap allocated structures to be unconditionally Unpin.

To understand pinning better try reading the module-docs of std::pin and the API of Pin. The important things that T: Unpin allows is

  • converting Pin<SomePointerType<T> into SomePointerType<T> via Pin::into_inner

    • e.g. Pin<&T> to &T
      or Pin<&mut T> to &mut T (this conversion is also provided by Pin::get_mut)
      or Pin<Box<T>> to Box<T>
  • freely creating Pin<SomePointerType<T>> without needing to hold up any guarantees, by using Pin::new

For implementing T: Unpin soundly, the thing to keep in mind is that you must not also implement some kind of Pin<&mut T> -> Pin<&mut SomeField> projection. The reason why Unpin implementations can be done without unsafe is that these projections do require use of unsafe except if you’re using macros such as the one provided by pin-project.


What alice said is true, but it doesn’t have anything to do with the Unpin for Box<T> implementation.

Because moving a Box<T> does not change where the T is stored.

This is in fact the reason why Box::pin exists and lets you turn T into Pin<Box<T>> for any type T. Afterwards, the Pin<Box<T>> can be moved freely while also providing mutable pinned views Pin<&mut T> via Pin::as_mut.

3 Likes

A very simple but important point!
Thanks

The point is unlike stack movement, heap moves do not need to copy the memory. They just swap the ownerships

Every move and copy are guaranteed to be equivalent to the single memcpy call, if not optimized out. There's no exceptions, since it's the language built-in rule instead of some conventions like C++ "move semantics". Not any user defined code like swapping can run in this process. It's just moving Box<T> doesn't copy the T. It only copy the ptr.

Note that the move and the copy are strictly identical on runtime operation. They only differ at compile time, specifically using moved out value throws compile error while using copied out value is valid.

2 Likes

IOW, it has nothing to do with "stack vs. heap". It has to do with "value vs. pointer".

2 Likes

This seems like a good example of read the manual/docs :grinning_face_with_smiling_eyes::

Where a structural projection from Box<T> to T would be a non-unsafe function such as:

fn pin_project_box<T> (p: Pin<&mut Box<T>>) -> Pin<&mut T>

If Box where to feature such a function, then indeed we'd need a T : Unpin bound on the impl of Unpin for Box<T>.

And, indeed, the motivation for Box and most pointer types not to offer such a "structural" Pin-projection, is that the Box<T>, in and of itself, does not live in the same storage / location as the T it points to. In other words, types such as Pin<&mut Box<T>> are "silly" to begin with.

@steffahn sorry I have miss-clicked my answer, I did not inted to direct my post to you specifically :sweat_smile: – quite the contrary I may add!

1 Like

Since you’re answering to my comment, I’ll answer back:

I myself are very aware of those docs, I even reformatted the whole thing recently and also linked them in a post above

and – by the way – clicking on the word “structural”

takes you to the docs as well, directly to the relevant section.

Nonetheless I appreciate your effort in quoting and further explaining (for others) some relevant part directly in this thread. I personally think that the documented motivation section on why Box doesn’t do structural pinning feels somewhat somewhat weak. It really boils down to “we don’t want to offer pin projections for Box now or in the future” and a type that never implements those might as well implement Unpin then. The Unpin implementation then probably also leads to some useful things like the fact that e.g. Pin<Box<dyn Future<Output = T>>> can implement Future itself, too.


That came slightly too late :laughing:

1 Like