Returning a trait object from a reference

The following code does not compile (playground link):

trait Trait {}

impl Trait for str {}

fn tmp(s: &str) -> &dyn Trait {
    s
}

Compiler output:

error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/lib.rs:6:5
  |
6 |     s
  |     ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `str`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = note: required for the cast to the object type `dyn Trait`

I don't understand why the compiler expects a size known at compile-time. Isn't the point of trait objects precisely to allow dynamic sizes?

Given that I can't implement Sized for str, what should I do to make the compiler happy and return a trait object?

Thanks!

Due to a technical limitation of how they’re implemented, you can’t make a trait object point directly to an unsized object. You’ll need to use String, Box<str>, or similar instead of str.

1 Like

The reason is that a &dyn Trait consists of a pointer to the value and a pointer to a vtable. For a &str, you cannot know how long the slice is only from the pointer to the start of the string slice, so the vtable must carry the information. But where should it go? For it to go there, there has to be a separate vtable for every possible length of string.

Of course, if you know the length, it is certainly possible to create a &[u8; LEN] for some compile-time constant LEN and have the compiler generate a vtable specific to that length.

1 Like

&str is a fat pointer that contains not only a pointer (to the start of the slice), but also the byte-length of that slice.

&dyn Traitis a fat pointer too, that contains not only the original pointer (to the underlying instance), but also a pointer to a struct containing function pointers to the implementations of the methods of the trait by the underlying type (the VTable, or Virtual function table).

If &str as &dyn Trait worked, we'd have to deal with a super fat pointer, since the "original pointer" would be an already fat one.

And then why stop there? We could have &str as &dyn FirstTrait as &dyn SecondTrait provided we had impl SecondTrait for dyn FirstTrait, which means, unless some colapsing rule applied, that we could end up with arbitrarily large pointer types, which kind of defeats the purpose of a pointer :sweat_smile:.

Hence the error.

Solution

I don't know what Trait is supposed to represent, but usually anything that str implements, &'_ str ought to implement it too.

Take, for instance, Display:

fn tmp<'s> (s: &'s &'_ str) -> &'s dyn Display
{
    s
}
  • Or you can use ::with_locals to avoid the double indirection on input:

    #[with]
    fn tmp (s: &'_ str) -> &'self dyn Display
    {
        &s
    }
    
3 Likes

Why would the second example require 2 fat pointers? By casting &dyn First to &dyn Second, you "lose" further information about the type, couldn't you just keep the vtable for Second?

trait Foo {
    fn foo (self: &'_ Self)
    {
        // ...
    }
}

trait Bar {
    fn bar (self: &'_ Self);
}

impl Bar for dyn Foo + '_ {
    fn bar (self: &'_ Self)
    {
        self.foo(); /* <- requires access to the vtable:
        (self.Foo_vtable.foo)(self.data_ptr); // */
    }
}

So if you had a (at_dyn_foo as &dyn Bar).bar(), this would be doing:

(self.Bar_vtable.bar)(self.data_ptr)

But self.data_ptr, here, would need to be a fat pointer:

/// The layout of `at_dyn_foo as &dyn Bar` would need to be:
struct SuperFatTraitObject {
    data_ptr: FooTraitObject,
    Bar_vtable: &'static BarVTable,
}
/// where:
struct FooTraitObject {
    data_ptr: *const TypeErased,
    Foo_vtable: &'static FooVTable,
}

At which point, nothing makes sense anywhere (since SuperFatTraitObject would not be compatible with a classic BarTraitObject) :smile:

Also dyn FistTrait still is a type, so &dyn FirstTrait as &dyn SecondTrait should behave consistently with any other &T as &dyn SecondTrait casting. Current strategy is to allow it only if T: Sized.

2 Likes