Phantom Types
A phantom type is a generic type whose type parameter T (or lifetime 'a) does not appear in any runtime data field, existing solely at compile time to enforce static invariants, units of measure, variance, or lifetime bounds. In Rust, phantom type parameters are bound using std::marker::PhantomData<T>, a zero-sized marker type (size_of::<PhantomData<T>>() == 0) that disappears completely during code generation.
Why Unused Type Parameters Require PhantomData (E0392)
If a struct or enum declares a generic type parameter <Tag> or lifetime <'a> without referencing it in at least one field, the compiler cannot determine whether the type should be covariant, contravariant, or invariant over that parameter, and rejects the definition with error E0392. Including a _marker: PhantomData<...> field resolves the variance and ownership semantics at zero runtime byte cost.
Controlling Variance, Lifetimes, and Auto-Traits with PhantomData<...>
The exact type passed inside PhantomData<...> controls how the compiler treats the enclosing struct with respect to variance, drop check, and Send / Sync:
PhantomData Pattern |
Variance in 'a |
Variance in T |
Send / Sync Effect |
Drop Check (Owns T?) |
|---|---|---|---|---|
PhantomData<T> |
— | Covariant | Send/Sync only if T: Send/Sync |
Yes (acts as if owning T) |
PhantomData<&'a T> |
Covariant | Covariant | Send/Sync if &'a T is |
No (acts like a shared borrow &'a T) |
PhantomData<&'a mut T> |
Covariant | Invariant | Send/Sync if &'a mut T is |
No (acts like &'a mut T) |
PhantomData<*const T> |
— | Covariant | Makes struct !Send and !Sync |
No |
PhantomData<*mut T> |
— | Invariant | Makes struct !Send and !Sync |
No |
PhantomData<fn(T)> |
— | Contravariant | Always Send + Sync (regardless of T!) |
No |
PhantomData<fn() -> T> |
— | Covariant | Always Send + Sync (regardless of T!) |
No |
PhantomData<fn(T) -> T> |
— | Invariant | Always Send + Sync (regardless of T!) |
No |
Notice the fn pointer patterns in the last three rows: because function pointers fn(...) always implement Send and Sync regardless of whether their parameter or return types implement Send or Sync, PhantomData<fn() -> T> (covariant) and PhantomData<fn(T) -> T> (invariant) let you attach a phantom type tag T without inheriting T's Send, Sync, or drop-check restrictions!
Type-Level Units and Domain Tagging
By parameterizing a single generic Quantity<Value, Unit> struct over uninhabited or zero-sized unit marker types (enum Meters {}, enum Seconds {}), you can share arithmetic and formatting code across all units while forbidding accidental addition of incompatible units (Meters + Seconds):
use std::marker::PhantomData;
use std::ops::Add;
enum Milliseconds {}
enum Bytes {}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Metric<Unit> {
raw: u64,
_unit: PhantomData<fn() -> Unit>,
}
impl<Unit> Metric<Unit> {
const fn new(raw: u64) -> Self {
Self {
raw,
_unit: PhantomData,
}
}
const fn get(self) -> u64 {
self.raw
}
}
// Only `Metric<U> + Metric<U>` of the exact same `Unit` is implemented:
impl<Unit> Add for Metric<Unit> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.raw + rhs.raw)
}
}
fn main() {
let t1 = Metric::<Milliseconds>::new(120);
let t2 = Metric::<Milliseconds>::new(80);
let payload = Metric::<Bytes>::new(4096);
let total_time = t1 + t2;
// `t1 + payload` would fail to compile (E0308: expected `Metric<Milliseconds>`, found `Metric<Bytes>`)!
println!("size of Metric<Bytes>: {} bytes", size_of::<Metric<Bytes>>());
println!("total_time: {} ms, payload: {} B", total_time.get(), payload.get());
}
size of Metric<Bytes>: 8 bytes
total_time: 200 ms, payload: 4096 B