Trait objects of trait objects

I think the real question here is what’s the purpose (and meaning) of impl T2 for T1. As @matklad noted, impl T2 for Box<T1> gets you what you’re after.

If we go back to the impl Trait for Trait case (impl trait for its own object type), that was introduced as part of the object safety set of RFCs. The motivation there, AFAIUI, is to allow the following to work:

trait Trait {}
struct S;

fn foo<T: Trait + ?Sized>(t: &T) {}

foo(&S); // OK
let obj: &Trait = &S;
foo(obj); // also OK

So as long as Trait is object safe, you get this "reflexive" automatic implementation that you can use.

Let’s look at your example, in light of the previous one:

trait T1 {} // methods don’t matter at the moment
trait T2 {}

struct S;

impl T1 for S {}
impl T2 for T1 {}

fn foo<T: T2 + ?Sized>(_: &T) {}
fn foo2(_: &T2) {}

let obj: &T1 = &S;
foo(obj); // OK
foo2(obj); // not OK

This might look weird in that it seems like obj is a T2 and isn’t a T2, at the same time. But I guess the way to view this is the impl T2 for T1 provides you only static virtual (:crazy_face:) dispatch mechanism, but doesn’t let you form object types of T2 on its own.

AKA Non Virtual Interface?

Perhaps. Truth be told, a method implementation in impl T2 for T1 might not actually invoke anything on &self, and thus there's no virtual dispatch at all. The "static" bit here is mostly that it allows you to call methods expressing a static bound on the trait, but not methods expecting an object type. Relatedly, it doesn't allow forming trait objects from other trait objects (since that's a runtime/dynamic, rather than static, representation).

Here's a more symmetric code snippet: Rust Playground

This is helpful; I think I understand where I went wrong now. My original reasoning was an argument from symmetry, as follows:

  • First we convert a Box<S> to a Box<T1>, thanks to an impl T1 for S.
  • Second (I argued) we should be able to convert a Box<T1> to a Box<T2>, thanks to an impl T2 for T1.

The two cases are exactly symmetrical. Your proposal converts a Box<Box<T1>> to a Box<T2>, but there should be no need to lose a Box here if there wasn't in the first conversion.

What I missed was that the conversion from Pointer<T> to Pointer<Trait> has a side requirement: in addition to T implementing Trait, it is also necessary that Pointer<T> not be a fat pointer.

The conversion from Box<S> to Box<T1> simply attaches a vtable pointer aside the original pointer. We can't do that twice, since that would result in a doubly-fat pointer:

It's essential that trait objects have a fixed size. In this diagram, the underlying type hasn't really been erased; you can tell from its size that was built from a fat pointer.

It is possible to have both impl T1 for T2 and impl T2 for T1 (give it a try!). I think this means that, if my conversion were permitted, converting back and forth between Box<T1> and Box<T2> repeatedly would produce a value that recorded the complete history of all such conversions. Such a value would necessarily require some sort of dynamic allocation, which isn't appropriate for an apparently primitive feature like trait objects.

2 Likes