Frontend Labs
⋯

60. Variance, PhantomData, and drop check

You need these when you write generic containers or unsafe code. You can skip this chapter until you do.

PhantomData<T> tells the compiler that a type logically contains a T when no field actually stores one. That affects three things: variance, auto traits (Send/Sync), and drop checking.

use std::marker::PhantomData;

/// A typed ID: Id<User> and Id<Order> are different types with the same 8-byte representation.
struct Id<T> { raw: u64, _kind: PhantomData<fn() -> T> }
// `fn() -> T` instead of `T`: the Id does not OWN a T, so it is Send + Sync and Copy whatever T is.

// Derive would add `T: Clone` bounds we do not want, so implement by hand.
impl<T> Clone for Id<T> { fn clone(&self) -> Self { *self } }
impl<T> Copy for Id<T> {}
impl<T> PartialEq for Id<T> { fn eq(&self, o: &Self) -> bool { self.raw == o.raw } }
impl<T> Id<T> { fn new(raw: u64) -> Self { Id { raw, _kind: PhantomData } } }

struct User;
struct Order;

fn load_user(id: Id<User>) -> String { format!("user #{}", id.raw) }

fn main() {
    let u: Id<User> = Id::new(7);
    let o: Id<Order> = Id::new(7);
    println!("{}", load_user(u));
    // load_user(o);                         // error[E0308]: expected `Id<User>`, found `Id<Order>`
    println!("{} bytes, same raw value: {}", size_of::<Id<User>>(), u.raw == o.raw);
}

Variance decides whether Foo<&'long T> can be used where Foo<&'short T> is expected. &'a T is covariant (a longer-lived reference can stand in for a shorter one). &'a mut T is invariant in T. If it were not, you could write a short-lived reference into a slot that expects a long-lived one, and that is a use-after-free. PhantomData<T> makes your type covariant in T, PhantomData<fn(T)> makes it contravariant, and PhantomData<*mut T> or PhantomData<Cell<T>> makes it invariant. The Rustonomicon's chapter on subtyping has the full table.

Drop check stops a Drop implementation from reading data that has already been dropped. If your Drop impl for Container<T> might use a T, PhantomData<T> tells the compiler that dropping a Container<T> drops T values, so borrows inside T must still be alive at that point.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36