Why doesn't this blanket impl allow trait object coercion?

Hello!

The recent stabilization of trait upcasting had me wondering - why does the following not compile?

trait Param {
    type Domain: Domain;
}

trait Domain {}

trait Foo {
    type Domain: Domain;
    
    fn test(&self, param: &dyn Param<Domain = Self::Domain>);
}

struct TestDomain;

impl Domain for TestDomain {}

struct Test;

impl Foo for Test {
    type Domain = TestDomain;
    
    fn test(&self, param: &dyn Param<Domain = Self::Domain>) {
        external(param);
    }
}

impl<T> Param for T where T: External {
    type Domain = TestDomain;
}

trait External {}

fn external(_param: &dyn External) {
    unimplemented!()
}

It fails with the error that the call to external expects trait External, but found trait Param<Domain = TestDomain>.

However, shouldn't the existence of this covering implementation:

impl<T> Param for T where T: External {
    type Domain = TestDomain;
}

give the compiler enough information to deduce that any T: Param<Domain = TestDomain> also means T: External? And thus, &dyn Param<Domain = TestDomain> should be able to be coerced to &dyn External?

Is this just not implemented as of now? Or maybe I'm missing something?

First, that blanket implementation means that T: External + Sized implies T: Param<Domain = TestDomain, not the other way around.

But changing it to this won't help...

impl<T: ?Sized> External for T where T: Param<Domain = TestDomain> {
}

...because trait upcasting is only for the use case of explicit sub/supertraits (as expressed by the bounds in the trait definition), and is not based on what implementations exist.

And and can't coerce dyn Param<..> itself to dyn External without that relationship[1] because coercions to dyn _ can only happen from another unsized type when you're dropping auto-traits, dropping the principal trait, coercing the dyn lifetime, or -- when the feature lands -- upcasting.

There's more information in the RFC.


  1. e.g. if you simply impl External for dyn Param<Domain = TestDomain> + '_ ↩︎

First, that blanket implementation means that T: External + Sized implies T: Param<Domain = TestDomain, not the other way around.

Right, thank you. I wrote that backwards.

I guess then, in the context of upcasting, what could help here is conditional trait bounds based on a set of discrete types for any associated types.

something like

trait Param: External when Self::Domain = TestDomain {
  type Domain: Domain
}

Basically to allow upcasting to multiple traits without needing to specify them as supertraits for all implementations of Param.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.