How to use PhantomData

Use PhantomData<T> as a struct field to inform the compiler about ownership or borrowing relationships for types that are not directly stored.

When the compiler can't see your type parameter

You're building a custom smart pointer. You have a raw pointer *mut T inside a struct. You implement Drop to free the memory. You try to store a String in your pointer. The compiler screams. It says the String inside the pointer isn't dropped, or worse, it lets you create a dangling reference that should be impossible.

The compiler is looking at your struct fields. It sees a *mut T. It knows raw pointers don't own anything. It doesn't know T is involved in the ownership or borrowing rules. To the compiler, T is a ghost. It doesn't affect drop check, it doesn't affect variance, and it doesn't affect auto traits like Send.

You need a way to whisper to the compiler: "This raw pointer points to a T, and I own that T." Or: "This struct borrows a T, even though the T isn't a field." That whisper is PhantomData.

PhantomData<T> is a zero-sized marker type. It takes no memory. It adds no runtime overhead. It exists solely to tell the compiler how to treat a type parameter that doesn't appear in your struct's actual data fields. Add a field of type PhantomData<T>, and the compiler suddenly sees T where it couldn't before.

The type tag analogy

Think of PhantomData as a shipping label on a crate. You're moving a crate across a border. The crate contains a raw pointer to a warehouse. The customs officer (the compiler) inspects the crate. The crate has no T inside. The officer says, "This crate doesn't contain T, so I don't need to check T's paperwork."

But you know the warehouse holds T. You slap a sticker on the crate: "Contains T." Now the officer checks T's paperwork. The sticker costs nothing to print. It doesn't change the contents. It just forces the inspection.

PhantomData is that sticker. You add it to your struct to force the compiler to apply the rules for T. Without it, the compiler assumes T is irrelevant. With it, the compiler enforces drop safety, variance, and trait bounds based on T.

Minimal example

Here's the pattern. You have a generic struct with a raw pointer. You add a private field _marker: PhantomData<T>.

use std::marker::PhantomData;

/// A wrapper that owns a T via a raw pointer.
struct MyBox<T> {
    ptr: *mut T,
    // Tells the compiler this type owns T.
    // This enables drop check, covariance, and Send/Sync propagation.
    _marker: PhantomData<T>,
}

The underscore in _marker suppresses the unused variable warning. The field is private so users can't construct the struct directly. PhantomData is zero-sized, so the compiler optimizes the field away entirely. The struct layout is identical to having just the pointer.

If you omit _marker, the compiler treats MyBox<T> as if T doesn't exist. This breaks drop check and variance. You'll get errors when you try to use MyBox with types that hold references, or when you try to coerce subtypes.

What PhantomData actually controls

PhantomData influences three compiler subsystems. Understanding these tells you exactly when you need the marker and which variant to use.

Drop check

The borrow checker performs drop check to ensure references aren't dropped while they're still borrowed. If your struct holds a raw pointer to a T, and T contains a reference, the compiler needs to know that dropping the struct might drop the T and thus the reference.

Without PhantomData, the compiler assumes the struct doesn't hold T. It skips the check. This allows dangling references to slip through.

use std::marker::PhantomData;

struct Wrapper<T> {
    ptr: *mut T,
    _marker: PhantomData<T>,
}

fn test_drop_check() {
    let s = String::from("hello");
    // This compiles because Wrapper tells the compiler it owns T.
    // The compiler checks that s lives long enough.
    let _w = Wrapper {
        ptr: &s as *const String as *mut _,
        _marker: PhantomData,
    };
    // s is dropped here. The compiler verified this is safe.
}

If you remove _marker, the compiler might reject this code with E0597 (borrowed value does not live long enough) in a different context, or worse, it might accept code that creates a use-after-free. The marker forces the compiler to treat Wrapper<T> as owning T.

Variance

Variance determines whether a generic type preserves subtyping relationships. If String is a subtype of str, can you treat a MyBox<String> as a MyBox<str>? The answer depends on variance.

PhantomData sets the variance rules. PhantomData<T> makes your struct covariant in T. Covariance means you can substitute a subtype for a supertype. This is correct for types that own T or borrow T immutably.

If your type actually borrows T mutably, or holds a raw pointer, covariance might be unsafe. You can adjust variance by changing the phantom type. PhantomData<&mut T> makes the struct invariant. PhantomData<*const T> also makes it invariant.

The compiler enforces variance at compile time. If you use the wrong phantom type, you'll get a variance mismatch error. The error message often mentions "cannot coerce" or "invariant type parameter".

Auto traits

Auto traits like Send and Sync are implemented automatically based on the fields of a struct. If all fields are Send, the struct is Send. PhantomData participates in this calculation.

PhantomData<T> makes your struct Send only if T is Send. This is usually what you want. If your struct owns a T via a raw pointer, it should only be Send if T is Send. Without PhantomData, the struct might be Send even if T isn't, which breaks thread safety.

Convention aside: The community treats PhantomData as the contract for auto traits. If your type wraps a raw pointer, you almost always need PhantomData to get Send and Sync right. Don't rely on the default behavior.

Realistic example: A typed arena

An arena allocator manages a block of memory and hands out pointers to allocated items. The arena needs to know the type of items it manages to enforce drop safety. If you allocate a String in the arena, the arena must drop the String when the arena is dropped.

Here's a simplified arena using PhantomData.

use std::marker::PhantomData;
use std::ptr;

/// An arena that allocates T values.
/// The arena owns all T values it allocates.
struct TypedArena<T> {
    buffer: Vec<u8>,
    // Tells the compiler the arena owns T.
    // This ensures T is dropped when the arena is dropped.
    _marker: PhantomData<T>,
}

impl<T> TypedArena<T> {
    fn new() -> Self {
        TypedArena {
            buffer: Vec::new(),
            _marker: PhantomData,
        }
    }

    /// Allocate a T in the arena.
    /// Returns a pointer to the allocated value.
    fn alloc(&mut self, value: T) -> *mut T {
        // Simplified allocation logic.
        // In a real arena, you'd manage offsets and alignment.
        let ptr = self.buffer.as_mut_ptr() as *mut T;
        // SAFETY: We assume the buffer is large enough and aligned.
        // This is a sketch, not production code.
        unsafe {
            ptr::write(ptr, value);
        }
        ptr
    }
}

impl<T> Drop for TypedArena<T> {
    fn drop(&mut self) {
        // Drop all T values in the buffer.
        // SAFETY: The buffer contains valid T values.
        // PhantomData<T> ensures the compiler knows T is involved.
        // Without PhantomData, the compiler might skip dropping T.
        unsafe {
            // In a real implementation, you'd iterate and drop each T.
            // This is a placeholder.
        }
    }
}

The _marker: PhantomData<T> field is essential. It tells the compiler that TypedArena<T> owns T. When the arena is dropped, the compiler checks that T is dropped correctly. If T contains references, the compiler ensures those references are valid. Without PhantomData, the compiler thinks the arena doesn't hold T, and drop check fails.

Variance matters here too. TypedArena<T> should be covariant in T. If you have an arena of String, you should be able to use it where an arena of str is expected. PhantomData<T> provides this covariance.

Pitfalls and compiler errors

Wrong variance

The most common mistake is using PhantomData<T> when your type actually borrows T mutably or holds a raw pointer. This gives the compiler incorrect variance information. You'll get errors when you try to use subtypes.

The compiler rejects this with a variance mismatch. The error often looks like E0308 (mismatched types) or a message about "invariant type parameter". If your type holds a &mut T internally, use PhantomData<&mut T> instead of PhantomData<T>. If your type holds a raw pointer and you don't want covariance, use PhantomData<*const T>.

Match the phantom type to the actual access pattern. If you're unsure, start with PhantomData<T> and adjust based on errors.

Send and Sync surprises

PhantomData<T> affects auto traits. If T is not Send, your struct won't be Send. This can cause E0277 (trait bound not satisfied) when you try to send the struct across threads.

This is usually correct behavior. If your struct owns a T via a raw pointer, it shouldn't be Send if T isn't Send. If you need to override this, you can implement Send manually, but be careful. Overriding auto traits is unsafe and requires a // SAFETY: comment justifying the decision.

Convention aside: The community prefers PhantomData<T> for ownership semantics. If you need to suppress Send or Sync, use PhantomData<*const T> or implement the traits manually. Don't fight the auto trait system unless you have a proven reason.

Forgetting the marker

If you forget PhantomData, the compiler might accept code that is unsound. Drop check might skip references. Variance might be wrong. Auto traits might be wrong. The errors aren't always immediate. You might get a warning, or the code might compile and crash at runtime.

Always add PhantomData when your struct has a type parameter that doesn't appear in the fields. This includes raw pointers, file descriptors, and other opaque handles. Treat PhantomData as mandatory for generic wrappers.

Decision: when to use which PhantomData

Use PhantomData<T> when your type owns T and you want covariant behavior. This is the default for smart pointers and containers. The compiler treats your type as owning T, enabling drop check and Send/Sync propagation.

Use PhantomData<&T> when your type borrows T immutably. This preserves covariance and tells the compiler your type holds a reference to T. Use this for wrappers around &T or types that cache references.

Use PhantomData<&mut T> when your type borrows T mutably. This makes your type invariant in T. Use this for wrappers around &mut T or types that allow mutable access.

Use PhantomData<*const T> when you need invariant behavior and don't want ownership semantics. This suppresses covariance and doesn't imply ownership. Use this for raw pointer wrappers where you want to control variance manually.

Use PhantomData<fn(T)> when you need contravariant behavior. This is rare. Use this only when you have a function pointer or closure that consumes T. Most code doesn't need this.

Add the ghost field. The compiler needs the lie to tell the truth.

Where to go next