Trait object safety still confuses me

The main idea of trait object safety is that a trait object cannot be made of a trait which requires knowing the Self type. Methods which take self in any form work fine because they can find the vtable pointer in self and use it to find the concrete method of the trait object.

The confusing part is, how are associated functions so special that they cannot be looked up using a vtable? (Same thing with associated constants, i.e. I don't see anything wrong with the constants residing in the vtable.) Every explanation seems to mention that having the live trait object is required to figure out the vtable and thus to call its method definition, but the fact that a code path can encounter this issue by itself means that it knows that there's a trait object at compile time and that dynamic dispatch is needed, as well as the object itself, which means free access to the vtable.

This seems contrary to what the definition of trait object safety says. The exact point where I start to disagree with it is when it says that the code which should resolve the associated function has no access to the trait object — but the part which performs dynamic dispatch is the calling code, i.e. the code which has the trait object itself, therefore it can check the vtable and get out the associated function, if it's there, and it has zero reasons not to be there... right..?

Let me demonstrate what I want to happen with a pseudocode example.

trait ThingDoer {
    fn do_a_thing() -> Self;
};
struct HWThingDoer;
impl ThingDoer for HWThingDoer {
    fn do_a_thing() -> Self {
        println!("Did a thing at lightning speed using a GPU!");
    }
}
struct DefaultThingDoer;
impl ThingDoer for DefaultThingDoer {
    fn do_a_thing() -> Self {
        println!("Slowly did a thing on the CPU.");
    }
}

fn main()  {
    let mut thingdoer: Box<dyn ThingDoer> = Box::from(HWThingDoer);
    thingdoer::do_a_thing();
    thingdoer = Box::from(DefaultThingDoer);
    thingdoer::do_a_thing();
}

It's crystal clear that in main, the trait object and its vtable (i.e. the concrete implementation) are trackable at runtime. So, why doesn't Rust allow this?

thingdoer::do_a_thing();

In current Rust syntax a path goes before the :: rather than a value. Paths and values have separate namespaces, e.g. you might have both a type and value with the same name. At the very least you'd need a different syntax, to disambiguate this for the parser.

If you need to provide a value in order to call the function, then why not pass it as a self parameter and then your code will work today: thingdoer.do_a_thing()

If you also want to be able to call the method on non-dyn types without providing a self argument, then you can have a second self-less version with a where Self: Sized bound. Playground. (A macro could perhaps eliminate the boilerplate here.)

And yes, it would be possible for the compiler to generate similar code to access associated functions/consts through the vtable of a trait object pointer. We just might need to add some new syntax to do so. There's no fundamental obstacle, though it might not be the highest priority because other workarounds are possible.

At the very least you'd need a different syntax, to disambiguate this for the parser.

That's the biggest issue with this. Since Rust 2021 is proposed, and this playground example works, there's a good chance of this change entering Rust pretty soon if an RFC is composed. (Would it be a good idea to make one myself? I have never contributed to the Rust compiler of the RFCs before, so I don't really know where to start.)

it might not be the highest priority because other workarounds are possible

Passing &self to a method which is meant to be associated, or worse, having a &selfified version of it is horribly, horribly wrong from a semantic standpoint, screaming "I'm a hack to overcome language limitations". Redesigning code to avoid using traits at all just feels like a better thing to do, but harms flexibility.

A better vision of the new syntax is trait_object::Self::associated_function(). Because Self is a keyword, the trait can never declare an associated type named Self, meaning that a new edition of Rust is not even required, and this change can reach stable even in Rust 2015, let alone 2018 and the proposed 2021. This also feels more concise and readable, speaking for itself ("dispatch the actual type of trait_object using vtables and invoke its associated function"). Same thing for associated constants, they just need to reside in the vtable too. (A more problematic question is, though, how complex would that be in terms of implementation, since that'd require modifying the vtable format. We don't have ABI stability, but just stuffing constants into vtables could require editing the part responsible for finding function pointers in vtables, since then it'd mess up the assumption that all fields in a vtable have the same size, which is not guaranteed by any kind of alignment rule, since constants can be bigger than a pointer.)

Also note that an associated const accessed through dynamic dispatch would no longer be usable in const context, which would be a little confusing.

And several of your examples are functions that return Self, which still wouldn't be usable for trait objects (or other unsized types) even if the vtable issues were solved.

So this feature might not be as useful as it first appears.

1 Like

Why not take one bite at a time? You could write a pre-RFC for associated functions, leaving the topic of associated constants for a future RFC. You will want to start that discussion on the Rust internals forum, https://internals.rust-lang.org/, where your initial post can link to this thread in this forum.

1 Like

I don't see what's so wrong about this. You're trying to dispatch a function call on an object's vtable, it stands to reason that you have access to that object. Why is it 'horribly wrong' to ask that the function signature denote that object access is required?

It makes plenty of sense to have associated functions in a vtable, but you never have a vtable without an object, because you could always just refer to the concrete type or the trait directly. Dynamic dispatch requires an object because without an object, there's no need for dynamic dispatch.

Self doesn't refer to a trait, it always refers to a concrete type. It would be surprising for Self to be used to resolve a trait method.

3 Likes

A bit of that idea, regarding constants though (but an associated function is just a constant, it's just that one does not need to name its (compiler-generated) type), was being informally discussed on Discord with @Centril.

They seemed to be, at first glance, not against the idea, and the only actual question was the syntax.

It seemed that field access still made more sense than anything else (e.g., it would be slightly less confusing if trait_obj.CONSTANT wasn't usable in a const context).

So extrapolating the syntax from constants to associated functions, the most logical candidate is just method syntax:

trait_obj.associated_function()

Given that an associated function cannot have the same name as a method, this, at first glance, looks like it could work well.

Obviously one would have to think about how that interacts with method resolution order, to:

  • either ensure there would be no breakage and thus no need for a new edition,

  • or there could be breakage because of how methods would shadow each other, in which case it would require an edition change.

2 Likes

I created an RFC addressing this.

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