Loading pageā¦
Rust walkthroughs
Loading pageā¦
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 neededUnderstanding variance helps choose the right PhantomData parameter.
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:
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:
_marker or _phantom to indicate intentPhantomData<T> suggests ownershipPhantomData<*const T> to avoid false drop obligationsPhantomData<fn() -> T> or similar patterns to express non-owning, non-covariant relationships