Variance and PhantomData

Rust · Expert · 18 min

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 of F<Super>. &'a T is covariant in 'a and T; so are Box<T>, Vec<T> and *const T.
  • Invariant: no subtyping at all. &'a mut T is invariant in T, as are Cell<T>, UnsafeCell<T> and *mut T.
  • Contravariant: reversed. fn(T) is contravariant in T: a function accepting any &'a str can 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 a T" (covariant, affects drop checking and auto traits like a real T).
  • PhantomData<&'a T>: "I borrow a T for 'a", typical for iterators over raw pointers.
  • PhantomData<fn() -> T>: covariant but does not own a T, so the type stays Send/Sync whatever T is. Good for typed IDs.
  • PhantomData<fn(T) -> T> or PhantomData<*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 T is 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 makes Id<T> non-Send whenever T is; use PhantomData<fn() -> T>.

Check yourself

  1. Which type is invariant in T?
  2. Why does a raw-pointer slice iterator include `PhantomData<&'a T>`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/nomicon/subtyping.html

Builds on

Unlocks

Nothing builds on this yet.

Variance and PhantomData · Rust · Topolearn