Bounds when deriving on PhantomData

Having read #[derive] sometimes uses incorrect bounds · Issue #26925 · rust-lang/rust · GitHub, I agree that in general getting proper type bounds for #[derive(...)] is a can of worms. There's one special case I'm thinking about posting an issue for, though: PhantomData<T>. This type is already treated "specially"; I think that given its strong guarantee that T plays no role in any usage of the type things like this

use std::marker::PhantomData;
#[derive(Default)]
struct S<T>(PhantomData<T>);

should produce an implementation of Default with no bounds on T like this

impl<T> Default for S<T> {
    fn default() -> Self {
        Self(PhantomData)
    }
}

Presumably this would require modifying the derive macro to treat PhantomData specially. I don't know how difficult that would be.

Thoughts? Is this a good idea? Is it worth doing?

It cannot be done AFAIK because the macro cannot resolve the name.

The macros right now don't look at the field types at all.

The only things the std derives do is bound the list of generic type parameters.

(That's intentional, because that way changing the field types can't change the bounds. Though it's often not what people want, in more complicated situations.)

4 Likes

I wonder to what extent we could get the trait system to solve this? What if #[derive(Default)] on a struct had an implicit bound of FieldType: Default for every field? Then, this would have a bound of PhantomData<T>: Default, which in turn doesn't actually need T: Default. Similarly, Option<T>: Default doesn't need T: Default. But [T; 4]: Default would require T: Default.

It does not work when the field type is private.

We could just make that work.

You can already bound on non-exported traits (using some pub-in-a-private-module trick), so bounding on non-exported types would be no big deal.

EDIT: Oh, I think we might have already done that: Relax priv-in-pub lint on generic bounds and where clauses of trait impls. by jswrenn · Pull Request #90586 · rust-lang/rust · GitHub (I thought it sounded familiar...)

That's a breaking change, because I can do this today: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b67320d076991cb7e5732fc2fd7c8212

#[derive(Default)]
struct Foo<T>(Option<T>);

struct NotDefaultType;

impl Default for Foo<NotDefaultType> {
    fn default() -> Self { Self(Some(NotDefaultType)) }
}

So it would need an opt-in of some sort.

(And even in cases where it's not directly breaking, it's potentially scary. Weakening the requirements might be logically incorrect, and that might be really really bad if there's unsafe code that's only sound today because of the current bounds.)

2 Likes

EventuallyTM I want to write an RFC that would permit arbitrary tokens to be provided as a parameter to derives. The built-in macros could then do #[derive(Default(where T: Default, Option<U>: Default))] and similar. It would also work for deriving const impls via #[derive(Default(const))]. No reason they couldn't be combined (comma-separated), either.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.