Rust 99 🦀 PhantomData (PhantomData) for Unused Type Parameters
Rust’s type system forbids unused generic parameters in struct definitions. A lifetime that appears in a struct’s header but nowhere in its fields is unbounded, and unbounded parameters cause the compiler to reject the definition outright. This restriction exists because unused parameters leave the compiler unable to reason about variance, drop safety, and auto-trait propagation. PhantomData<T> is the sanctioned solution: a zero-sized marker that tells the compiler “this type logically contains, owns, or borrows a T” without storing any actual data .
The chapter begins with the problem that PhantomData solves: raw-pointer wrappers, FFI handles, and lifetime-tagged views all need to communicate relationships that their fields do not express. It then explains the four distinct jobs PhantomData performs—variance control, drop-check participation, auto-trait propagation, and lifetime bounding—and shows how the choice of phantom shape determines which job is done. The later sections cover the common patterns in practice, the subtle interactions with Drop and Send/Sync, and the pitfalls that arise from picking the wrong shape.
Key point: PhantomData<T> occupies zero bytes at runtime, but the compiler treats it as if the struct contained a real T for every static analysis it performs. The choice of T—or &'a T, or *const T, or fn(T)—determines what the compiler concludes.
Why PhantomData exists
The unused parameter restriction. Rust requires every generic parameter in a struct’s header to appear somewhere in its fields. A lifetime that appears only in the header is unbounded: the compiler has no field through which to constrain it, and no way to determine its variance . The same applies to type parameters that are semantically meaningful but not stored in any field. PhantomData provides a field position that the compiler can analyze, satisfying the restriction without adding runtime cost.
The variance problem. Variance determines whether a type can be substituted for a subtype or supertype. A struct’s variance over its parameters is inferred from how those parameters appear in its fields. A raw pointer *const T is covariant over T, but a struct that logically borrows T for a lifetime 'a needs to be covariant over 'a as well. Without a field that mentions 'a, the compiler has no variance information to work with. PhantomData<&'a T> supplies exactly that information: the struct becomes covariant over 'a and T, matching the relationship the programmer intends .
The drop-check problem. When a type implements Drop, the compiler must determine whether the destructor could access data that has already been freed. A raw pointer *const T does not count as owning a T, so the compiler concludes that a struct holding only a *const T does not need to worry about T‘s destructor. But if the struct’s Drop implementation actually drops a T through that pointer, the compiler’s conclusion is wrong. PhantomData<T> tells the drop checker that the struct does own a T, restoring the safety analysis . Since RFC 1238, a Drop implementation itself communicates ownership, but PhantomData remains relevant for types without Drop that still have drop glue through other fields.
The auto-trait problem. Send and Sync are auto-traits: the compiler derives them automatically based on a type’s fields. A raw pointer is neither Send nor Sync by default on all targets, but a wrapper around a pointer may logically be Send if it uniquely owns its referent. Conversely, a type with a phantom field may accidentally become !Send if the phantom shape implies a non-Send relationship. The choice between PhantomData<T>, PhantomData<&'a T>, and PhantomData<*const T> changes which auto-traits the struct inherits .
The zero-cost abstraction requirement. PhantomData is a zero-sized type: size_of::<PhantomData<T>>() == 0 and align_of::<PhantomData<T>>() == 1 for all T . A struct with a PhantomData field has exactly the same layout as the same struct without it. The marker exists entirely at compile time. This is what makes it suitable for performance-sensitive code: a wrapper can carry precise type-level information without costing a single byte or a single instruction at runtime.
a. The four jobs of PhantomData
PhantomData is not a single tool. It is a marker whose meaning depends entirely on the type argument given to it. The same zero-sized field can communicate ownership, borrowing, variance, or thread-safety, and choosing the wrong shape produces code that compiles but is either unsound or unnecessarily restrictive .
The first job is lifetime bounding. A struct that wraps raw pointers into a slice needs to prevent the slice from being freed while the struct is alive. The lifetime parameter alone does nothing; it must appear in a field. PhantomData<&'a T> simulates a shared borrow of T for 'a, which bounds the lifetime and makes the struct covariant over 'a and T .
use std::marker::PhantomData;
// A cursor over a slice, holding raw pointers.
// The 'a lifetime ensures the slice outlives the cursor.
struct SliceCursor<'a, T> {
ptr: *const T, // raw pointer: no lifetime information
len: usize,
_marker: PhantomData<&'a T>, // simulates holding &'a T
}
impl<'a, T> SliceCursor<'a, T> {
fn new(slice: &'a [T]) -> Self {
SliceCursor {
ptr: slice.as_ptr(),
len: slice.len(),
_marker: PhantomData, // zero-sized, no runtime value
}
}
}
The second job is ownership simulation. A struct that owns a T through a raw pointer must tell the drop checker. PhantomData<T> is the marker for ownership: the compiler treats the struct as containing a T, which means T must be droppable whenever the struct is dropped, and the struct inherits T‘s auto-traits .
// A unique pointer that owns its referent.
// PhantomData<T> tells dropck: "this struct owns a T."
struct UniquePtr<T> {
ptr: *mut T,
_owns: PhantomData<T>, // ownership marker
}
impl<T> Drop for UniquePtr<T> {
fn drop(&mut self) {
// Safety: we own the T, so dropping through the pointer is correct.
unsafe { drop(Box::from_raw(self.ptr)); }
}
}
The third job is variance control. PhantomData<fn(T)> makes a struct contravariant over T, which is useful for callback slots and function-like types. PhantomData<&'a mut T> makes a struct invariant over T, which is necessary for mutable references where substitution would be unsound . The variance cheat sheet is precise: the phantom shape’s own variance becomes the struct’s variance over that parameter.
// A callback slot: contravariant over T.
// A Slot<&'static str> can be used where Slot<&'a str> is expected,
// because a function accepting &'static str can handle any &'a str.
struct CallbackSlot<T> {
_marker: PhantomData<fn(T)>, // contravariant over T
}
// A mutable borrow wrapper: invariant over T.
// Prevents unsound lifetime shortening through mutation.
struct MutRef<'a, T> {
ptr: *mut T,
_marker: PhantomData<&'a mut T>, // invariant over T, covariant over 'a
}
The fourth job is auto-trait propagation. The phantom shape determines whether the struct is Send and Sync. PhantomData<T> inherits T‘s Send/Sync. PhantomData<&'a T> inherits them from T as well, but with the shared-reference semantics. PhantomData<*const T> is neither Send nor Sync on targets where raw pointers are not, which can be either a feature (preventing accidental thread sharing) or a bug (making a type unusable in concurrent contexts) .
b. FFI handles and type tagging
The most common production use of PhantomData is wrapping foreign handles. A C library often represents all its objects as opaque pointers of type *mut c_void, and the Rust wrapper needs to distinguish handles to different foreign types. Without PhantomData, the wrappers would be identical and the compiler could not prevent mixing them. With it, each wrapper is a distinct type, and the type system enforces correct usage .
use std::marker::PhantomData;
use std::ffi::c_void;
// A foreign resource type. The actual handle is an opaque pointer.
struct ForeignResource<R> {
handle: *mut c_void,
_type: PhantomData<R>, // ties the handle to a Rust type R
}
// Two distinct resource kinds.
struct Database;
struct NetworkSocket;
impl<R> ForeignResource<R> {
fn new(handle: *mut c_void) -> Self {
ForeignResource {
handle,
_type: PhantomData,
}
}
}
// These are different types and cannot be mixed.
let db: ForeignResource<Database> = ForeignResource::new(ptr1);
let sock: ForeignResource<NetworkSocket> = ForeignResource::new(ptr2);
// let wrong: ForeignResource<Database> = sock; // compile error
The PhantomData<R> here simulates ownership of an R, which is appropriate because dropping the ForeignResource typically closes or releases the foreign object. If the wrapper merely borrows the foreign object, PhantomData<&'a R> would be more precise and would prevent the wrapper from outliving the borrow .
A second pattern is lifetime-tagged FFI. When a foreign transaction must not outlive the connection that created it, the PhantomData carries the lifetime without storing a reference, saving memory on 64-bit platforms where a reference costs 8 bytes but the foreign handle may be a single byte .
// The transaction owns a foreign handle, but must not outlive the connection.
// PhantomData<&'a mut DatabaseConnection> enforces the borrow relationship
// without storing an actual reference (saving 7 bytes on 64-bit).
struct Transaction<'a> {
handle: u8, // foreign handle, 1 byte
_phantom: PhantomData<&'a mut DatabaseConnection>,
}
impl DatabaseConnection {
fn new_transaction<'a>(&'a mut self) -> Transaction<'a> {
Transaction {
handle: ffi::begin(self.raw),
_phantom: PhantomData,
}
}
}
The type Transaction<'a> is the same size as u8 because PhantomData is zero-sized. The borrow checker still prevents the transaction from outliving the connection: the 'a lifetime is bound through the phantom field, and the constructor’s signature ties it to &'a mut self .
c. Phantom shapes and their consequences
The choice between PhantomData<T>, PhantomData<&'a T>, PhantomData<&'a mut T>, PhantomData<*const T>, and PhantomData<fn(T)> is not cosmetic. Each shape produces different variance, different auto-trait behavior, and different drop-check participation. The table below summarizes the relationships, and the paragraphs after it explain when each is appropriate.
| Phantom shape | Variance over T | Variance over 'a | Ownership signaled |
|---|---|---|---|
PhantomData<T> | Covariant | N/A | Yes |
PhantomData<&'a T> | Covariant | Covariant | Borrowed |
PhantomData<&'a mut T> | Invariant | Covariant | Mutably borrowed |
PhantomData<*const T> | Covariant | N/A | No |
PhantomData<fn(T)> | Contravariant | N/A | No |
PhantomData<T> is the ownership marker. Use it when the struct logically owns a T and should participate in T‘s drop check. A container that allocates and frees T values through a raw pointer falls into this category .
PhantomData<&'a T> is the shared-borrow marker. Use it when the struct views or borrows a T and should be covariant over both the lifetime and the type. Slice iterators, views, and read-only FFI wrappers fit this pattern .
PhantomData<&'a mut T> is the mutable-borrow marker. The invariance over T is critical: if a struct could substitute a &'a mut &'static str for a &'a mut &'short str, it could write a short-lived reference into a static slot. The compiler prevents this through invariance, and the phantom field is what supplies the invariance .
PhantomData<*const T> is the non-owning pointer marker. It is covariant over T and does not signal ownership, so drop check does not require T to outlive the struct. It is also neither Send nor Sync on most targets, which can be either desired or unwanted depending on the wrapper’s semantics .
PhantomData<fn(T)> is the contravariant marker. It is rare but precise: a callback slot that accepts a T should be contravariant over T, because a function that handles a supertype can be used where a function that handles a subtype is expected. The fn(T) shape also does not signal ownership and inherits no auto-trait restrictions from T .
A common mistake is to reach for PhantomData<T> by default because it is the shortest to write. When the struct is a view or a borrow, PhantomData<T> overconstrains the drop check: it requires T to be droppable at the struct’s drop point, even though the struct does not actually own a T . The correct default depends on the relationship: ownership, borrowing, or mere reference.
Complete Example Session
// ============================================
// PART 1: THE SETUP
// ============================================
// We build a small FFI wrapper library that uses PhantomData
// for lifetime binding, ownership, and type tagging.
use std::marker::PhantomData;
use std::ffi::c_void;
// ============================================
// PART 2: THE RAW HANDLE
// ============================================
// The foreign library returns opaque pointers.
mod ffi {
use std::ffi::c_void;
pub fn open() -> *mut c_void { 0x1000 as *mut c_void }
pub fn close(_: *mut c_void) {}
pub fn begin(_: *mut c_void) -> u8 { 1 }
}
// ============================================
// PART 3: THE OWNED CONNECTION
// ============================================
// PhantomData<Connection> signals ownership:
// the wrapper owns the foreign object and must close it.
struct Connection {
handle: *mut c_void,
_owns: PhantomData<Connection>,
}
impl Connection {
fn open() -> Self {
Connection {
handle: ffi::open(),
_owns: PhantomData, // zero-sized ownership marker
}
}
}
impl Drop for Connection {
fn drop(&mut self) {
// Safety: we own the handle, so closing it is correct.
ffi::close(self.handle);
}
}
// ============================================
// PART 4: THE BORROWING TRANSACTION
// ============================================
// The transaction does not own the connection.
// PhantomData<&'a mut Connection> enforces the borrow
// without storing a reference (saves 7 bytes on 64-bit).
struct Transaction<'a> {
handle: u8,
_borrow: PhantomData<&'a mut Connection>,
}
impl Connection {
fn begin<'a>(&'a mut self) -> Transaction<'a> {
Transaction {
handle: ffi::begin(self.handle),
_borrow: PhantomData, // ties the transaction to 'a
}
}
}
// ============================================
// PART 5: THE VIEW TYPE
// ============================================
// A read-only view into a slice. PhantomData<&'a T>
// makes the view covariant over 'a and T.
struct View<'a, T> {
ptr: *const T,
len: usize,
_view: PhantomData<&'a T>,
}
impl<'a, T> View<'a, T> {
fn new(slice: &'a [T]) -> Self {
View {
ptr: slice.as_ptr(),
len: slice.len(),
_view: PhantomData,
}
}
fn get(&self, i: usize) -> Option<&T> {
if i < self.len {
// Safety: ptr points into a slice valid for 'a.
unsafe { Some(&*self.ptr.add(i)) }
} else {
None
}
}
}
// ============================================
// PART 6: THE TYPE-TAGGED HANDLE
// ============================================
// PhantomData<R> distinguishes handles to different resource kinds.
struct Resource<R> {
ptr: *mut c_void,
_kind: PhantomData<R>,
}
struct Database;
struct Socket;
impl<R> Resource<R> {
fn new(ptr: *mut c_void) -> Self {
Resource { ptr, _kind: PhantomData }
}
}
fn use_database(_db: &Resource<Database>) {}
fn use_socket(_sock: &Resource<Socket>) {}
// ============================================
// PART 7: THE CONTRAVARIANT CALLBACK
// ============================================
// PhantomData<fn(T)> makes the slot contravariant over T.
// A slot accepting a supertype can be used where one
// accepting a subtype is expected.
struct CallbackSlot<T> {
_marker: PhantomData<fn(T)>,
}
impl<T> CallbackSlot<T> {
fn new() -> Self {
CallbackSlot { _marker: PhantomData }
}
}
// ============================================
// PART 8: THE AUTO-TRAIT CONSEQUENCE
// ============================================
// PhantomData<*const T> does not inherit Send/Sync from T,
// which prevents accidental thread sharing of non-owning views.
struct NonOwningView<T> {
ptr: *const T,
_marker: PhantomData<*const T>,
}
// NonOwningView<T> is !Send and !Sync because *const T is.
// This is a feature: the view must not be sent across threads
// without explicit synchronization.
// ============================================
// PART 9: THE SIZED MARKER
// ============================================
// PhantomData is zero-sized and has alignment 1.
fn size_facts() {
assert_eq!(std::mem::size_of::<PhantomData<Connection>>(), 0);
assert_eq!(std::mem::align_of::<PhantomData<Connection>>(), 1);
assert_eq!(std::mem::size_of::<Transaction<'_>>(), 1); // just the u8
}
// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// PhantomData is a communication channel to the compiler.
// The shape of the phantom determines what is communicated.
fn main() {
let mut conn = Connection::open();
let tx = conn.begin();
// tx cannot outlive conn; the borrow is enforced by PhantomData.
let data = [1, 2, 3];
let view = View::new(&data);
assert_eq!(view.get(1), Some(&2));
let db: Resource<Database> = Resource::new(0x2000 as *mut c_void);
use_database(&db);
size_facts();
}
The ten parts build a small library that exercises each phantom shape: ownership, mutable borrowing, shared borrowing, type tagging, and contravariance. The key observation is that none of the PhantomData fields appears at runtime. The Transaction is one byte because its phantom field is zero-sized. The safety comes entirely from what the compiler infers from the field’s type.
Quick Reference
Phantom Shapes
| Shape | Variance over T | Variance over 'a | Drop Check |
|---|---|---|---|
PhantomData<T> | Covariant | N/A | Owns T |
PhantomData<&'a T> | Covariant | Covariant | Borrows T |
PhantomData<&'a mut T> | Invariant | Covariant | Mutably borrows |
PhantomData<*const T> | Covariant | N/A | Does not own |
PhantomData<fn(T)> | Contravariant | N/A | Does not own |
Auto-Trait Inheritance
| Phantom | Send | Sync |
|---|---|---|
PhantomData<T> | if T: Send | if T: Sync |
PhantomData<&'a T> | if T: Sync | if T: Sync |
PhantomData<&'a mut T> | if T: Send | if T: Sync |
PhantomData<*const T> | No | No |
PhantomData<fn(T)> | Yes | Yes |
When to Use Each
| Situation | Phantom Shape |
|---|---|
| Wrapper owns the referent | PhantomData<T> |
| View borrows the referent | PhantomData<&'a T> |
| Exclusive borrow of referent | PhantomData<&'a mut T> |
| Non-owning raw pointer | PhantomData<*const T> |
| Callback / function slot | PhantomData<fn(T)> |
Size and Alignment
| Property | Value |
|---|---|
size_of::<PhantomData<T>>() | 0 |
align_of::<PhantomData<T>>() | 1 |
| Layout effect on struct | None |
Best Practices
✅ Do This:
// Use PhantomData<&'a T> for borrowing views.
struct View<'a, T> { ptr: *const T, _m: PhantomData<&'a T> } // ✅
// Use PhantomData<T> for owned wrappers.
struct Owned<T> { ptr: *mut T, _m: PhantomData<T> } // ✅
// Use PhantomData<&'a mut T> for exclusive borrows.
struct Mut<'a, T> { ptr: *mut T, _m: PhantomData<&'a mut T> } // ✅
// Use PhantomData<fn(T)> for contravariant callbacks.
struct Slot<T> { _m: PhantomData<fn(T)> } // ✅
// Name the field `_marker` or `_phantom` to signal non-use.
_marker: PhantomData<&'a T> // ✅
// Document what invariant the phantom represents.
// Phantom: ties this handle to the Connection's lifetime. // ✅
❌ Don’t Do This:
// Don't use PhantomData<T> for a non-owning view.
struct View<'a, T> { ptr: *const T, _m: PhantomData<T> } // ❌ overconstrains dropck
// Don't use PhantomData<&'a mut T> when a shared borrow is intended.
struct Reader<'a, T> { ptr: *const T, _m: PhantomData<&'a mut T> } // ❌ invariance too strict
// Don't forget the lifetime parameter on the constructor.
fn new(slice: &[T]) -> View<'_, T> { ... } // ❌ unbound lifetime
// Don't assume PhantomData<*const T> is Send.
// It is !Send, which may break concurrent use. // ❌
// Don't add PhantomData just to silence a compiler error
// without understanding the relationship it encodes. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Unbounded lifetime error | Lifetime parameter unused in fields | Add PhantomData<&'a T> |
| View overconstrains drop | PhantomData<T> signals ownership | Use PhantomData<&'a T> or *const T |
Type accidentally !Send | PhantomData<*const T> inherits pointer’s auto-traits | Choose a shape that matches thread-safety intent |
| Unsound variance | Wrong phantom shape allows unsound substitution | Match shape to ownership/borrow/mutability |
| Drop check too permissive | Raw pointer without PhantomData<T> for owned data | Add PhantomData<T> or implement Drop |
| Constructor lifetime unbound | fn new(x: &T) elides lifetime, phantom becomes unbounded | Write fn new<'a>(x: &'a T) -> Self explicitly |
| Size unexpectedly large | Phantom field was replaced with a real reference | Verify size_of with PhantomData |
Real-World Examples
1. Slice Iterator
struct Iter<'a, T> {
ptr: *const T,
end: *const T,
_marker: PhantomData<&'a T>,
}
2. Owned FFI Handle
struct Handle {
ptr: *mut c_void,
_owns: PhantomData<()>,
}
3. Borrowing Transaction
struct Tx<'a> {
id: u8,
_borrow: PhantomData<&'a mut Db>,
}
4. Type-Tagged ID
struct Id<T> {
raw: u64,
_kind: PhantomData<fn() -> T>,
}
5. Contravariant Callback
struct FnSlot<T> {
_marker: PhantomData<fn(T)>,
}
6. Invariant MutRef
struct MutRef<'a, T> {
ptr: *mut T,
_marker: PhantomData<&'a mut T>,
}
7. Non-Owning View
struct View<'a, T> {
ptr: *const T,
_marker: PhantomData<&'a T>,
}
8. Zero-Sized Type Tag
struct Tagged<T> {
value: u64,
_tag: PhantomData<T>,
}
9. Covariant Cursor
struct Cursor<'a, T> {
ptr: *const T,
_marker: PhantomData<&'a T>,
}
10. Ownership Marker for Vec-Like
struct RawVec<T> {
ptr: *mut T,
cap: usize,
_owns: PhantomData<T>,
}
Visual
The PhantomData Decision Tree
┌─────────────────────────────────────────────────────────┐
│ Does the struct logically OWN the data? │
│ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────┐ Does it BORROW the data? │
│ │ PhantomData<T>│ │ │
│ │ (ownership) │ ┌─────────┴─────────┐ │
│ └───────────────┘ │ │ │
│ SHARED EXCLUSIVE │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐│
│ │PhantomData<&'a T>│ │PhantomData<&'a ││
│ │ (covariant) │ │ mut T> ││
│ └─────────────────┘ │ (invariant over││
│ │ T) ││
│ └─────────────────┘│
│ │
│ If it merely REFERENCES without owning or borrowing: │
│ ┌─────────────────┐ │
│ │PhantomData< │ │
│ │ *const T> │ │
│ │ (non-owning) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
Zero-Sized Layout
┌─────────────────────────────────────────────────────────┐
│ struct Transaction<'a> { │
│ handle: u8, │
│ _phantom: PhantomData<&'a mut Connection>, │
│ } │
│ │
│ Memory layout: │
│ ┌───┐ │
│ │ h │ byte 0: handle (u8) │
│ └───┘ │
│ │
│ The PhantomData field occupies 0 bytes. │
│ size_of::<Transaction>() == 1 │
│ │
│ Compare to storing a real reference: │
│ struct Transaction<'a> { │
│ handle: u8, │
│ _borrow: &'a mut Connection, // 8 bytes on x86-64 │
│ } │
│ size_of() == 16 (1 byte + 7 padding + 8 pointer) │
│ │
│ PhantomData saves 15 bytes per transaction. │
└─────────────────────────────────────────────────────────┘
Variance Consequences
┌─────────────────────────────────────────────────────────┐
│ PhantomData<&'a T>: COVARIANT over 'a │
│ │
│ A View<'long, T> can be used where View<'short, T> │
│ is expected. │
│ │
│ let data = vec![1, 2, 3]; │
│ let view: View<'_, i32> = View::new(&data); │
│ // view borrows data; cannot outlive data. │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ PhantomData<&'a mut T>: INVARIANT over T │
│ │
│ A MutRef<'a, &'static str> CANNOT be used where │
│ MutRef<'a, &'short str> is expected. │
│ │
│ This prevents writing a short-lived reference │
│ into a slot that expects a static one. │
└─────────────────────────────────────────────────────────┘
Auto-Trait Flow
┌─────────────────────────────────────────────────────────┐
│ PhantomData<T> │
│ │
│ If T: Send → Struct: Send │
│ If T: Sync → Struct: Sync │
│ │
│ PhantomData<*const T> │
│ │
│ *const T is !Send and !Sync (most targets) │
│ → Struct: !Send and !Sync │
│ │
│ This is why the phantom shape matters for concurrency: │
│ a non-owning view should not be sent across threads │
│ without synchronization. │
│ │
│ If thread-safety is intended, use PhantomData<T> │
│ or manually implement Send/Sync with unsafe. │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Definition | Zero-sized marker for unused type/lifetime parameters |
| Size | 0 bytes, alignment 1, no layout effect |
| Primary job | Communicate ownership, borrowing, variance, auto-traits |
PhantomData<T> | Owns T, covariant, participates in drop check |
PhantomData<&'a T> | Borrows T, covariant over 'a and T |
PhantomData<&'a mut T> | Mutably borrows, invariant over T |
PhantomData<*const T> | Non-owning, covariant, !Send/!Sync |
PhantomData<fn(T)> | Contravariant over T, no ownership |
| Unused lifetime error | Fixed by adding PhantomData<&'a T> |
| Drop-check marker | PhantomData<T> tells compiler the struct owns T |
Key takeaways:
PhantomDatais a compile-time communication channel. It occupies zero bytes but changes what the compiler infers about a type.- The shape determines the meaning.
PhantomData<T>is not interchangeable withPhantomData<&'a T>. Each encodes a different relationship. - Unused lifetime parameters are forbidden. A lifetime that appears only in a struct’s header is unbounded;
PhantomDatagives it a field position. - Variance follows the phantom shape.
&'a Tis covariant,&'a mut Tis invariant overT, andfn(T)is contravariant. - Drop check depends on ownership signaling. A raw pointer alone does not own its referent;
PhantomData<T>corrects that. - Auto-traits propagate from the phantom.
PhantomData<*const T>makes a struct!Send, which may be a feature or a bug. - FFI wrappers are the canonical use case. Type tags, lifetime bounds, and ownership markers all appear in foreign-function boundaries.
PhantomDatadoes not validate anything at runtime. The invariants it encodes are the programmer’s responsibility to uphold in unsafe code.- The zero-size property makes it free. There is no runtime cost to carrying precise type-level information.
- Document the relationship. A
_markerfield whose purpose is unexplained is a maintenance hazard; a comment explaining what invariant it represents is essential.
Remember: PhantomData<T> exists because Rust’s type system requires every parameter to be accounted for, and because the compiler needs field-level evidence to reason about variance, drop safety, and auto-traits. It is not a hack or a workaround; it is the precise mechanism for expressing relationships that a struct’s stored fields do not capture. The programmer chooses the phantom shape to match the abstraction’s actual semantics: ownership, shared borrowing, exclusive borrowing, or mere reference. When the shape is correct, the compiler enforces the relationship for free. When it is wrong, the code either fails to compile or, worse, compiles with unsound assumptions. The zero-sized field is a small thing, but what it tells the compiler is not.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!