How do I work with PhantomData in Rust?

Walkthrough

PhantomData is a zero-sized type that tells the compiler your type "logically owns" a T, even though it doesn't physically contain one. It's a way to express type relationships, lifetime constraints, and ownership semantics in your types.

Key use cases:

  • Lifetime variance — Express how lifetimes relate to your type
  • Ownership markers — Indicate your type owns data of type T
  • Send/Sync traits — Control auto-trait implementation
  • Type parameters — Use type parameters that don't appear in fields
  • Drop behavior — Make your type participate in drop check

PhantomData has zero runtime cost — it compiles to nothing. It's purely a compile-time construct for expressing type system constraints.

Code Examples

Basic PhantomData Usage

use std::marker::PhantomData;
 
struct MyStruct<T> {
    _marker: PhantomData<T>,
    value: i32,
}
 
fn main() {
    let s: MyStruct<String> = MyStruct {
        _marker: PhantomData,
        value: 42,
    };
    
    println!("Value: {}", s.value);
    println!("Size of MyStruct: {} bytes", std::mem::size_of::<MyStruct<String>>());
    // Size is just the i32 (4 bytes), PhantomData is zero-sized
}

Using Type Parameters Without Fields

use std::marker::PhantomData;
 
// A generic type where T doesn't appear in any field
struct Container<T> {
    data: Vec<u8>,
    _marker: PhantomData<T>,
}
 
impl<T> Container<T> {
    fn new(data: Vec<u8>) -> Self {
        Self {
            data,
            _marker: PhantomData,
        }
    }
    
    fn into_inner(self) -> Vec<u8> {
        self.data
    }
}
 
// Type-safe API even though T isn't stored
struct Json;
struct Xml;
 
fn main() {
    let json: Container<Json> = Container::new(b"{\"key\": \"value\"}".to_vec());
    let xml: Container<Xml> = Container::new(b"<key>value</key>".to_vec());
    
    // Can't mix them up!
    fn process_json(c: Container<Json>) {
        println!("Processing JSON: {} bytes", c.data.len());
    }
    
    fn process_xml(c: Container<Xml>) {
        println!("Processing XML: {} bytes", c.data.len());
    }
    
    process_json(json);
    process_xml(xml);
}

Lifetime Relationships with PhantomData

use std::marker::PhantomData;
 
// A type that logically holds a reference but doesn't store it
struct Iter<'a, T> {
    ptr: *const T,
    len: usize,
    _marker: PhantomData<&'a T>, // Express the lifetime relationship
}
 
impl<'a, T> Iter<'a, T> {
    fn new(slice: &'a [T]) -> Self {
        Self {
            ptr: slice.as_ptr(),
            len: slice.len(),
            _marker: PhantomData,
        }
    }
    
    fn get(&self, index: usize) -> Option<&'a T> {
        if index < self.len {
            // Safe because we know ptr is valid for 'a
            Some(unsafe { &*self.ptr.add(index) })
        } else {
            None
        }
    }
}
 
fn main() {
    let data = vec![1, 2, 3, 4, 5];
    let iter = Iter::new(&data);
    
    println!("First: {:?}", iter.get(0));
    println!("Last: {:?}", iter.get(4));
}

Ownership Semantics

use std::marker::PhantomData;
 
// Different ownership semantics with PhantomData
 
// This type acts like it owns a T (affects drop check)
struct Owns<T> {
    ptr: *mut T,
    _marker: PhantomData<T>, // Owning semantics
}
 
// This type acts like it holds a reference to T
struct Borrows<'a, T> {
    ptr: *const T,
    _marker: PhantomData<&'a T>, // Borrowing semantics
}
 
// This type acts like it holds a mutable reference to T
struct MutBorrows<'a, T> {
    ptr: *mut T,
    _marker: PhantomData<&'a mut T>, // Mutable borrowing semantics
}
 
fn main() {
    let mut value = 42;
    
    let owns: Owns<i32> = Owns {
        ptr: &mut value,
        _marker: PhantomData,
    };
    
    let borrows: Borrows<i32> = Borrows {
        ptr: &value,
        _marker: PhantomData,
    };
    
    println!("Pointer addresses: {:?}, {:?}", owns.ptr, borrows.ptr);
}

Controlling Send and Sync

use std::marker::PhantomData;
 
// Not Send or Sync because of raw pointer
struct RawContainer {
    ptr: *mut i32,
}
 
// By adding PhantomData, we can control auto-traits
struct SendableContainer {
    ptr: *mut i32,
    _marker: PhantomData<i32>, // Makes it Send if i32: Send
}
 
// Note: This is unsafe! Raw pointers are not Send/Sync for safety reasons.
// Only do this if you know the pointer is thread-safe.
unsafe impl Send for SendableContainer {}
 
// To explicitly NOT be Send/Sync:
struct NotSend {
    _marker: PhantomData<*const i32>, // *const i32 is !Send
}
 
fn main() {
    fn check_send<T: Send>() {}
    fn check_sync<T: Sync>() {}
    
    check_send::<SendableContainer>();
    // check_send::<NotSend>(); // Would fail to compile
    
    println!("Container types created");
}

PhantomData for Variance

use std::marker::PhantomData;
 
// Covariant in T (can substitute T with a subtype)
struct Covariant<T> {
    _marker: PhantomData<fn() -> T>, // Covariant
}
 
// Contravariant in T (can substitute T with a supertype)
struct Contravariant<T> {
    _marker: PhantomData<fn(T)>, // Contravariant
}
 
// Invariant in T (cannot substitute at all)
struct Invariant<T> {
    _marker: PhantomData<fn(T) -> T>, // Invariant
}
 
fn main() {
    // &'a T is covariant in both 'a and T
    // This means &'static str can be used where &'short str is expected
    
    fn take_covariant<T>(_: Covariant<T>) {}
    fn take_contravariant<T>(_: Contravariant<T>) {}
    fn take_invariant<T>(_: Invariant<T>) {}
    
    let cov: Covariant<&'static str> = Covariant { _marker: PhantomData };
    take_covariant(cov); // Works
    
    println!("Variance examples compiled");
}

Drop Check with PhantomData

use std::marker::PhantomData;
 
struct NoDrop {
    data: i32,
}
 
impl Drop for NoDrop {
    fn drop(&mut self) {
        println!("NoDrop::drop called");
    }
}
 
// Without PhantomData, this wouldn't drop NoDrop
struct Wrapper {
    ptr: *mut NoDrop,
    _marker: PhantomData<NoDrop>, // Tells compiler we "own" NoDrop
}
 
impl Drop for Wrapper {
    fn drop(&mut self) {
        println!("Wrapper::drop called");
        unsafe {
            drop(Box::from_raw(self.ptr));
        }
    }
}
 
fn main() {
    let ptr = Box::into_raw(Box::new(NoDrop { data: 42 }));
    let wrapper = Wrapper {
        ptr,
        _marker: PhantomData,
    };
    
    // When wrapper is dropped, it will drop the NoDrop
    drop(wrapper);
}

Type-State Pattern

use std::marker::PhantomData;
 
// States
struct Empty;
struct Filled;
 
struct Buffer<State> {
    data: Vec<u8>,
    _state: PhantomData<State>,
}
 
impl Buffer<Empty> {
    fn new() -> Self {
        Self {
            data: Vec::new(),
            _state: PhantomData,
        }
    }
    
    fn fill(self, data: Vec<u8>) -> Buffer<Filled> {
        Buffer {
            data,
            _state: PhantomData,
        }
    }
}
 
impl Buffer<Filled> {
    fn data(&self) -> &[u8] {
        &self.data
    }
    
    fn clear(self) -> Buffer<Empty> {
        Buffer {
            data: Vec::new(),
            _state: PhantomData,
        }
    }
}
 
fn main() {
    let empty_buffer: Buffer<Empty> = Buffer::new();
    
    // Can't call data() on empty buffer!
    // empty_buffer.data(); // Compile error!
    
    let filled_buffer = empty_buffer.fill(vec![1, 2, 3, 4]);
    
    // Now we can access data
    println!("Data: {:?}", filled_buffer.data());
    
    let empty_again = filled_buffer.clear();
    // Can't call data() anymore!
    // empty_again.data(); // Compile error!
}

Unit Types with PhantomData

use std::marker::PhantomData;
 
// Different measurement units
struct Meters;
struct Feet;
struct Kilometers;
 
#[derive(Debug)]
struct Distance<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}
 
impl<Unit> Distance<Unit> {
    fn new(value: f64) -> Self {
        Self {
            value,
            _unit: PhantomData,
        }
    }
    
    fn value(&self) -> f64 {
        self.value
    }
}
 
impl Distance<Meters> {
    fn to_kilometers(self) -> Distance<Kilometers> {
        Distance::new(self.value / 1000.0)
    }
    
    fn to_feet(self) -> Distance<Feet> {
        Distance::new(self.value * 3.28084)
    }
}
 
fn main() {
    let meters = Distance::<Meters>::new(1000.0);
    println!("{:?} meters", meters.value());
    
    let kilometers = meters.to_kilometers();
    println!("{:?} kilometers", kilometers.value());
    
    let meters2 = Distance::<Meters>::new(100.0);
    let feet = meters2.to_feet();
    println!("{:?} feet", feet.value());
}

Implementing Smart Pointers

use std::marker::PhantomData;
use std::ops::Deref;
use std::ptr::NonNull;
 
struct MyBox<T> {
    ptr: NonNull<T>,
    _marker: PhantomData<T>,
}
 
impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        let ptr = NonNull::new(Box::into_raw(Box::new(value))).unwrap();
        Self {
            ptr,
            _marker: PhantomData,
        }
    }
}
 
impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &T {
        unsafe { self.ptr.as_ref() }
    }
}
 
impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        unsafe {
            drop(Box::from_raw(self.ptr.as_ptr()));
        }
    }
}
 
fn main() {
    let boxed = MyBox::new(42);
    println!("Value: {}", *boxed);
}

Foreign Function Interface (FFI)

use std::marker::PhantomData;
use std::ffi::c_void;
 
// Opaque handle from C library
type c_handle = *mut c_void;
 
// Type-safe wrapper around C handle
struct Handle<T> {
    raw: c_handle,
    _marker: PhantomData<T>,
}
 
// Different resource types
struct FileHandle;
struct NetworkHandle;
 
impl<T> Handle<T> {
    unsafe fn from_raw(raw: c_handle) -> Self {
        Self {
            raw,
            _marker: PhantomData,
        }
    }
    
    fn as_raw(&self) -> c_handle {
        self.raw
    }
}
 
// Type-specific operations
impl Handle<FileHandle> {
    fn read(&self, buf: &mut [u8]) -> usize {
        // Call C function: c_file_read(self.raw, buf.ptr, buf.len)
        0
    }
}
 
impl Handle<NetworkHandle> {
    fn send(&self, data: &[u8]) {
        // Call C function: c_network_send(self.raw, data.ptr, data.len)
    }
}
 
fn main() {
    // Type-safe handles prevent mixing different resource types
    let file: Handle<FileHandle> = unsafe { Handle::from_raw(std::ptr::null_mut()) };
    let net: Handle<NetworkHandle> = unsafe { Handle::from_raw(std::ptr::null_mut()) };
    
    // Can't call send() on file handle!
    // file.send(&[]); // Compile error!
    
    println!("Handles created with type safety");
}

Common Patterns Summary

use std::marker::PhantomData;
 
fn main() {
    // 1. Unused type parameter
    struct Generic<T> {
        _marker: PhantomData<T>,
    }
    
    // 2. Covariant lifetime
    struct CovariantLifetime<'a> {
        _marker: PhantomData<&'a ()>,
    }
    
    // 3. Invariant lifetime (for mutability)
    struct InvariantLifetime<'a> {
        _marker: PhantomData<&'a mut ()>,
    }
    
    // 4. Ownership semantics
    struct Owns<T> {
        _marker: PhantomData<T>,
    }
    
    // 5. Not Send/Sync
    struct NotThreadSafe {
        _marker: PhantomData<*const ()>,
    }
    
    // 6. Type state
    struct Stateful<S> {
        _marker: PhantomData<S>,
    }
    
    println!("PhantomData patterns demonstrated");
}

Summary

PhantomData Variants and Their Effects:

Form Variance Send/Sync Drop Check
PhantomData<T> Covariant Inherits from T Yes
PhantomData<&'a T> Covariant Inherits from T No
PhantomData<&'a mut T> Invariant Inherits from T No
PhantomData<fn() -> T> Covariant Always No
PhantomData<fn(T)> Contravariant Always No
PhantomData<fn(T) -> T> Invariant Always No
PhantomData<*const T> Covariant !Send, !Sync No

When to Use PhantomData:

Use Case Pattern
Unused type parameter PhantomData<T>
Express lifetime PhantomData<&'a T>
Make invariant PhantomData<Cell<T>> or PhantomData<fn(T) -> T>
Not Send/Sync PhantomData<*const T> or PhantomData<Rc<T>>
Ownership semantics PhantomData<T>

Key Points:

  • PhantomData<T> is zero-sized (no runtime cost)
  • It tells the compiler your type "acts like" it contains a T
  • Controls variance, Send/Sync, and drop behavior
  • Essential for raw pointer wrappers and FFI
  • Use for type-state patterns and unit type distinctions
  • Different PhantomData forms have different semantic effects
  • Always document why PhantomData is being used