Why is the trait not to be object safe?

Consider this example

trait BaseTrait<T>{}

trait DerivedTrait:BaseTrait<Self::Target>{
   type Target;
}

struct A(i32);

impl BaseTrait<A> for A{}


impl DerivedTrait for A{
    type Target = A;
}

fn main(){
	let rr:Box<dyn DerivedTrait<Target = A>> = Box::new(A(1));
}

Why isn't DerivedTrait to be object safe? It seems not to violate any requirement defined in Traits - The Rust Reference. The super trait only requires Self::Target to be Sized but it does not require Self to be Sized, why is the trait DerivedTrait not to be object safe?

3 Likes

It most definitely violates them.

Because any variable of type DerivedTrait gives you access to type BaseTrait too. And that one is not object safe.

You can add more indirection:

trait BaseTrait<T>{}

trait DerivedTrait:BaseTrait<Self::Target>{
   type Target;
}

struct A(i32);

impl BaseTrait<A> for A{}


impl DerivedTrait for A{
    type Target = A;
}

trait ClosedTraitA {
}

impl<X> ClosedTraitA for X where Self: DerivedTrait<Target = A> {
}

fn main(){
	let rr:Box<dyn ClosedTraitA> = Box::new(A(1));
}

That's valid because variable rr can not use functions from DerivedTrait or BaseTrait now, only functions from ClosedTraitA.

But it is, since this code compiles:

trait BaseTrait<T>{}

struct A(i32);

impl BaseTrait<A> for A{}

fn main(){
	let rr:Box<dyn BaseTrait<A>> = Box::new(A(1));
}
2 Likes

These are very different things.

When type is argument of your trait then caller (or maker of dyn) decides the type. Your BaseTrait is supposed to accept any T without limitation.

In your original version, though, it's not true: there Target is member of trait, which means callee (the implementation of trait) decided the type.

Trait parameters are like function inputs while trait associated types are like function outputs.

Big difference.

Now, when you deal with types you really don't want to distinguish between inputs and outputs.

And would be great if DerivedTrait<Target = A> would be restricting the BaseTrait instead, but, alas, that's not how Rust works today.

Chalk was supposed to provide these, more Prolog-style environment (in Prolog functions are symmetric, inputs and outpus are not determined by function but by caller), but I don't think there's hope of seeing it any time soon.

Another approach would be to just make all types object-safe (like Swift did).

But Rust today doesn't do that thus you can constrain trait parameters but they can not be used as restrictions for other traits.

This works, too:

trait DerivedTrait{
   type Target;
}

struct A(i32);

impl DerivedTrait for A{
    type Target = A;
}

fn main(){
	let rr:Box<dyn DerivedTrait<Target = A>> = Box::new(A(1));
}

It's not what topicstarter wants, but that's how Rust is today.

Looks like the original RFC and the Reference omit this, but to be object safe a trait also can't refer to Self in supertraits or other trait bounds (with the only expection being Self: Trait bounds where Trait is an object safe trait). The reason is that Self becomes a different type when the trait is made into a trait object, so those bounds become invalid/without meaning/weird/whatever. This rule however seems to be somewhat over-constraining since in your example Self::Target is not really refering to Self, but to the associated type Target, which is well defined even when DerivedTrait is made into a trait object. In fact this bug seems to have already been reported a couple of years ago in Object safety error when using an associated type as a type parameter in the super-trait listing · Issue #40533 · rust-lang/rust · GitHub

10 Likes

Thanks for linking to that issue. I took a quick look and a relatively small change would make these compile (but more would be needed to keep rejecting invalid code that references assoc types from super traits). I'm looping in t-lang for them to make a determination, but this indeed seems like it might be a slight oversight.

1 Like