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
Tisn'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
