The ghost in the machine
You write a wrapper struct. It takes a generic type T. You store a raw pointer or an index, but never the actual T. You try to compile. Rust rejects you with E0392 (type parameter is not constrained by the input type). You add a field data: T to silence the error. Now you're storing a T you don't have, or you're allocating memory you don't need. You're stuck.
This is where PhantomData appears. It tells the compiler, "I care about this type, even though I don't hold it." It bridges the gap between your internal representation and the type system's expectations. Without it, Rust cannot reason about variance, auto traits like Send and Sync, or drop check. With it, your unsafe abstractions become sound.
What PhantomData actually is
PhantomData<T> is a zero-sized type. It occupies no memory. It has no runtime cost. It exists purely to influence how the compiler treats your struct. When you add a field of type PhantomData<T>, the compiler pretends your struct contains a T. This affects three things:
- Variance: How subtyping flows through your generic parameters.
- Auto traits: Whether your struct is
Send,Sync, orUnpinbased onT. - Drop check: Whether the compiler assumes you drop
Twhen your struct goes out of scope.
You use PhantomData whenever your struct has a generic parameter or lifetime that doesn't appear in any actual field. The compiler needs a way to know how that parameter relates to the struct. PhantomData provides that link.
use std::marker::PhantomData;
/// A wrapper that holds a pointer but claims ownership of T.
struct Wrapper<T> {
ptr: *const T,
// PhantomData<T> tells the compiler we logically own T.
// This makes Wrapper<T> invariant in T and requires T: Send for Wrapper<T>: Send.
_marker: PhantomData<T>,
}
The field name _marker is convention. The underscore prefix suppresses the "unused variable" warning. The community also uses _phantom or _marker. Pick one and stick with it. The name doesn't matter to the compiler, but consistency helps readers.
Controlling variance with markers
Variance determines whether a struct accepts subtypes of its generic parameters. If Dog is a subtype of Animal, does Wrapper<Dog> count as a Wrapper<Animal>? The answer depends on how you use T inside the struct.
If you store T in a field, Rust infers variance automatically. If you only have a raw pointer, Rust knows nothing. PhantomData lets you declare the variance explicitly.
PhantomData<T> makes your struct invariant in T. The compiler treats T as if it can be both read and written. No subtyping is allowed. This is the default assumption for ownership. If you claim to own T, you might mutate it, so the compiler refuses to substitute subtypes.
PhantomData<&'a T> makes your struct covariant in 'a and T. The compiler treats T as if it's only read through a reference. Subtypes are allowed. This is correct for borrowed data.
use std::marker::PhantomData;
/// Covariant handle. Accepts subtypes.
struct CovariantHandle<'a, T> {
index: usize,
// PhantomData<&'a T> signals we only borrow T.
// CovariantHandle<'a, Dog> can be used where CovariantHandle<'a, Animal> is expected.
_marker: PhantomData<&'a T>,
}
/// Invariant handle. Rejects subtypes.
struct InvariantHandle<T> {
index: usize,
// PhantomData<T> signals we own T.
// InvariantHandle<Dog> cannot be used where InvariantHandle<Animal> is expected.
_marker: PhantomData<T>,
}
Get the variance wrong, and your safe abstraction leaks unsafety. If you mark a struct covariant when it actually mutates T, you can create references to the wrong type. The compiler trusts PhantomData. If you lie, you break memory safety.
Auto traits and Send/Sync
Rust derives auto traits like Send and Sync based on the fields of a struct. If all fields are Send, the struct is Send. Raw pointers are never Send or Sync. The compiler doesn't know if the data behind a *mut T is thread-safe.
PhantomData links your struct's auto traits to T. If you use PhantomData<T>, the compiler requires T: Send for your struct to be Send. This is essential for smart pointers. Box<T> is Send only if T is Send. Box uses PhantomData<T> internally to enforce this rule.
use std::marker::PhantomData;
/// A thread-safe handle that requires T to be Send.
struct ThreadSafeHandle<T> {
id: u64,
// PhantomData<T> ties Send/Sync to T.
// ThreadSafeHandle<T> is Send only if T is Send.
_marker: PhantomData<T>,
}
If you omit PhantomData, the compiler sees only id: u64. It marks ThreadSafeHandle<T> as Send regardless of T. You can now send a non-Send type across threads by wrapping it in your handle. The abstraction is broken. PhantomData closes the hole.
The drop check trap
Drop check is a compiler rule that prevents dangling references. If a struct holds a reference and a raw pointer, the compiler must ensure the reference doesn't outlive the data. PhantomData influences drop check by declaring ownership.
PhantomData<T> tells the compiler you drop T. If T is a reference, you cannot drop a reference through a raw pointer. The compiler rejects this. You must use PhantomData<&'a T> instead. This tells the compiler you borrow T, so you don't drop it.
use std::marker::PhantomData;
/// Correct: We borrow T, so we don't drop it.
struct BorrowedWrapper<'a, T> {
ptr: *const T,
// PhantomData<&'a T> signals we borrow T.
// Drop check passes because we don't claim to drop T.
_marker: PhantomData<&'a T>,
}
/// Incorrect: We claim to own T, but we can't drop it through a pointer.
struct OwnedWrapper<'a, T> {
ptr: *const T,
// PhantomData<T> signals we own T.
// Drop check fails if T contains references, because we can't drop T safely.
_marker: PhantomData<T>,
}
The error here is subtle. If T is a simple type like i32, both compile. If T contains a reference, OwnedWrapper fails drop check. The compiler thinks you're trying to drop a reference through a raw pointer, which could leave a dangling reference. Use PhantomData<&'a T> for borrowed data. Use PhantomData<T> only when you truly own T and can drop it.
Realistic example: A handle type
Handle types are common in game engines and UI frameworks. You store data in a global arena and return lightweight handles. The handle must carry lifetime information and variance.
use std::marker::PhantomData;
/// A handle that borrows data from an arena for lifetime 'a.
struct ArenaHandle<'a, T> {
/// The index into the arena.
index: usize,
/// PhantomData marks the lifetime and type.
/// Covariant in 'a and T. No ownership implied.
_marker: PhantomData<&'a T>,
}
impl<'a, T> ArenaHandle<'a, T> {
/// Create a handle. The caller must ensure the arena lives for 'a.
fn new(index: usize) -> Self {
ArenaHandle {
index,
_marker: PhantomData,
}
}
/// Access the data. Returns a reference tied to 'a.
fn get(&self, arena: &'a [T]) -> &'a T {
&arena[self.index]
}
}
The _marker field is never initialized with a value. PhantomData is a unit struct. You write PhantomData without parentheses. The compiler fills in the type from the field declaration. This pattern is standard. Every unsafe abstraction that exposes generic parameters or lifetimes needs this treatment.
Pitfalls and compiler errors
E0392 (type parameter is not constrained): This error appears when you have a generic parameter that doesn't appear in any field. The compiler assumes the parameter is unused. Add PhantomData<T> to constrain it.
E0277 (trait bound not satisfied): If you use PhantomData<T>, your struct inherits trait requirements from T. If T isn't Send, your struct isn't Send. You might see this error when trying to send your struct across threads. Fix it by ensuring T satisfies the trait, or by adjusting the PhantomData marker if the requirement is wrong.
Variance mismatches: The compiler doesn't always catch variance bugs at the definition site. They surface when you try to use subtypes. If your code compiles but panics or corrupts memory at runtime, check your PhantomData markers. Invariance is the safe default. Covariance requires proof that you never mutate T.
Convention aside: Keep PhantomData fields private. They are implementation details. Exposing them invites misuse. If you need to expose the type parameter, do it through methods or associated types, not through the marker field.
Convention aside: Use PhantomData only when necessary. If you can store the value directly, do it. PhantomData adds complexity. It's a tool for unsafe abstractions, not for everyday structs.
Decision matrix
Use PhantomData<T> when your struct logically owns a T but stores it elsewhere, like a raw pointer or an index into a global arena. This makes the struct invariant in T and requires T: Send for the struct to be Send.
Use PhantomData<&'a T> when your struct borrows data of type T for lifetime 'a. This makes the struct covariant in 'a and tells the compiler you don't drop T. This is the correct marker for handles and views.
Use PhantomData<fn(T) -> T> when you need invariance in T without implying ownership or borrowing. This is rare and usually appears in unsafe abstractions like Cell or UnsafeCell. The function type signals that T can be transformed, which blocks subtyping without claiming ownership.
Reach for a regular field when you actually store the value. PhantomData is only for the type system, not storage. If you can add a field of type T, the compiler infers everything automatically. PhantomData is the escape hatch, not the default.
PhantomData is the glue between your raw memory and the type system. Use it to bridge the gap, not to hide bugs. Get the marker wrong, and you get a compile error. Get the safety wrong, and you get memory corruption. Treat every PhantomData field as a contract with the compiler. If you can't justify the marker, you don't have a safe abstraction.