Writing generic containers, typed handles or unsafe code over raw pointers requires getting variance and PhantomData right.
Rust's only subtyping is between lifetimes: &'static str is a subtype of &'a str, because a longer-lived reference can be used where a shorter one is expected. Variance says how that relationship passes through a type constructor:
- Covariant:
F<Sub>is a subtype ofF<Super>.&'a Tis covariant in'aandT; so areBox<T>,Vec<T>and*const T. - Invariant: no subtyping at all.
&'a mut Tis invariant inT, as areCell<T>,UnsafeCell<T>and*mut T. - Contravariant: reversed.
fn(T)is contravariant inT: a function accepting any&'a strcan stand in for one that only needs&'static str.
Why must &mut T be invariant? If &mut &'static str could shrink to &mut &'a str, you could write a short-lived &'a str through it and the original owner would later read a dangling 'static reference. Invariance blocks exactly that.
The compiler infers variance from fields. If a type parameter is not used in any field (common with raw pointers or typed IDs), you must add `PhantomData<...>`, a zero-sized marker that tells the compiler how to treat it. Choose the form that describes what you mean:
PhantomData<T>: "I own aT" (covariant, affects drop checking and auto traits like a realT).PhantomData<&'a T>: "I borrow aTfor'a", typical for iterators over raw pointers.PhantomData<fn() -> T>: covariant but does not own aT, so the type staysSend/SyncwhateverTis. Good for typed IDs.PhantomData<fn(T) -> T>orPhantomData<*mut T>: invariant.
Getting this wrong in unsafe code is a soundness bug: a raw-pointer iterator without PhantomData<&'a T> would let callers keep items after the collection is freed.
Example
use std::marker::PhantomData;
// Typed ID: Id<User> and Id<Order> can't be mixed; fn() -> T keeps it Send + Sync for any T.
struct Id<T> { raw: u64, _t: PhantomData<fn() -> T> }
struct User; struct Order;
fn load_user(id: Id<User>) -> u64 { id.raw }
// Raw-pointer iterator: PhantomData<&'a T> ties the items to the borrowed slice.
struct RawIter<'a, T> { ptr: *const T, end: *const T, _life: PhantomData<&'a T> }
impl<'a, T> Iterator for RawIter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<&'a T> {
if self.ptr == self.end { return None; }
// SAFETY: ptr is in bounds of a slice that lives for 'a
unsafe { let item = &*self.ptr; self.ptr = self.ptr.add(1); Some(item) }
}
}
fn raw_iter<T>(s: &[T]) -> RawIter<'_, T> {
let r = s.as_ptr_range();
RawIter { ptr: r.start, end: r.end, _life: PhantomData }
}
fn main() {
let (uid, _oid) = (Id::<User> { raw: 7, _t: PhantomData }, Id::<Order> { raw: 8, _t: PhantomData });
println!("{}", load_user(uid)); // 7; load_user(_oid) is a type error
println!("{:?}", raw_iter(&[1, 2, 3]).collect::<Vec<_>>()); // [1, 2, 3]
}Remember
- Covariant:
&'a T,Box<T>; invariant:&mut T,Cell<T>; contravariant:fn(T) &mut Tis invariant so you cannot smuggle a short-lived value into a long-lived slot- Unused type or lifetime parameters need
PhantomData; pick the form that states ownership and variance
Watch out for
- Using
PhantomData<T>for a typed ID, which makesId<T>non-Send wheneverTis; usePhantomData<fn() -> T>.