Traits and type erasure (trait objects)

trait T {
    fn erase_type(&self) -> &dyn T {
        self as &dyn T
    }
}
error[E0277]: the size for values of type `Self` cannot be known at compilation time
 --> src/main.rs:3:9
  |
3 |         self as &dyn T
  |         ^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `Self`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = help: consider adding a `where Self: std::marker::Sized` bound
  = note: required for the cast to the object type `dyn T`

What is happening here: where Self: Sized (i.e. on concrete types), the compiler is able to use type erasure to convert to &dyn T.

What is not happening here: the compiler is unable to recognise that in the unsized case, &self is already a &dyn T (or else it refuses to generate a subtly different implementation of the method).

Is there any good reason this cannot be implemented?

1 Like
impl T for [u8] {}

or, in fact

extern type U;
impl T for U {}

Though it seems it should still be possible to attach an appropriate vtable to the pointer in each case, so… yes, it is a bit strange.

4 Likes

I notice that if the implementation of erase_type is moved to trait implementations, then no implementation for &dyn T is needed (although adding one is not harmful). ~Unfortunately there doesn't appear to be any way to avoid having to implement on each type (short of specialisation or negative trait bounds).~ [See below: since impl for &dyn T is not needed, there is no conflict.]

trait Foo: AsFoo {}

trait AsFoo {
    fn as_foo(&self) -> &dyn Foo;
}

impl<T: Foo> AsFoo for T {
    fn as_foo(&self) -> &dyn Foo {
        self
    }
}
3 Likes

Afaik fat pointers can current only store one usize of metadata. As unsized types are already using this metadata to store it's size, there is currently no space to store the vtable.

Extending fat pointers to store more data would make pointers able to have any size, meaning &dyn Trait would no longer be Sized. So I'm not sure how this would be implemented.

By @adamAndMath's logic, &dyn T is not possible, since trait T objects are not Sized. I guess we are missing an important distinction here: objects needing a length parameter vs those which do not (but are still not Sized). [I assume @felix.s's `extern type U` falls into the latter category.]

Is it worth opening an RFC to add a HasNoLengthParameter bound? I suspect though the collateral damage of another bound (complexity + confusion) would outweigh the benefits.

At least this thread points out @RustyYato's workaround.

@RustyYato’s workaround does not actually work around anything; it relies on the implicit Sized bound on generic parameters. You might as well add where Self: Sized to the erase_type method itself.

And now I understand why Sized is necessary to convert a reference to a &dyn T; the vtable contains a field used to implement std::mem::size_of_val, and vtables are expected to be statically-allocated. So the compiler cannot attach a vtable to a [u8], because it would have to be dynamically-managed; and cannot attach any vtable to an extern type at all.

Accommodating extern type is going to require some changes here anyway. Or not; we may just accept that trait objects cannot be formed out of non-statically-sized types.

Doing this would prevent erase_type from being used on trait objects in order to upcast them. i.e.

trait Foo {
    fn as_foo(&self) -> &dyn Foo where Self: Sized { self }
}

trait Bar: Foo {}

fn check(bar: &dyn Bar) -> &dyn Foo {
    bar.as_foo() // does not compile
}

You could put as_foo on Foo itself, but that would force every implementor of Foo to put the correct default implementation. So, we move that to another trait that automatically gives the correct implementation.

1 Like

Oh my. I was going to reply ‘yes, but it doesn't work with AsFoo either’, but then I tried it in the playground and it does. How come? dyn Bar isn't Sized and so shouldn't be covered by the blanket impl. Does an impl over all T: Foo+Sized imply an impl for dyn X: Foo as well?

dyn Bar will implement Bar, Foo, and AsFoo because of compiler generated implementations, not the generic ones provided, it uses the vtable stored with a pointer to dyn Bar to figure out what methods to call. It doesn't actually matter if there are any concrete implementations. For example, this works,

trait Foo {
    fn hi(&self);
}

fn check(foo: &dyn Foo) {
    foo.hi()
}

Even though it there are no implementations for Foo

1 Like

Yes, I know how dynamic dispatch works. It's obvious dyn Foo should implement Foo and its prerequisites, as if there was an impl Foo for dyn Foo. What is surprising is that it also implements AsFoo even if the only implementation specified for it covers Sized types only, which dyn Foo isn't. Question is, where does that implicit impl come from? What does it cover, and when is it generated?

The implicit impl covers all super traits, so for dyn Bar it covers AsFoo, Foo, and Bar

Silly me, I overlooked this line. That answers my questions. Sorry about the confusion.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.