What is the role of std::marker::PhantomData and how is it used in type-level programming?

PhantomData<T> is a zero-sized type that allows you to indicate that a type logically owns or holds a T without actually storing any data of that type. The compiler treats PhantomData<T> as if the type contains a T for the purposes of variance, drop checking, and trait bounds, even though no memory is allocated for it. This is essential in type-level programming for representing ownership relationships, enforcing variance semantics, and carrying type parameters through structures without runtime overhead. The pattern enables sophisticated type system guarantees while maintaining zero-cost abstractions.

Basic PhantomData Usage

use std::marker::PhantomData;
 
struct Container<T> {
    data: String,
    // Pretend we hold a T without actually storing one
    _phantom: PhantomData<T>,
}
 
fn main() {
    let container: Container<i32> = Container {
        data: "hello".to_string(),
        _phantom: PhantomData,
    };
    
    println!("Size: {}", std::mem::size_of::<Container<i32>>());
    // Size: 24 (just the String, PhantomData adds nothing)
}

PhantomData is zero-sized—it adds no runtime overhead.

Carrying Type Parameters

use std::marker::PhantomData;
 
// A struct that needs a type parameter but doesn't store values of that type
struct Parser<T> {
    input: String,
    _output: PhantomData<T>,
}
 
impl<T> Parser<T> {
    fn new(input: String) -> Self {
        Parser {
            input,
            _output: PhantomData,
        }
    }
}
 
// Different type parameter = different type, even if stored data is identical
fn main() {
    let int_parser: Parser<i32> = Parser::new("42".to_string());
    let str_parser: Parser<String> = Parser::new("hello".to_string());
    
    // int_parser and str_parser are different types
    // This enables type-level distinctions at compile time
}

PhantomData carries type information without storage cost.

Variance and PhantomData

use std::marker::PhantomData;
 
// PhantomData<T> makes Container covariant in T
struct Covariant<T> {
    _marker: PhantomData<T>,
}
 
// PhantomData<fn(T)> makes Container contravariant in T
struct Contravariant<T> {
    _marker: PhantomData<fn(T)>,
}
 
// PhantomData<fn(T) -> T> makes Container invariant in T
struct Invariant<T> {
    _marker: PhantomData<fn(T) -> T>,
}
 
fn main() {
    // Covariant: &'a T can be used where &'static T is expected
    // Contravariant: fn(&'static T) can be used where fn(&'a T) is expected
    // Invariant: no substitution allowed
    
    fn covariance() {
        fn longer_lifetime(_: &String) {}
        fn shorter_lifetime(_: &String) {}
        // Lifetime can be shortened: covariant
    }
}

PhantomData controls variance through its type parameter.

Covariance Example

use std::marker::PhantomData;
 
// By default, PhantomData<T> is covariant
struct RefHolder<'a, T> {
    _marker: PhantomData<&'a T>,
}
 
fn use_longer_lifetime<'long, 'short>(holder: RefHolder<'long, String>)
where
    'long: 'short,
{
    // Can use RefHolder<'long, T> where RefHolder<'short, T> is expected
    let _: RefHolder<'short, String> = holder;
}
 
fn main() {
    let holder: RefHolder<'static, String> = RefHolder { _marker: PhantomData };
    use_longer_lifetime(holder);
}

Covariance allows substituting longer lifetimes for shorter ones.

Invariance Example

use std::marker::PhantomData;
 
// fn(T) -> T makes T invariant
struct Cell<T> {
    value: T,
}
 
// Invariant wrapper prevents lifetime narrowing
struct InvariantCell<'a, T> {
    _marker: PhantomData<fn(&'a T) -> &'a T>,
}
 
fn main() {
    // Invariant types cannot have their lifetimes substituted
    // This prevents certain classes of bugs with mutable references
}

Invariant types prevent lifetime substitution.

Drop Checking with PhantomData

use std::marker::PhantomData;
 
// Without PhantomData, compiler doesn't know T might need drop
struct Wrapper<T> {
    ptr: *mut T,
    _marker: PhantomData<T>,  // Tells drop checker we own T
}
 
impl<T> Drop for Wrapper<T> {
    fn drop(&mut self) {
        unsafe {
            // Drop the owned T
            std::ptr::drop_in_place(self.ptr);
        }
    }
}
 
fn main() {
    // PhantomData<T> ensures T's drop glue runs when Wrapper drops
}

PhantomData<T> signals ownership for drop checking.

PhantomData for Lifetimes

use std::marker::PhantomData;
 
struct BorrowedData<'a> {
    // Doesn't actually store references, but lifetime matters
    data: Vec<u8>,
    _marker: PhantomData<&'a ()>,  // Covariant in 'a
}
 
impl<'a> BorrowedData<'a> {
    fn new(data: Vec<u8>) -> Self {
        BorrowedData {
            data,
            _marker: PhantomData,
        }
    }
}
 
fn main() {
    let data = BorrowedData::new(vec![1, 2, 3]);
    // Lifetime 'a is inferred but doesn't constrain anything
    // Useful for APIs that logically depend on lifetimes
}

PhantomData can carry lifetime parameters.

Type State Pattern

use std::marker::PhantomData;
 
// Type states for a connection
struct Disconnected;
struct Connected;
struct Authenticated;
 
struct Connection<State> {
    address: String,
    _state: PhantomData<State>,
}
 
impl Connection<Disconnected> {
    fn new(address: String) -> Self {
        Connection {
            address,
            _state: PhantomData,
        }
    }
    
    fn connect(self) -> Connection<Connected> {
        println!("Connecting to {}...", self.address);
        Connection {
            address: self.address,
            _state: PhantomData,
        }
    }
}
 
impl Connection<Connected> {
    fn authenticate(self, token: &str) -> Connection<Authenticated> {
        println!("Authenticating with {}...", token);
        Connection {
            address: self.address,
            _state: PhantomData,
        }
    }
}
 
impl Connection<Authenticated> {
    fn send(&self, message: &str) {
        println!("Sending to {}: {}", self.address, message);
    }
}
 
fn main() {
    let conn = Connection::<Disconnected>::new("server.example.com".to_string());
    let conn = conn.connect();
    let conn = conn.authenticate("secret");
    conn.send("Hello, World!");
    
    // Cannot call send() on Disconnected or Connected
    // Cannot call authenticate() on Disconnected
}

PhantomData enables type-state patterns with zero runtime cost.

Builder Pattern with Type State

use std::marker::PhantomData;
 
// Marker types for required fields
struct NoName;
struct HasName;
struct NoEmail;
struct HasEmail;
 
struct UserBuilder<Name, Email> {
    name: Option<String>,
    email: Option<String>,
    _name: PhantomData<Name>,
    _email: PhantomData<Email>,
}
 
impl UserBuilder<NoName, NoEmail> {
    fn new() -> Self {
        UserBuilder {
            name: None,
            email: None,
            _name: PhantomData,
            _email: PhantomData,
        }
    }
}
 
impl<Email> UserBuilder<NoName, Email> {
    fn name(self, name: impl Into<String>) -> UserBuilder<HasName, Email> {
        UserBuilder {
            name: Some(name.into()),
            email: self.email,
            _name: PhantomData,
            _email: PhantomData,
        }
    }
}
 
impl<Name> UserBuilder<Name, NoEmail> {
    fn email(self, email: impl Into<String>) -> UserBuilder<Name, HasEmail> {
        UserBuilder {
            name: self.name,
            email: Some(email.into()),
            _name: PhantomData,
            _email: PhantomData,
        }
    }
}
 
impl UserBuilder<HasName, HasEmail> {
    fn build(self) -> User {
        User {
            name: self.name.unwrap(),
            email: self.email.unwrap(),
        }
    }
}
 
struct User {
    name: String,
    email: String,
}
 
fn main() {
    // This compiles
    let user = UserBuilder::new()
        .name("Alice")
        .email("alice@example.com")
        .build();
    
    // This would not compile - missing email
    // let user = UserBuilder::new()
    //     .name("Alice")
    //     .build();
}

PhantomData tracks required fields at compile time.

FFI and PhantomData

use std::marker::PhantomData;
 
// Opaque handle from FFI
struct FfiHandle {
    _private: (),
}
 
// Type-safe wrapper using PhantomData
struct Handle<T> {
    raw: *mut FfiHandle,
    _marker: PhantomData<T>,
}
 
impl<T> Handle<T> {
    fn new(raw: *mut FfiHandle) -> Self {
        Handle {
            raw,
            _marker: PhantomData,
        }
    }
}
 
// Different types cannot be mixed up
struct FileHandle;
struct NetworkHandle;
 
fn read_file(handle: &Handle<FileHandle>) { }
fn read_network(handle: &Handle<NetworkHandle>) { }
 
fn main() {
    // let file: Handle<FileHandle> = Handle::new(ptr);
    // let network: Handle<NetworkHandle> = Handle::new(ptr2);
    
    // read_file(&network);  // Compile error!
}

PhantomData adds type safety to opaque FFI handles.

Thread Safety Markers

use std::marker::PhantomData;
 
// PhantomData can help with Send/Sync markers
struct NotThreadSafe {
    _marker: PhantomData<*const ()>,  // *const () is !Send + !Sync
}
 
// This type is now automatically !Send and !Sync
 
struct ThreadSafe {
    _marker: PhantomData<()>,  // () is Send + Sync
}
 
// This type is Send + Sync
 
fn main() {
    fn assert_send<T: Send>() {}
    fn assert_sync<T: Sync>() {}
    
    assert_send::<ThreadSafe>();
    assert_sync::<ThreadSafe>();
    
    // These would not compile:
    // assert_send::<NotThreadSafe>();
    // assert_sync::<NotThreadSafe>();
}

PhantomData influences automatic Send and Sync implementations.

Owning References with PhantomData

use std::marker::PhantomData;
 
// Represents owning a T without storing it
struct OwningRef<Owner, T> {
    owner: Owner,
    _marker: PhantomData<T>,
}
 
impl<Owner, T> OwningRef<Owner, T> {
    fn new(owner: Owner) -> Self {
        OwningRef {
            owner,
            _marker: PhantomData,
        }
    }
}
 
fn main() {
    let owner = vec![1, 2, 3];
    let _ref: OwningRef<Vec<i32>, i32> = OwningRef::new(owner);
    // OwningRef owns the Vec, and logically references i32
}

PhantomData models ownership relationships in type system.

Avoiding False Drop Checks

use std::marker::PhantomData;
 
// Without PhantomData<*const T>, drop check assumes T must be dropped
// With PhantomData<*const T>, drop check knows we just hold a pointer
 
struct Pointer<T> {
    ptr: *const T,
    _marker: PhantomData<*const T>,  // Non-owning pointer semantics
}
 
// Alternative: non-owning marker
struct Pointer2<T> {
    ptr: *const T,
    _marker: PhantomData<*mut T>,  // More explicit about pointer-ness
}
 
fn main() {
    // Drop check behavior differs based on PhantomData type
}

PhantomData<*const T> signals non-owning pointer semantics.

Generic Newtype Pattern

use std::marker::PhantomData;
 
// Newtype for type-safe IDs
struct Id<T> {
    value: u64,
    _marker: PhantomData<T>,
}
 
impl<T> Id<T> {
    fn new(value: u64) -> Self {
        Id {
            value,
            _marker: PhantomData,
        }
    }
    
    fn get(&self) -> u64 {
        self.value
    }
}
 
// Different ID types cannot be mixed
struct User;
struct Post;
struct Comment;
 
fn main() {
    let user_id: Id<User> = Id::new(1);
    let post_id: Id<Post> = Id::new(2);
    
    // These are different types
    fn get_user(id: Id<User>) { }
    fn get_post(id: Id<Post>) { }
    
    // get_user(post_id);  // Compile error!
    // get_post(user_id);  // Compile error!
    
    println!("User ID: {}", user_id.get());
    println!("Post ID: {}", post_id.get());
}

PhantomData creates distinct types for each entity.

Smart Pointers with PhantomData

use std::marker::PhantomData;
use std::ops::Deref;
 
struct Rc<T> {
    ptr: *const RcInner<T>,
    _marker: PhantomData<T>,  // We logically own T
}
 
struct RcInner<T> {
    count: std::cell::Cell<usize>,
    data: T,
}
 
impl<T> Rc<T> {
    fn new(value: T) -> Self {
        let inner = Box::new(RcInner {
            count: std::cell::Cell::new(1),
            data: value,
        });
        Rc {
            ptr: Box::into_raw(inner),
            _marker: PhantomData,
        }
    }
}
 
impl<T> Deref for Rc<T> {
    type Target = T;
    
    fn deref(&self) -> &Self::Target {
        unsafe { &(*self.ptr).data }
    }
}
 
impl<T> Drop for Rc<T> {
    fn drop(&mut self) {
        unsafe {
            let inner = &*self.ptr;
            let count = inner.count.get();
            if count == 1 {
                std::ptr::drop_in_place(self.ptr as *mut RcInner<T>);
            } else {
                inner.count.set(count - 1);
            }
        }
    }
}

PhantomData<T> signals that Rc owns T for drop check.

Zero-Cost Abstraction

use std::marker::PhantomData;
 
fn main() {
    struct Empty<T> {
        _marker: PhantomData<T>,
    }
    
    struct ContainsData {
        data: u64,
    }
    
    println!("PhantomData<i32>: {}", std::mem::size_of::<Empty<i32>>());
    // 0 bytes
    
    println!("ContainsData: {}", std::mem::size_of::<ContainsData>());
    // 8 bytes
    
    // PhantomData is truly zero-cost at runtime
}

PhantomData has zero size—it's purely compile-time information.

Covariance Summary

use std::marker::PhantomData;
 
// Covariant in T: can substitute T with subtype
// PhantomData<T> is covariant
struct Covariant<T> {
    _marker: PhantomData<T>,
}
 
// Contravariant in T: can substitute T with supertype
// PhantomData<fn(T)> is contravariant
struct Contravariant<T> {
    _marker: PhantomData<fn(T)>,
}
 
// Invariant in T: no substitution allowed
// PhantomData<fn(T) -> T> is invariant
struct Invariant<T> {
    _marker: PhantomData<fn(T) -> T>,
}
 
// For references: PhantomData<&'a T> is covariant in both 'a and T
// For mutable refs: invariance is often needed

Understanding variance helps choose the right PhantomData parameter.

Synthesis

PhantomData<T> is a bridge between compile-time type information and runtime representation:

Core purpose: Signal to the compiler that your type logically interacts with T—affecting variance, drop checking, Send/Sync auto-traits, and type coherence—without storing T at runtime. This is zero-cost: no memory overhead.

Type-level programming applications:

  • Type states: Encode state machines in types, ensuring only valid operations compile
  • Type-safe IDs: Prevent mixing identifiers for different entities
  • FFI handles: Add type safety to opaque pointers
  • Ownership modeling: Clarify whether you own, borrow, or reference data
  • Lifetime threading: Carry lifetime parameters through structs that don't directly contain references

Variance control: The type parameter of PhantomData determines variance. PhantomData<T> is covariant; PhantomData<fn(T)> is contravariant; PhantomData<fn(T) -> T> is invariant. Choose based on the semantics your type needs.

Best practices:

  • Name the field _marker or _phantom to indicate intent
  • Consider what drop behavior you need—PhantomData<T> suggests ownership
  • For non-owning pointers, use PhantomData<*const T> to avoid false drop obligations
  • Use PhantomData<fn() -> T> or similar patterns to express non-owning, non-covariant relationships