165. PhantomData<T> — The Zero-Sized Marker That Pretends to Own a T
You write a generic struct, never actually store a T in any field, and the compiler stops you with “parameter T is never used”. PhantomData<T> is the zero-cost lie that fixes it — a marker that occupies no bytes but tells the compiler “act as if I own a T.”
The problem shows up the moment you build a typed handle around something that isn’t a T:
| |
rustc rejects this because an unused type parameter is almost always a bug — variance, drop checking, and Send/Sync all depend on what a struct claims to own. std::marker::PhantomData<T> is the escape hatch: a zero-sized struct that pretends the type parameter is used:
| |
The _marker field disappears at runtime — size_of::<TypedId<User>>() is exactly size_of::<u64>(). But at compile time, TypedId<User> and TypedId<Order> are distinct types you can’t accidentally swap.
The same pattern fixes lifetimes too. FFI wrappers borrow from a buffer they don’t physically point into:
| |
Without the PhantomData<&'a [u8]>, the 'a would be unused and the compiler wouldn’t enforce that buf outlives cursor. With it, the borrow checker treats CursorHandle<'a> as if it held a real &'a [u8].
Three flavors of PhantomData you’ll see in the wild — pick by what you want the compiler to believe:
| |
That last one is the cheap way to opt a type out of Send/Sync without unsafe — Rc<T> uses exactly this trick internally to stay single-threaded.
PhantomData is the bookkeeping behind almost every wrapper type you’ve used. Cell, Cow, Pin, Rc, and NonNull all carry one — it’s how they tell the compiler what they conceptually own without paying for it at runtime.