Search code examples
genericsrusttraits

Auto-Generated Trait constraints on derived `Eq`, `Ord`,... implementations


I'm a bit confused by the constraints that are generated for derived implementations of Ord and similar traits.

I've noticed, that it's not possible to derive Ord for a struct that contains PhantomData<T> if T is not Ord, even though PhantomData<T> implements Ord also if T does not.

For instance the following example does not compile:

use core::marker::PhantomData;

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct NewPhantomData<T>(PhantomData<T>);

fn needs_ord<T>(_b : T) where T : Ord{ todo!() }

struct TypeWithoutOrd{}

fn foo(){
    let a : NewPhantomData<u32> =
        NewPhantomData(PhantomData::default());
    let b : PhantomData<TypeWithoutOrd> = PhantomData::default();
    let c : NewPhantomData<TypeWithoutOrd> =
        NewPhantomData(PhantomData::default());
    needs_ord(a); //works
    needs_ord(b); //works too
    needs_ord(c); //does not work
}

(Playground)

The expanded macros make the reason why it doesn't compile obvious:

impl<T: ::core::cmp::Ord> ::core::cmp::Ord for NewPhantomData<T> {
    #[inline]
    fn cmp(&self, other: &NewPhantomData<T>) -> ::core::cmp::Ordering {
        ::core::cmp::Ord::cmp(&self.0, &other.0)
    }
}

What confuses me, however, are the trait bounds on the derived implementation. I would have expected the bounds to look like this instead:

impl<T> ::core::cmp::Ord for NewPhantomData<T> where PhantomData<T> : ::core::cmp::Ord {
    #[inline]
    fn cmp(&self, other: &NewPhantomData<T>) -> ::core::cmp::Ordering {
        ::core::cmp::Ord::cmp(&self.0, &other.0)
    }
}

(Playground)

Or, in other words, I would have expected a trait bound on the types of the fields of the struct, not the generic parameters directly.

What is the reasoning for generating the bounds the way they are? Is this a bug?


Solution

  • There is a big problems with this: cycles.

    The expected derive bounds for struct like the following:

    #[derive(Clone)]
    struct List<T> {
        data: T,
        next: Option<Box<List<T>>>,
    }
    

    Would include the bound Option<Box<List<T>>>: Clone, and cycles like that are currently incorrectly handled.

    Making it to work is possible, but tricky. And getting it wrong will have soundness implications. As I understand, it is also very hard with the current trait solver (though the next-gen trait solver will make it easier).

    Even if we fix that, changing it now is a breaking change, since people can rely on the bounds to be more restrictive than needed, in order to not expose implementation details (what the types of the fields are).

    Still, there is a desire for so-called perfect derive that will get those bounds correctly, and it may be implemented at some time in the future.