Phantom Types
A phantom type is a generic type or lifetime parameter on a struct or enum that does not appear in any runtime data field, bound instead using the zero-sized marker std::marker::PhantomData<T>.
Why Unreferenced Type Parameters Are Rejected (E0392)
If you declare a struct struct TaggedId<Tag> { raw: u64 } without referencing Tag in any field, the compiler rejects the definition with E0392 because it cannot determine the variance, drop behavior, or auto-trait (Send/Sync) properties of Tag:
// ❌ does not compile
struct TaggedId<Tag> {
raw: u64,
}
fn main() {}
error[E0392]: type parameter `Tag` is never used
--> src/main.rs:2:17
|
2 | struct TaggedId<Tag> {
| ^^^ unused type parameter
|
= help: consider removing `Tag`, referring to it in a field, or using a marker such as `PhantomData`
Adding a field _marker: std::marker::PhantomData<Tag> binds the parameter at the type level while occupying 0 bytes of memory at runtime (size_of::<PhantomData<T>>() == 0).
Type-Safe Tagged Identifiers and Units of Measure
Using PhantomData<Tag>, a single generic struct Id<Entity> or Amount<Currency> can represent dozens of incompatible domain types without duplicating code or paying any runtime overhead:
use std::marker::PhantomData;
#[derive(Debug, PartialEq, Eq)]
pub struct User;
#[derive(Debug, PartialEq, Eq)]
pub struct Invoice;
#[derive(Debug, PartialEq, Eq)]
pub struct Id<Entity> {
value: u64,
_marker: PhantomData<Entity>,
}
impl<Entity> Id<Entity> {
pub const fn new(value: u64) -> Self {
Self {
value,
_marker: PhantomData,
}
}
pub const fn get(&self) -> u64 {
self.value
}
}
fn lookup_invoice(id: Id<Invoice>) {
println!("Looking up invoice #{}, size = {} bytes", id.get(), size_of_val(&id));
}
fn main() {
let _user_id = Id::<User>::new(42);
let invoice_id = Id::<Invoice>::new(42);
lookup_invoice(invoice_id);
// `lookup_invoice(_user_id)` is a compile-time error: expected `Id<Invoice>`, found `Id<User>`!
}
Looking up invoice #42, size = 8 bytes
Controlling Variance, Auto Traits, and Lifetimes with PhantomData
Beyond phantom type tags, PhantomData tells the compiler how a struct containing raw pointers or FFI handles behaves with respect to lifetimes, variance, and Send / Sync:
PhantomData Field Type |
Meaning Communicated to the Compiler | Variance in T / 'a |
Send / Sync Impact |
|---|---|---|---|
PhantomData<T> |
Struct logically owns a T (may affect drop check) |
Covariant | Send/Sync iff T: Send/Sync |
PhantomData<&'a T> |
Struct logically borrows a shared &'a T (e.g., raw pointer slice wrapper) |
Covariant in 'a and T |
Send/Sync iff &'a T: Send/Sync |
PhantomData<&'a mut T> |
Struct logically holds an exclusive mutable borrow &'a mut T |
Covariant in 'a, Invariant in T |
Send/Sync iff &'a mut T: Send/Sync |
PhantomData<fn() -> T> |
Covariant marker without claiming ownership of T |
Covariant | Always Send + Sync (even if T: !Send) |
PhantomData<fn(T) -> T> |
Invariant marker without claiming ownership of T |
Invariant | Always Send + Sync |
PhantomData<*const ()> |
Opts a struct out of Send and Sync |
— | !Send and !Sync |