Casting interior type to a dyn trait object

Hi,

I am having trouble understanding an error trying to cast from a specific type to a trait object.

Here is the minimal example:

struct Outer<'a, T: ?Sized> {
    inner: &'a T,
}

trait InnerTrait: std::fmt::Debug + 'static {}

impl InnerTrait for u32 {}
impl InnerTrait for u64 {}

fn main() {
    // Directly assigning works fine.
    let b: Outer<dyn InnerTrait> = Outer { inner: &23u32 };

    // Using an intermediate variable fails
    let c = Outer { inner: &46u64 };
    let d: Outer<dyn InnerTrait> = c as Outer<dyn InnerTrait>;

    let mut v: Vec<Outer<dyn InnerTrait>> = Vec::new();
    v.push(b);
    v.push(d);
}

A playground is here

The error message from the assignment to d is:

error[E0605]: non-primitive cast: `Outer<'_, u64>` as `Outer<'_, (dyn InnerTrait + 'static)>`
  --> src/main.rs:14:36
   |
14 |     let d: Outer<dyn InnerTrait> = c as Outer<dyn InnerTrait>;
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object

For more information about this error, try `rustc --explain E0605`.

The only difference between the 1st case and the 2nd case is the use of an intermediate variable. I don't understand.

1 Like

What I think it happening here is that when you initialise b, the type parameter dyn InnerTrait flows backwards into the initialiser, meaning that the &23u32 is what gets coerced into a &dyn InnerTrait. No Outer<u32> ever exists in the first place.

The sort of coercion you expect cannot be done by user-defined types as far as I know. Some types in the standard library (like Rc) can do this, but only because they implement experimental traits that you aren't allowed to touch.

3 Likes

The solution here is easy since you don't need to unsize your struct:

impl<'a, T: InnerTrait> Outer<'a, T> {
    pub fn coerce_inner_trait(self) -> Outer<'a, dyn InnerTrait> {
        Outer {
            inner: self.inner
        }
    }
}

let c = Outer { inner: &46u64 };
let d: Outer<dyn InnerTrait> = c.coerce_inner_trait();

You're allowed to coerce &T to &dyn InnerTrait, just not Outer<T> to Outer<dyn InnerTrait>. The only downside is you need a separate method for every type of trait object you want to create.

1 Like

I think @DanielKeep has it exactly correct.
The problem is entirely to do with converting between the types.
With b, there is no conversion, so it's fine. b is of type Outer<dyn InnerTrait> and that's it.

With c though, it's type is inferred to be Outer<u64>.
You can even see this in the error message where it says: non-primitive cast: Outer<'_, u64>.

Then, the type system tries to convert an Outer<u64> into an Outer<dyn InnerTrait> and sees no such conversion is allowed - hence, an error message.


So the real question is why isn't this conversion allowed? Obviously u64 implements InnerTrait.

Well, actually, I can't speak for 'why' (you'd have to ask the compiler gods)...
But normally, you can only perform conversions like yours on the 'outer type' ie. not it's type parameters. Because it isn't always sensible to convert a generic trait like you've done.

So, you need to opt into it, using that magic trait they mentioned! Like all the best things in rust, it's nightly only, but there's nothing unsafe about it or anything. Although it is a little magical I guess : v)

Here's a playground with your exact code, but implementing CoerceUnsized for it, and indeed everything works. But this is really more for education's sake than anything else. Others here have submitted far better solutions for the practical world! :slight_smile:

2 Likes

Another (inline) approach:

    let d: Outer<dyn InnerTrait> = Outer { inner: c.inner };

The last field of your own types can be unsize coerced. So another approach is to use &Outer<dyn InnerTrait> with this alternative definition:

struct Outer<T: ?Sized> {
    inner: T,
}

But custom DSTs like this have some annoying limitations.

1 Like

Indeed, this has been educational. Thanks to everyone.

I see now that when implementing a custom "fat pointer" one needs to use Unsized and CoerceUnsized. That is why this magically works for types like Box<T>.