RFA-423 · Case file with fixtures · Case 395 of 694 · Compiler evidence
An Unused Rust Generic Parameter Needs Meaning, Not Just a Name
A generic parameter must affect a type's representation or static semantics. Remove accidental parameters, store the value, or choose a deliberate PhantomData shape that communicates ownership, variance, and drop-check meaning.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A generic parameter must affect representation or static semantics such as variance, ownership, auto traits, and drop checking; a name alone communicates none of these relationships.
- First discriminating check
- Determine whether T is accidental or whether the type owns, borrows, points to, produces, or consumes T before selecting a precise PhantomData form.
I wanted a typed identifier so a user ID could not be passed where an order ID belongs. I wrote Handle<T> { id: u64 }, but Rust reported E0392: T is never used.
The failing fixture can even construct Handle::<String>. The type argument appears in source and in the type's name, yet no field tells the compiler what relationship the handle has with T.
A parameter must participate in the type
The E0392 explanation offers three broad repairs: remove the parameter, use it in a field, or represent its intended relationship with PhantomData.
Rust needs more than a decorative label because generic participation affects variance, automatic trait implementations, lifetime checking, and drop checking. Two marker shapes can occupy zero bytes and still communicate different static claims.
Remove an accidental parameter
If T does not distinguish values and no operation depends on it, the simplest repair is struct Handle { id: u64 }. Carrying unused generic machinery makes signatures noisier and suggests safety that the program does not actually use.
I look at call sites before deciding. If every handle freely crosses domains, a phantom type would be theatre rather than protection.
Store T when the handle owns T
If the structure logically contains a value, storing value: T is the direct model. Size, destruction, Send, Sync, and other auto-trait behavior then follow the real field.
A raw pointer to T is not equivalent to owning T. Unsafe wrappers must state whether they own, borrow, or only name the pointed-to type. That is where choosing PhantomData requires care rather than a compiler-silencing habit.
PhantomData encodes a static relationship
The repaired fixture uses:
marker: PhantomData<fn() -> T>
For this typed numeric handle, the marker distinguishes Handle<User> from Handle<Order> without storing either value. The fixture is not an unsafe owner and does not claim to borrow a T.
PhantomData is a zero-sized marker, but the type inside it matters. PhantomData<T>, PhantomData<&'a T>, PhantomData<*const T>, and PhantomData<fn() -> T> can differ in ownership, variance, and auto-trait effects.
I therefore document why a particular form exists. “Needed for E0392” is not enough.
Lifetimes need a relationship too
The error also applies to an unused lifetime parameter. A raw pointer wrapper may intend not to outlive borrowed data, but *const T alone does not mention 'a. A marker such as PhantomData<&'a T> can express that borrow relationship.
This is safety-critical in unsafe abstractions. If the marker says less than the implementation assumes, the compiler may permit lifetimes or thread movement that invalidate those assumptions.
The Nomicon discussion is important reading for ownership and drop-check details. I do not copy a marker form from an unrelated wrapper.
Zero size does not mean zero semantic effect
size_of::<PhantomData<T>>() is zero, and adding it commonly does not enlarge the surrounding structure beyond layout alignment effects. Still, derived traits can gain bounds depending on how the generic field is expressed.
For public typed IDs, I often implement or derive equality, ordering, hashing, serialization, and formatting deliberately. I check whether those implementations unnecessarily require marker types such as User to implement the same traits.
Typed handles protect boundaries
The pattern is useful when IDs have the same machine representation but different domains:
struct User;
struct Invoice;
let user: Handle<User> = load_user_id();
let invoice: Handle<Invoice> = load_invoice_id();
The compiler prevents accidental interchange without runtime tags. Conversion between domains should then be explicit and rare.
This is not runtime validation. A Handle<User> can still contain an unknown numeric ID. The marker prevents category confusion in typed code; storage existence and authorization remain separate checks.
My marker review
When E0392 appears, I ask:
- Was the parameter accidental?
- Does the type own, borrow, point to, produce, or consume T?
- What variance is intended?
- Should T affect Send and Sync?
- Does drop checking need to treat T as owned or borrowed?
- Are derived trait bounds acceptable?
- Is the safety meaning documented beside the field?
The core principle is that generic parameters communicate relationships to the type system. If there is no relationship, I remove the parameter. If one exists without runtime storage, I encode the exact relationship with a carefully chosen PhantomData rather than treating it as an empty placeholder.