Loading pageā¦
Rust walkthroughs
Loading pageā¦
std::marker::PhantomData and how is it used in type-level programming?std::marker::PhantomData<T> is a zero-sized type that allows a type to act as if it owns, references, or uses a type parameter T without actually storing any data of that type. It tells the compiler about the relationship between a struct and its type parameters, affecting variance, drop order, and trait implementations. Without PhantomData, unused type parameters would cause compilation errors, and types couldn't express ownership relationships that are important for memory safety. In type-level programming, PhantomData enables encoding compile-time invariants, carrying type information through computation graphs, and building APIs that are type-safe without runtime overhead.
// This doesn't compile
struct Container<T> {
data: Vec<u8>,
// T is never used - compiler error: parameter `T` is never used
}
// This compiles
use std::marker::PhantomData;
struct Container<T> {
data: Vec<u8>,
_marker: PhantomData<T>, // Tells compiler we "use" T
}
fn main() {
let c: Container<String> = Container {
data: vec![1, 2, 3],
_marker: PhantomData,
};
}PhantomData satisfies the compiler's requirement that type parameters be used.
use std::marker::PhantomData;
use std::mem::size_of;
struct WithPhantom<T> {
data: u32,
marker: PhantomData<T>,
}
struct WithoutPhantom {
data: u32,
}
fn main() {
println!("WithPhantom<String>: {} bytes", size_of::<WithPhantom<String>>());
println!("WithPhantom<u64>: {} bytes", size_of::<WithPhantom<u64>>());
println!("WithoutPhantom: {} bytes", size_of::<WithoutPhantom>());
// All three print 4 bytes (size of u32)
// PhantomData is truly zero-sized
}PhantomData<T> contributes zero bytes to struct size regardless of T.
use std::marker::PhantomData;
// Covariant in T (default PhantomData behavior)
struct Covariant<T> {
marker: PhantomData<T>,
}
// Contravariant in T
struct Contravariant<T> {
marker: PhantomData<fn(T)>,
}
// Invariant in T
struct Invariant<T> {
marker: PhantomData<fn(T) -> T>,
}
// Variance determines when one type can be used in place of another
fn covariance() {
let longer: Covariant<&'static str> = Covariant { marker: PhantomData };
let shorter: Covariant<&'static str> = longer; // &'static str can become &'a str
// &'static str is a subtype of &'a str (longer lifetime is more permissive)
// So Covariant<&'static str> can be assigned to Covariant<&'a str>
}
fn contravariance() {
// fn(&'static str) is a subtype of fn(&'a str)
// Contravariance flips the relationship
}
fn invariance() {
// Invariant types cannot be substituted at all
// Necessary for interior mutability and other patterns
}PhantomData<T> makes the containing type covariant in T by default.
use std::marker::PhantomData;
// Acts as if it owns a T
struct Owns<T> {
marker: PhantomData<T>,
}
// Acts as if it holds a reference to T
struct References<'a, T> {
marker: PhantomData<&'a T>,
}
// Acts as if it holds a mutable reference to T
struct MutReferences<'a, T> {
marker: PhantomData<&'a mut T>,
}
// Acts as if it holds a Box<T>
struct Boxes<T> {
marker: PhantomData<Box<T>>,
}
fn main() {
// Owns<T> drops T when it drops (if T: Drop)
// References<'a, T> requires T lives for 'a
// The marker affects lifetime bounds and drop behavior
}Different PhantomData types express different ownership relationships.
use std::marker::PhantomData;
struct DropCheck<T> {
marker: PhantomData<T>,
}
struct HasDrop;
impl Drop for HasDrop {
fn drop(&mut self) {
println!("HasDrop dropped");
}
}
fn main() {
// DropCheck<HasDrop> doesn't actually drop HasDrop
// PhantomData affects variance, not actual drop behavior
// But it tells the compiler about drop checking obligations
let _: DropCheck<HasDrop> = DropCheck { marker: PhantomData };
// HasDrop::drop is NOT called because there's no actual HasDrop
// However, DropCheck<HasDrop> cannot outlive HasDrop's validity
}PhantomData<T> affects drop check but doesn't actually drop T.
use std::marker::PhantomData;
// Tag for different units at type level
struct Meters;
struct Feet;
struct Distance<Unit> {
value: f64,
_marker: PhantomData<Unit>,
}
impl Distance<Meters> {
fn from_meters(m: f64) -> Self {
Distance { value: m, _marker: PhantomData }
}
}
impl Distance<Feet> {
fn from_feet(f: f64) -> Self {
Distance { value: f, _marker: PhantomData }
}
}
// Can only add same units
impl<Unit> std::ops::Add for Distance<Unit> {
type Output = Distance<Unit>;
fn add(self, other: Distance<Unit>) -> Distance<Unit> {
Distance {
value: self.value + other.value,
_marker: PhantomData,
}
}
}
// Conversions between units
impl Distance<Meters> {
fn to_feet(self) -> Distance<Feet> {
Distance {
value: self.value * 3.28084,
_marker: PhantomData,
}
}
}
fn main() {
let m1 = Distance::from_meters(10.0);
let m2 = Distance::from_meters(5.0);
let meters = m1 + m2; // OK: Distance<Meters>
let f1 = Distance::from_feet(30.0);
// let error = m1 + f1; // Won't compile: different units
let feet = meters.to_feet(); // OK: conversion
println!("{} meters", meters.value); // Actually errors - meters moved
}PhantomData enables type-safe unit arithmetic with zero runtime cost.
use std::marker::PhantomData;
// States
struct Idle;
struct Connected;
struct Authenticated;
struct Connection<State> {
stream: Option<String>, // Simplified - would be actual stream
_state: PhantomData<State>,
}
impl Connection<Idle> {
fn new() -> Self {
Connection {
stream: None,
_state: PhantomData,
}
}
fn connect(self, address: &str) -> Connection<Connected> {
Connection {
stream: Some(format!("connected to {}", address)),
_state: PhantomData,
}
}
}
impl Connection<Connected> {
fn authenticate(self, credentials: &str) -> Connection<Authenticated> {
Connection {
stream: self.stream,
_state: PhantomData,
}
}
fn disconnect(self) -> Connection<Idle> {
Connection {
stream: None,
_state: PhantomData,
}
}
}
impl Connection<Authenticated> {
fn send_data(&self, data: &str) {
println!("Sending: {}", data);
}
fn disconnect(self) -> Connection<Idle> {
Connection {
stream: None,
_state: PhantomData,
}
}
}
fn main() {
let conn = Connection::new();
// Cannot call send_data here - not authenticated
let conn = conn.connect("example.com");
// Cannot call send_data here - not authenticated
let conn = conn.authenticate("secret");
conn.send_data("Hello!"); // OK: now authenticated
// Type system enforces state machine transitions
}PhantomData carries state information without runtime cost.
use std::marker::PhantomData;
// Different index types
struct VertexIndex;
struct EdgeIndex;
struct Index<T> {
idx: usize,
_marker: PhantomData<T>,
}
struct Graph {
vertices: Vec<String>,
edges: Vec<(usize, usize)>,
}
impl Graph {
fn add_vertex(&mut self, name: String) -> Index<VertexIndex> {
let idx = self.vertices.len();
self.vertices.push(name);
Index { idx, _marker: PhantomData }
}
fn add_edge(&mut self, from: Index<VertexIndex>, to: Index<VertexIndex>) -> Index<EdgeIndex> {
let idx = self.edges.len();
self.edges.push((from.idx, to.idx));
Index { idx, _marker: PhantomData }
}
fn get_vertex(&self, idx: Index<VertexIndex>) -> &str {
&self.vertices[idx.idx]
}
fn get_edge(&self, idx: Index<EdgeIndex>) -> (usize, usize) {
self.edges[idx.idx]
}
}
fn main() {
let mut graph = Graph {
vertices: vec![],
edges: vec![],
};
let v1 = graph.add_vertex("A".to_string());
let v2 = graph.add_vertex("B".to_string());
let e1 = graph.add_edge(v1, v2);
// Type safety: can't mix up vertex and edge indices
// graph.get_vertex(e1); // Won't compile!
}PhantomData creates type-safe handles for internal indices.
use std::marker::PhantomData;
struct BorrowedBuffer<'a> {
ptr: *const u8,
len: usize,
_marker: PhantomData<&'a [u8]>,
}
impl<'a> BorrowedBuffer<'a> {
fn new(data: &'a [u8]) -> Self {
BorrowedBuffer {
ptr: data.as_ptr(),
len: data.len(),
_marker: PhantomData,
}
}
fn as_slice(&self) -> &'a [u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
}
fn main() {
let data = vec![1, 2, 3, 4, 5];
let buffer = BorrowedBuffer::new(&data);
// Lifetime is tracked correctly
// buffer cannot outlive data
}PhantomData carries lifetime information for raw pointers.
use std::marker::PhantomData;
use std::cell::UnsafeCell;
// Invariant cell - necessary for RefCell-like types
struct InvariantCell<T> {
value: UnsafeCell<T>,
// This makes InvariantCell invariant in T
_marker: PhantomData<fn(T) -> T>,
}
// Why invariance matters:
fn why_invariance() {
// If InvariantCell<&'static str> were covariant in &'static str,
// we could convert InvariantCell<&'static str> to InvariantCell<&'a str>
// then write a &'a str into it and read it back as &'static str
// This would be unsound!
// Invariance prevents this conversion
}PhantomData<fn(T) -> T> creates invariance for safe interior mutability.
use std::marker::PhantomData;
// Acts as if it contains T
struct Contains<T> {
marker: PhantomData<T>,
}
// T: Send implies Contains<T>: Send
// T: Sync implies Contains<T>: Sync
// Example: pointer wrapper
struct Pointer<T> {
ptr: *const T,
_marker: PhantomData<T>,
}
// *const T is not Send or Sync by default
// But PhantomData<T> makes Pointer<T> inherit Send/Sync from T
// This is correct because PhantomData<T> means "acts as if it owns T"
fn main() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Contains<i32>>();
assert_sync::<Contains<i32>>();
// Contains<*const i32> would not be Send/Sync
}PhantomData<T> inherits Send and Sync from T.
use std::marker::PhantomData;
// Pattern 1: Unused type parameter
struct Container<T> {
data: Vec<u8>,
_marker: PhantomData<T>,
}
// Pattern 2: Type-level state machine
struct StateA;
struct StateB;
struct Machine<S> {
data: i32,
_state: PhantomData<S>,
}
// Pattern 3: Type-safe handles
struct Handle<T> {
id: u32,
_marker: PhantomData<T>,
}
// Pattern 4: Lifetime carrying for raw pointers
struct RawSlice<'a, T> {
ptr: *const T,
len: usize,
_marker: PhantomData<&'a T>,
}
// Pattern 5: Invariance marker
struct Invariant<T> {
_marker: PhantomData<fn(T) -> T>,
}
// Pattern 6: Ownership assertion
struct Owns<T> {
_marker: PhantomData<Box<T>>,
}Each pattern serves a specific type-level programming purpose.
use std::marker::PhantomData;
use std::cell::Cell;
// Force not Send by including !Send type in PhantomData
struct NotSend<T> {
data: T,
_marker: PhantomData<*const ()>, // *const () is !Send
}
// Or use Cell which is !Sync
struct NotSync<T> {
data: T,
_marker: PhantomData<Cell<()>>, // Cell is !Sync
}
// Use PhantomData to add bounds
struct MustBeSend<T: Send> {
_marker: PhantomData<T>,
}
fn main() {
fn assert_send<T: Send>() {}
assert_send::<MustBeSend<i32>>();
// assert_send::<MustBeSend<*const i32>>(); // Won't compile
}PhantomData can control Send/Sync auto-traits.
| PhantomData<T> | Effect on containing type |
|------------------|--------------------------|
| PhantomData<T> | Covariant in T, owns T conceptually |
| PhantomData<&'a T> | Covariant in T and 'a, borrows T |
| PhantomData<&'a mut T> | Invariant in T, covariant in 'a |
| PhantomData<fn(T)> | Contravariant in T |
| PhantomData<fn(T) -> T> | Invariant in T |
| PhantomData<Box<T>> | Owns T, affects drop check |
| PhantomData<*const T> | Neither Send nor Sync |
PhantomData solves the fundamental problem of communicating type relationships to the compiler without runtime representation:
Variance specification: The compiler needs to know how type parameters relate to subtyping. PhantomData<T> makes the containing type covariant in T, the most permissive variance. PhantomData<fn(T)> makes it contravariant. PhantomData<fn(T) -> T> makes it invariant. This variance information determines when one generic type can be substituted for another.
Lifetime tracking: For types using raw pointers, FFI, or interior mutability, PhantomData carries lifetime information that raw types don't. A *const T has no lifetime, but PhantomData<&'a T> tells the compiler the containing type must not outlive 'a.
Type-level programming: PhantomData enables encoding invariants in the type system. Tag types like Meters vs Feet, state types like Idle vs Connected, and handle types like VertexIndex vs EdgeIndex all use PhantomData to carry type information with zero runtime cost. The type system enforces correctness at compile time.
Key insight: PhantomData is a bridge between the value-level world and the type-level world. It has no runtime representation but carries semantic information. Every zero-sized marker type in Rustā(), PhantomData, empty enumsāserves to make the type system more expressive without runtime cost. PhantomData specifically exists to express "this type has a relationship with T" when no actual T value exists. Use it when the compiler needs to know about type parameter relationships that aren't expressed through actual fields.