Why doesn't Rust automatically insert `PhantomData` for unused generic parameters? Or why is unused generic parameters disallowed?

Sometimes, I wish the following struct was legal:

struct Foo<T0, T1, T2> {
  pub x: u32,
  pub y: u32,
}

So I can have a nice pattern match:

let Foo { x, y } = get_foo();

The valid code looks unwieldy:

struct Foo<T0, T1, T2> {
  pub x: u32,
  pub y: u32,
  _phantom: PhantomData<(T0, T1, T2)>,
}

let Foo { x, y, .. } = get_foo();

I have to define an additional _phantom field, repeat the same generic parameters, and add .. to the pattern match.

The code for PhantomData in Rust code is exactly like this:

#[lang = "phantom_data"]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct PhantomData<T: ?Sized>;

It is impossible for a normal user to create a similar struct like the one above since the Rust compiler requires T to be used in fields.

My question is: What is the reason behind requiring the user to explicitly add PhantomData?

Needing PhantomData does not necessarily mean you need PhantomData<T>.

The struct you want to create is perfectly legal. Just delete the unused type parameters. If you have a reason for needing them, you should know what kind of PhantomData you want, and be glad the compiler allows you to specify it rather than assuming PhantomData<T> all the time.

But most of the time the correct solution is to delete the unused type parameters.

3 Likes

I was quite surprised that there are many ways PhantomData's "held" type(s) can be set up, and they each have different effects on drop checking, variance, and even whether a struct can be Sync or Send (see the docs).

3 Likes

Well, it's nothing special. Storing a PhantomData<T> means that your type will behave as if it contained an actual value of type T, except that it doesn't. Everything else follows from this.

When a struct has a type parameter, there are effects depending on how that type is used. This can affect variance and certain auto-trait implementations.

An example:

#![allow(dead_code)]
#![allow(unused_variables)]

use std::marker::PhantomData;

struct A<T> {
    _phantom: PhantomData<T>,
}

struct B<T> {
    _phantom: PhantomData<fn(T)>,
}

impl<T> A<T> {
    fn new(arg: T) -> Self {
        Self { _phantom: PhantomData }
    }
}

impl<T> B<T> {
    fn new(arg: T) -> Self {
        Self { _phantom: PhantomData }
    }
}

fn foo(arg: A<&'static String>) {}
fn bar(arg: B<&'static String>) {}

fn main() {
    let s = "Hello".to_string();
    let a = A::new(&s);
    let b = B::new(&s);
    //foo(a); // this will fail
    bar(b);
}

(Playground)

Not a very useful example, but you can see that B<T> behaves differently here (because it is contravariant over T).

2 Likes

There's no way to infer the variance of unused parameters. (Take care reading the RFC, as not all the ideas were implemented; there's no PhantomFn or other way to make trait parameters anything but invariant, etc.) Inserting PhantomData automatically would be akin to choosing a variance (et cetera). It could never get it always correct.

What are the parameters in your example used for? There may be another hacky way to work around it. You'd probably have to carry bounds around everywhere. This may not be as onerous as it sounds, since you probably already have to do that or to specify the types of the otherwise unused parameters everywhere already.

1 Like