[Resolved] PhantomData and Unpin

I noticed that PhantomData only implements Unpin for T: Unpin (through auto-trait implementation). Is there a reason for this? Isn't PhantomData completely empty and, as such, can always safely implement Unpin?

Would it be appropriate to implement:

impl<T: ?Sized> Unpin for PhantomData<T> {}
1 Like

PhantomData<T> is meant to behave the same as T at compile time.

Hm, sorry but I don't think I understand. From reading the std docs the impression that act like a compiler marker that owns some T but not necessarily behaves like T. Here is an example that I consider to be somewhat similar:

use std::marker::PhantomData;
fn assert_sized<T: Sized>() {}
fn main() {
    assert_sized::<PhantomData<[u8]>>(); // OK
    assert_sized::<[u8]>(); // Compile error
}

edit: Just to clarify - here the PhantomData is SIzed event if T is not. To me this is analogous to PhantomData being Unpin even if T is not.

What is the motivation for making PhantomData implement Unpin?

I don't know of a reason why it can't, but that's different from arguing that it should. IIUC, making PhantomData impl Unpin would make a lot of PhantomData-using types automatically impl Unpin as well, but I would expect that for most PhantomData-using types it's not at all obvious whether Unpin is right and that needs to be decided on a case-by-case basis.

Ah, that'a is a valid point. So a couple things:

  • One of the motivations of my question was "is there something super obvious that I'm missing why PhantomData can't implement Unpin for T: !Unpin". From what I'm understanding, while PhantomData itself is always safe to be Unpin there might be some unsoundness that needs to be explored further if it were always automatically implemented for all T.
  • To answer your question, I think that the fact that Unpin is an auto-trait to me signals that as many things as possible that can safely be Unpin should.

All auto-traits for PhantomData depend on T: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#synthetic-implementations

PhantomData<&UnSync> could implement Send, even though &UnSync doesnt, but that would make PhantomData useless. In fact PhantomData is mostly used to prevent implementation of auto traits when the wrapped type doesn't implement it. For example Box<T> shouldn't implement Send when T doesn't. Box uses PhantomData<T> to do this.

OK, I think I understand. PhantomData<T> is intended for all intents and purposes to behave exactly like T with the exception of size. This means that certain safety related behaviours of T (such as Send, Sync, Unpin etc.) must also be maintained. I think in the specific case I was thinking of just using PhantomData to use a type's associated functions, which is something that PhantomData allows but not necessarily its primary purpose. (Please correct me if I'm wrong).

To that extent, I think what I was looking for in my particular case was to use PhantomData<Box<T>> or similar to allow both the ability to use a type's associated functions and also always be Unpin.

Thanks for the clarification guys, I really appreciate it.

3 Likes

That sounds super weird, because associated functions with a self argument (a.k.a. "methods") obviously can't be invoked if there is no actual self/T value to pass to them (and PhantomData<T> doesn't contain an actual T), but associated functions without one can be trivially invoked from anywhere as T::foo() (so there's no reason to get PhantomData involved).

Is there something else I'm missing, or was this just confusion?

Though the precise semantics of Unpin still melt my mind, I imagine this trait bound actually serves a useful purpose, since it does so for other auto traits. Basically, impl !Trait for T is still unstable, but you can simulate it by adding PhantomData members. That is,

#[repr(transparent)]
struct NoSyncI32 {
    value: i32,
    _no_sync: PhantomData<Cell<()>>,
}

is a wrapper around i32 that disables the Sync impl. Perhaps there are use cases for similarly using PhantomData to disable Unpin impls.

Edit: Ah. Hm. No, there's a dedicated PhantomPinned for that, presumably because no standard library types impl !Unpin. Interesting. (However, PhantomData is still useful in generic impls in cases where the intent is to disable Unpin when T: !Unpin)

1 Like

IIRC trait objects lack all auto-traits, unless explicitly specified, so you could use something like PhantomData<dyn Fn()>.

@Ixrec: Is there something else I'm missing, or was this just confusion?

The idea was I needed something to be Unpin that looked like:

struct Foo<T: Bar> {
    _bar: PhantomData<Box<T>>,
}

So that Foo could be generic over T for calling T::bar(). If I used just PhantomData<T> then Foo would only be Unpin if T: Unpin. Using PhantomData<Box<T>> allowed Foo to be Unpin regardless of T which is what we wanted in this case, since T is just used for the associated method T::bar(). I hope this clarifies things...

Ah, that does make more sense. I still think wrappers like this are usually cases where the type author needs to think about whether Unpin is appropriate, but I definitely see why that wouldn't be nearly as obvious as, say, structs with a raw pointer and a PhantomData describing the pointee.