How do I work with PhantomData in Rust?

Walkthrough

PhantomData is a zero-sized type marker that tells the compiler your struct acts as if it owns or references a value of type T, even though it doesn't actually store one. It's essential for expressing type relationships without runtime overhead.

Key use cases:

  • Lifetime parameters — When you have a lifetime parameter not used in fields
  • Type parameters — When generic type T isn't stored directly
  • Ownership semantics — Indicating drop order and Send/Sync behavior
  • FFI — Representing opaque types with proper semantics

Why PhantomData matters:

  • Zero-sized (no runtime overhead)
  • Affects drop order
  • Affects Send/Sync auto-traits
  • Enables variance annotations

Code Examples

Basic PhantomData Usage

use std::marker::PhantomData;
 
struct Container<T> {
    data: Vec<u8>,
    _marker: PhantomData<T>,  // Zero-sized, but tells compiler about T
}
 
impl<T> Container<T> {
    fn new() -> Self {
        Self {
            data: Vec::new(),
            _marker: PhantomData,
        }
    }
    
    fn push(&mut self, byte: u8) {
        self.data.push(byte);
    }
}
 
fn main() {
    let int_container: Container<i32> = Container::new();
    let str_container: Container<String> = Container::new();
    
    println!("Size of Container<i32>: {}", std::mem::size_of::<Container<i32>>());
    println!("Size of Vec<u8>: {}", std::mem::size_of::<Vec<u8>>());
    // Same size - PhantomData is zero-sized!
}

PhantomData with Lifetimes

use std::marker::PhantomData;
 
// A reference-like type that stores an index instead of a reference
struct SliceRef<'a, T> {
    ptr: *const T,     // Raw pointer - no lifetime info
    len: usize,
    _lifetime: PhantomData<&'a T>,  // Tells compiler about the lifetime
}
 
impl<'a, T> SliceRef<'a, T> {
    fn new(slice: &'a [T]) -> Self {
        Self {
            ptr: slice.as_ptr(),
            len: slice.len(),
            _lifetime: PhantomData,
        }
    }
    
    fn get(&self, index: usize) -> Option<&'a T> {
        if index < self.len {
            unsafe { Some(&*self.ptr.add(index)) }
        } else {
            None
        }
    }
}
 
fn main() {
    let data = vec![1, 2, 3, 4, 5];
    let slice_ref = SliceRef::new(&data);
    
    println!("Element 0: {:?}", slice_ref.get(0));
    println!("Element 2: {:?}", slice_ref.get(2));
}

PhantomData for Ownership Semantics

use std::marker::PhantomData;
 
// Acts like it owns a T (affects drop order)
struct OwningHandle<T> {
    handle: *mut std::ffi::c_void,
    _marker: PhantomData<T>,  // Treated as owning T
}
 
impl<T> Drop for OwningHandle<T> {
    fn drop(&mut self) {
        println!("Dropping handle, freeing T");
        // In real code: free the handle and drop T
    }
}
 
// Acts like it references T (covariant)
struct BorrowingHandle<'a, T> {
    handle: *mut std::ffi::c_void,
    _marker: PhantomData<&'a T>,  // Treated as borrowing T
}
 
fn main() {
    let owning: OwningHandle<String> = OwningHandle {
        handle: std::ptr::null_mut(),
        _marker: PhantomData,
    };
    
    // owning will be dropped, triggering Drop impl
    println!("Created owning handle");
}

Implementing Smart Pointers

use std::marker::PhantomData;
use std::ops::Deref;
use std::ptr::NonNull;
 
struct RcBox<T> {
    value: T,
    ref_count: usize,
}
 
pub struct Rc<T> {
    ptr: NonNull<RcBox<T>>,
    _marker: PhantomData<RcBox<T>>,  // Indicates ownership
}
 
impl<T> Rc<T> {
    pub fn new(value: T) -> Self {
        let boxed = Box::new(RcBox {
            value,
            ref_count: 1,
        });
        Self {
            ptr: unsafe { NonNull::new_unchecked(Box::into_raw(boxed)) },
            _marker: PhantomData,
        }
    }
}
 
impl<T> Deref for Rc<T> {
    type Target = T;
    
    fn deref(&self) -> &T {
        unsafe { &self.ptr.as_ref().value }
    }
}
 
impl<T> Drop for Rc<T> {
    fn drop(&mut self) {
        unsafe {
            let rc_box = self.ptr.as_mut();
            rc_box.ref_count -= 1;
            if rc_box.ref_count == 0 {
                drop(Box::from_raw(self.ptr.as_ptr()));
            }
        }
    }
}
 
fn main() {
    let rc = Rc::new(String::from("Hello"));
    println!("Value: {}", *rc);
}

PhantomData and Send/Sync

use std::marker::PhantomData;
 
// By default, this struct is Send + Sync if T is
struct Wrapper<T> {
    data: Vec<u8>,
    _marker: PhantomData<T>,
}
 
// Make it not Send by using PhantomData<*const T>
// Raw pointers are !Send
struct NotSend<T> {
    data: Vec<u8>,
    _marker: PhantomData<*const T>,  // Makes struct !Send
}
 
// Check Send/Sync at compile time
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
 
fn main() {
    // Wrapper is Send + Sync if T is
    assert_send::<Wrapper<i32>>();
    assert_sync::<Wrapper<i32>>();
    
    // NotSend is not Send
    // assert_send::<NotSend<i32>>();  // Would not compile!
    
    println!("Send/Sync assertions passed!");
}

Variance with PhantomData

use std::marker::PhantomData;
 
// Covariant in T (can use &'a T where &'static T expected)
struct Covariant<T> {
    _marker: PhantomData<T>,
}
 
// Contravariant in T (can use fn(&'static T) where fn(&'a T) expected)
struct Contravariant<T> {
    _marker: PhantomData<fn(T)>,
}
 
// Invariant in T (cannot substitute lifetimes)
struct Invariant<T> {
    _marker: PhantomData<fn(T) -> T>,
}
 
// Example: Cell<T> needs to be invariant
use std::cell::Cell;
 
struct MyCell<T> {
    value: T,
}
 
// To make it invariant like Cell:
struct InvariantCell<T> {
    value: T,
    _marker: PhantomData<Cell<T>>,  // Cell<T> is invariant
}
 
fn main() {
    // Covariant: &'a T can become &'static T
    let cov: Covariant<&'static str> = Covariant::<&str> { _marker: PhantomData };
    
    println!("Variance demonstrated");
}

Type-Safe IDs

use std::marker::PhantomData;
 
struct Id<T> {
    id: u64,
    _marker: PhantomData<T>,
}
 
impl<T> Id<T> {
    fn new(id: u64) -> Self {
        Self {
            id,
            _marker: PhantomData,
        }
    }
    
    fn get(&self) -> u64 {
        self.id
    }
}
 
struct User;
struct Product;
struct Order;
 
// Type-safe IDs - can't mix them up!
fn get_user(id: Id<User>) -> String {
    format!("User #{}", id.get())
}
 
fn get_product(id: Id<Product>) -> String {
    format!("Product #{}", id.get())
}
 
fn main() {
    let user_id = Id::<User>::new(42);
    let product_id = Id::<Product>::new(123);
    
    println!("{}", get_user(user_id));
    println!("{}", get_product(product_id));
    
    // This would not compile:
    // get_user(product_id);  // Type mismatch!
}

FFI Opaque Types

use std::marker::PhantomData;
use std::ffi::c_void;
 
// Represents an opaque FFI type
struct FfiType {
    _private: c_void,  // Cannot be constructed
}
 
// Handle to the opaque type
struct FfiHandle<'a> {
    ptr: *mut c_void,
    _marker: PhantomData<&'a FfiType>,  // Borrows the FFI type
}
 
impl<'a> FfiHandle<'a> {
    // Simulated FFI function
    fn get_data(&self) -> i32 {
        // In real code: call FFI function
        unsafe { *(self.ptr as *const i32) }
    }
}
 
// Owned handle (takes ownership)
struct OwnedFfiHandle {
    ptr: *mut c_void,
    _marker: PhantomData<Box<FfiType>>,  // Owns the FFI type
}
 
impl Drop for OwnedFfiHandle {
    fn drop(&mut self) {
        println!("Freeing FFI resource");
        // In real code: call FFI free function
    }
}
 
fn main() {
    let mut value: i32 = 42;
    let handle = FfiHandle {
        ptr: &mut value as *mut i32 as *mut c_void,
        _marker: PhantomData,
    };
    
    println!("Data: {}", handle.get_data());
}

Builder Pattern with State

use std::marker::PhantomData;
 
// State markers
struct Pending;
struct Ready;
struct Sent;
 
struct Request<State> {
    data: String,
    _state: PhantomData<State>,
}
 
impl Request<Pending> {
    fn new(data: &str) -> Self {
        Self {
            data: data.to_string(),
            _state: PhantomData,
        }
    }
    
    fn prepare(self) -> Request<Ready> {
        Request {
            data: self.data,
            _state: PhantomData,
        }
    }
}
 
impl Request<Ready> {
    fn send(self) -> Request<Sent> {
        println!("Sending: {}", self.data);
        Request {
            data: self.data,
            _state: PhantomData,
        }
    }
}
 
impl Request<Sent> {
    fn response(&self) -> &str {
        "OK"
    }
}
 
fn main() {
    let request = Request::<Pending>::new("Hello")
        .prepare()
        .send();
    
    println!("Response: {}", request.response());
    
    // Can't call prepare() on Ready or send() on Pending:
    // Request::<Pending>::new("test").send();  // Won't compile!
}

Type-Erased Storage

use std::marker::PhantomData;
 
struct AnyBox {
    data: Box<dyn std::any::Any>,
}
 
impl AnyBox {
    fn new<T: 'static>(value: T) -> Self {
        Self {
            data: Box::new(value),
        }
    }
    
    fn get<T: 'static>(&self) -> Option<&T> {
        self.data.downcast_ref::<T>()
    }
}
 
// Type-safe wrapper around AnyBox
struct TypedBox<T: 'static> {
    inner: AnyBox,
    _marker: PhantomData<T>,
}
 
impl<T: 'static> TypedBox<T> {
    fn new(value: T) -> Self {
        Self {
            inner: AnyBox::new(value),
            _marker: PhantomData,
        }
    }
    
    fn get(&self) -> &T {
        self.inner.get::<T>().unwrap()
    }
}
 
fn main() {
    let typed = TypedBox::new(42i32);
    println!("Value: {}", typed.get());
    
    let str_box = TypedBox::new(String::from("Hello"));
    println!("String: {}", str_box.get());
}

Zero-Cost Abstractions

use std::marker::PhantomData;
 
// Different iteration strategies
struct Forward;
struct Backward;
 
struct Iter<T, Direction> {
    data: Vec<T>,
    index: usize,
    _direction: PhantomData<Direction>,
}
 
impl<T> Iter<T, Forward> {
    fn new(data: Vec<T>) -> Self {
        Self {
            data,
            index: 0,
            _direction: PhantomData,
        }
    }
}
 
impl<T> Iter<T, Backward> {
    fn new(data: Vec<T>) -> Self {
        Self {
            data,
            index: data.len(),
            _direction: PhantomData,
        }
    }
}
 
impl<T: Clone> Iterator for Iter<T, Forward> {
    type Item = T;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.index < self.data.len() {
            let item = self.data[self.index].clone();
            self.index += 1;
            Some(item)
        } else {
            None
        }
    }
}
 
fn main() {
    let data = vec![1, 2, 3, 4, 5];
    
    let forward: Iter<i32, Forward> = Iter::new(data.clone());
    for item in forward {
        println!("Forward: {}", item);
    }
    
    println!("Size of Iter<i32, Forward>: {}", std::mem::size_of::<Iter<i32, Forward>>());
    println!("Size of Iter<i32, Backward>: {}", std::mem::size_of::<Iter<i32, Backward>>());
    // Same size - PhantomData is zero-sized!
}

Thread Safety Markers

use std::marker::PhantomData;
 
// Marker trait for thread-safe types
trait ThreadSafe {}
 
// Thread-local only type
struct ThreadLocal<T> {
    data: T,
    _marker: PhantomData<*const ()>,  // !Send + !Sync
}
 
// Thread-safe wrapper
struct ThreadSafeWrapper<T: ThreadSafe> {
    data: T,
    _marker: PhantomData<fn(T)>,  // Doesn't affect Send/Sync
}
 
impl<T: ThreadSafe + Send> ThreadSafeWrapper<T> {
    fn new(data: T) -> Self {
        Self {
            data,
            _marker: PhantomData,
        }
    }
}
 
// Example thread-safe type
struct SafeData(i32);
impl ThreadSafe for SafeData {}
 
fn main() {
    fn assert_send<T: Send>() {}
    
    // ThreadSafeWrapper is Send if T is Send
    assert_send::<ThreadSafeWrapper<SafeData>>();
    
    println!("Thread safety verified!");
}

Summary

PhantomData Patterns:

Pattern PhantomData Type Effect
Owns T PhantomData<T> Covariant, affects drop
Borrows T PhantomData<&'a T> Covariant, lifetime bound
!Send PhantomData<*const T> Makes struct !Send
Contravariant PhantomData<fn(T)> Contravariant in T
Invariant PhantomData<Cell<T>> Invariant in T

Common Use Cases:

Use Case Example
Unused type parameter struct Foo<T> { _marker: PhantomData<T> }
Unused lifetime struct Bar<'a> { _marker: PhantomData<&'a ()> }
Type-safe IDs Id<User> vs Id<Product>
State machines Request<Ready> vs Request<Sent>
FFI handles Handle<'a> with lifetime

Variance Summary:

PhantomData Variance in T
PhantomData<T> Covariant
PhantomData<&'a T> Covariant
PhantomData<&'a mut T> Covariant
PhantomData<*const T> Covariant
PhantomData<fn(T)> Contravariant
PhantomData<fn(T) -> U> Contravariant in T, covariant in U
PhantomData<fn(T) -> T> Invariant in T
PhantomData<Cell<T>> Invariant in T

Key Points:

  • PhantomData is zero-sized (no runtime cost)
  • Tells compiler about type relationships
  • Affects drop order, Send/Sync, and variance
  • Use _marker: PhantomData<T> naming convention
  • Essential for smart pointers and FFI
  • Enables type-state patterns
  • Use PhantomData<*const T> to make types !Send