When the compiler can't see your data
You're building a custom buffer. You hold a raw pointer to a heap allocation. You manage the length and capacity manually. You compile, and Rust is happy. You send the buffer to another thread. The program crashes with a segfault.
The crash happens because your buffer holds a String, which contains a pointer to heap data. String is Send, so sending it is safe. But your buffer holds a *const String. Raw pointers are Send by default, even if they point to non-Send data. The compiler sees a pointer. It doesn't see the String. It assumes the pointer is safe to cross thread boundaries. You lied to the compiler by omission, and the runtime paid the price.
Rust's type system relies on fields to infer behavior. If a struct has no field of type T, the compiler assumes the struct has nothing to do with T. It won't enforce Send or Sync bounds. It won't check variance. It won't trigger drop checks. Your struct is invisible to the rules that govern T.
PhantomData<T> fixes this. It is a zero-sized marker type that tells the compiler, "Treat this struct as if it owns or borrows a T, even though no field holds one." It bridges the gap between your runtime reality and the compiler's static analysis.
The sticker on the jar
Think of PhantomData as a warning label on a container. You have a glass jar. The jar is empty. Physically, it's just glass. But you stick a label on it that says "Contains Peanuts."
Now the jar is dangerous to people with allergies. The label changes how the jar is handled, even though the jar's contents haven't changed. The label is a promise about what the jar represents. If you put the label on an empty jar and never put peanuts in it, the label is a lie. If you put the label on a jar of nuts and someone ignores it, they get hurt.
PhantomData is that label. It occupies zero bytes. It costs nothing at runtime. It exists solely to influence the compiler's decisions about variance, drop behavior, and auto-traits like Send and Sync.
Minimal example
Here is a struct that wraps a raw pointer. Without PhantomData, the compiler treats it as a bag of bytes. With PhantomData, it inherits the rules of T.
use std::marker::PhantomData;
/// A wrapper around a raw pointer that claims ownership of T.
struct UnsafeWrapper<T> {
/// The raw pointer to the data.
ptr: *const T,
/// Marker to tell the compiler this struct owns a T.
/// This enforces Send/Sync bounds and drop check.
_marker: PhantomData<T>,
}
impl<T> UnsafeWrapper<T> {
/// Create a new wrapper.
fn new(ptr: *const T) -> Self {
Self {
ptr,
// PhantomData is a zero-sized type.
// It can be initialized with Default::default() or directly.
_marker: PhantomData,
}
}
}
fn main() {
// This compiles because i32 is Send.
let wrapper_i32 = UnsafeWrapper::new(&42 as *const i32);
// If T were !Send, this wrapper would also be !Send.
// The compiler now knows wrapper_i32 carries the baggage of T.
}
The _marker field is convention. The community names it _marker or _phantom to signal that it's a compiler artifact, not data. The underscore prefix suppresses the "unused variable" warning.
Variance: the silent trap
PhantomData does more than control Send and Sync. It controls variance. Variance determines whether you can substitute a type for a related type. For example, can you use a &'long str where a &'short str is expected? That's covariance.
The type you put inside PhantomData dictates the variance of your struct. This is where most bugs hide.
PhantomData<T>makes your struct invariant inT. You cannot substituteTfor anything else. This is correct when you ownT. Ownership breaks variance because you might write to the data.PhantomData<&'a T>makes your struct covariant inTand'a. You can substitute a longer lifetime for a shorter one, and a subtype for a supertype. This is correct when you only borrowT.PhantomData<fn(T) -> T>makes your struct contravariant inT. This is rare but useful for function pointers.
If you pick the wrong marker, the compiler rejects valid code. You'll get type mismatch errors that seem unrelated to your logic.
use std::marker::PhantomData;
/// A view that borrows T without owning it.
struct BorrowedView<'a, T> {
/// Pointer to borrowed data.
ptr: *const T,
/// Marker for borrowing.
/// Use PhantomData<&'a T> to enable covariance.
/// This allows BorrowedView<'long, T> to be used as BorrowedView<'short, T>.
_marker: PhantomData<&'a T>,
}
fn main() {
let data = 42;
// Create a view with a long lifetime.
let long_view = BorrowedView {
ptr: &data as *const i32,
_marker: PhantomData,
};
// Because of PhantomData<&'a T>, the compiler allows this coercion.
// If we used PhantomData<T>, this would fail with E0308.
let short_view: BorrowedView<'_, i32> = long_view;
}
Using PhantomData<T> in BorrowedView would make the struct invariant. The compiler would refuse to coerce long_view to short_view, even though the view only reads data. The marker must match the actual behavior. If you borrow, mark it as a borrow. If you own, mark it as ownership.
The drop contract
When you use PhantomData<T>, you are telling the compiler that your struct owns a T. The compiler takes this seriously. It assumes you will drop T when the struct goes out of scope. This triggers the drop check.
If you claim ownership but never drop the value, you have two problems. First, you leak memory. Second, if T has a Drop implementation, the compiler might assume you call it. If you don't, resources aren't released. Worse, if you manually drop T through the pointer and the compiler also tries to drop T because of the marker, you get a double-free.
PhantomData is a contract. If you use PhantomData<T>, you must implement Drop and correctly release the T.
use std::marker::PhantomData;
use std::alloc::{alloc, dealloc, Layout};
/// A raw vector that owns its elements.
struct RawVec<T> {
/// Pointer to the heap allocation.
ptr: *mut T,
/// Number of elements.
len: usize,
/// Capacity of the allocation.
cap: usize,
/// Marker claiming ownership of T.
/// This enforces that RawVec<T> is Send iff T is Send.
/// It also triggers drop check, requiring us to implement Drop.
_marker: PhantomData<T>,
}
impl<T> RawVec<T> {
/// Create a new RawVec with the given capacity.
fn with_capacity(cap: usize) -> Self {
let layout = Layout::array::<T>(cap).unwrap();
// SAFETY: Layout is valid for cap elements.
// If cap is 0, alloc returns a unique pointer.
let ptr = unsafe { alloc(layout) as *mut T };
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
Self {
ptr,
len: 0,
cap,
_marker: PhantomData,
}
}
}
impl<T> Drop for RawVec<T> {
fn drop(&mut self) {
// SAFETY: We must drop all initialized elements.
// 1. ptr is valid and aligned for T.
// 2. len elements are initialized.
// 3. We are the sole owner; no other RawVec points to this memory.
// 4. We call drop_in_place for each element to run their destructors.
unsafe {
for i in 0..self.len {
std::ptr::drop_in_place(self.ptr.add(i));
}
// Deallocate the memory.
// 1. ptr was allocated with alloc.
// 2. Layout matches the allocation.
if self.cap > 0 {
let layout = Layout::array::<T>(self.cap).unwrap();
dealloc(self.ptr as *mut u8, layout);
}
}
}
}
The // SAFETY: comment lists the invariants. This is the proof that your Drop implementation is correct. If you can't write the proof, you don't have a safe Drop. The compiler trusts PhantomData<T> to mean "I drop T." If you break that trust, you introduce undefined behavior.
Pitfalls and compiler errors
PhantomData is simple to use but easy to misuse. Here are the common traps.
Wrong variance. If you use PhantomData<T> for a struct that only borrows, you lose covariance. The compiler rejects valid substitutions with E0308 (mismatched types). You'll see errors about lifetimes not matching, even though the logic is sound. The fix is to switch to PhantomData<&'a T>.
Missing Drop. If you use PhantomData<T> and forget to implement Drop, the compiler warns you. It knows you claim ownership but never drop. The warning looks like "field _marker is never read" or a drop check error. In modern Rust, the compiler enforces drop check strictly. You can't accidentally leak owned data through a phantom marker.
Send/Sync leakage. If you hold a *const T and don't use PhantomData, your struct is Send and Sync by default. This is dangerous if T is not Send. The compiler won't stop you from sending the struct across threads. You'll get runtime crashes. Adding PhantomData<T> fixes this. The struct becomes Send only if T is Send. If you try to send a non-Send type, the compiler rejects it with E0277 (trait bound not satisfied).
Debug derive. PhantomData doesn't implement Debug. If you try to #[derive(Debug)] on a struct with PhantomData, the compilation fails. You must implement Debug manually. This is a convention aside: the community expects you to handle this. It's a small friction point that reminds you PhantomData is a marker, not data.
use std::marker::PhantomData;
use std::fmt;
struct Wrapper<T> {
value: i32,
_marker: PhantomData<T>,
}
// Manual Debug implementation because PhantomData isn't Debug.
impl<T: fmt::Debug> fmt::Debug for Wrapper<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Wrapper")
.field("value", &self.value)
.finish()
}
}
When to use PhantomData
PhantomData is a tool for specific scenarios. Use it when the compiler's static analysis falls short of your runtime reality.
Use PhantomData<T> when you own T and will drop it. This applies to raw pointer wrappers, custom allocators, and any struct that manages the lifetime of T manually. The marker enforces Send/Sync bounds and triggers drop check.
Use PhantomData<&'a T> when you borrow T and won't drop it. This applies to view structs, iterators, and references that hide a lifetime behind a raw pointer. The marker enables covariance, allowing lifetime elision and substitution.
Use PhantomData<fn(T) -> T> when you need invariance in T without implying ownership. This is an advanced case. Function pointers are contravariant in their arguments. Wrapping T in a function type makes the marker invariant. Use this when you need to prevent variance but don't want the drop check associated with PhantomData<T>.
Use PhantomData to control auto-traits. If you want a struct to be Send only when T is Send, but you hold no fields of type T, add PhantomData<T>. The compiler uses the marker to propagate trait bounds.
Don't use PhantomData for documentation. It's not a comment. It changes type behavior. If you don't need variance control, drop check, or trait bounds, you don't need PhantomData.