rusttraitstrait-objects

`T: Trait`/`impl Trait` doesn't satisfy `dyn Trait`, except when it does


I've encountered a strange variance behavior that I'm sure is a hole in my understanding of the type system, but feels like a compiler bug.

trait Trait: 'static {}
impl<T> Trait for T where T: 'static {}

struct Inner<T>(T);

fn make_inner<T: Trait>(t: T) -> Inner<Box<T>> {
    Inner(Box::new(t))
}

type DynNecessary = Vec<Inner<Box<dyn Trait>>>;

struct DynCarrier {
  things: DynNecessary,
}

impl DynCarrier {
    pub fn spawn(&mut self, t: impl Trait + 'static) {
        let inner = make_inner(t);
        
        self.things.push(inner);
    }
}

fn main() {}

playground link

In the above example, make_inner takes in T and returns Inner<Box<T>> where T is known to satisfy Trait.
However, when trying to use the return value of make_inner somewhere that accepts Inner<Box<dyn Trait>>, I get the following:

= note: expected struct `Inner<Box<(dyn Trait + 'static)>>`
           found struct `Inner<Box<impl Trait + 'static>>`

I had an inkling that this had something to do with the way that the program at runtime would treat a dyn Trait vs a concrete impl Trait, but where it falls apart for me is with the following change:

fn make_inner<T: Trait>(t: T) -> Inner<Box<dyn Trait>> {
    Inner(Box::new(t))
}

where now t is successfully coerced to dyn Trait.
What influence does return type inference have over types, that argument type inference does not? If .push takes ownership of inner, why isn't it capable of the same kind of coersion?


Solution

  • The thing to remember here is that dyn Trait is a concrete type. It's a weird concrete type, which happens to share its in-memory representation with many other types, but it is one type. Box<dyn Trait>, therefore, is also a specific concrete type (with its own particular layout in memory that includes a vtable pointer).

    In the first version of the code, you're constructing an Inner<Box<T>>, where T is some type implementing Trait (and T ≠ dyn Trait), and then trying to use it where Inner<Box<dyn Trait>> is expected. This fails because it is a type mismatch.

    But what about coercion to dyn Trait? Why isn't that happening? Because it only happens for specific containing types. Specifically, in current versions of Rust (the way it is described may change in future versions), you can consult the unstable CoerceUnsized trait to see all types that can be coerced in this way. Note that Box is on this list — so you can coerce Box<T> to Box<dyn Trait> — but of course, your Inner doesn't.

    In the second version with -> Inner<Box<dyn Trait>>, the declared return type informs type inference inside make_inner() such that the construction of Inner is understood to require a parameter of type Box<dyn Trait>, and that coercion is supported. The general principle here is: a Box<T> must be coerced to a Box<dyn Trait> before you wrap it in (almost) anything else. Setting the return type is one way to make that happen.

    One demonstration, or possibly useful workaround, is that your original code will compile if you disassemble and recreate the Inner with the right type:

    let inner = make_inner(t);
    
    let Inner(boxed) = inner;
    let inner: Inner<Box<dyn Trait>> = Inner(boxed);
    
    self.things.push(inner);
    

    Or, if you are happy to use unstable features, just implement CoerceUnsized and your code will work without other changes:

    #![feature(coerce_unsized)]
    
    use std::ops::CoerceUnsized;
    
    impl<T, U> CoerceUnsized<Inner<U>> for Inner<T> where T: CoerceUnsized<U> {}
    

    When this impl gets used, T will be equal to the Box<impl Trait> type and U will be equal to Box<dyn Trait>.

    However, I would recommend the simple solution you already found: specify Box<dyn Trait> soon enough that the coercion happens at construction time.