Idea: make object-safe subset of `dyn Trait` methods available as inherent methods

I got a brain wave today when I was reading this blog post, which briefly mentioned object safety in Rust and how Swift avoids that issue. Now that RFC 2027 has been implemented, if you enable the feature object_safe_for_dispatch, dyn Trait is always a well-formed type, it just doesn't implement Trait if the trait isn't object-safe. Being object-safe really just means that the dyn Trait type can't implement Trait, because of one of the following reasons:

  • the trait requires Self: Sized, which dyn Trait inherently is not,
  • the trait has an associated const, which dyn Trait couldn't provide a value for, or
  • the trait has one or more methods/associated functions that can't be implemented using dynamic dispatch. For example, there is no self argument, and therefore no vtable to do dynamic dispatch on; or the method returns Self; or one of a few other reasons.

There might be another reason that I'm missing, but the main thing to notice here is that there might be some methods that can definitely be implemented using dynamic dispatch, even if the trait as a whole cannot. What if we took the subset of methods that are object-safe, and made them available as inherent methods?

Take for example this trait:

trait Trait {
    // not object safe, because it has no `self` argument
    fn static_method();
    // this method is fine
    fn call_static_method(&self) {
        Self::static_method()
    }
}

dyn Trait can't provide an implementation of static_method, and therefore can't implement Trait. But it could still provide call_static_method as an inherent method. It would be like the following code was added:

impl dyn Trait {
    fn call_static_method(&self) {
        /* compiler implemented */
    }
}

Then you could do this, assuming i32 implements Trait:

let bx = Box::new(6) as Box<dyn Trait>;
bx.call_static_method();

Note that the cast to Box<dyn Trait> isn't allowed right now, even with #![feature(object_safe_for_dispatch)]. I don't see any reason why you couldn't or shouldn't be able to do that though.

call_static_method would be an inherent method on dyn Trait, as well as on any dyn SubTrait types where SubTrait requires Self: Trait. There's one complication I just thought of, though: what if SubTrait also has a method named call_static_method? Normally, you would disambiguate the two with <dyn SubTrait as Trait>::call_static_method and <dyn SubTrait as SubTrait>::call_static_method, but that doesn't work if they are both inherent methods on dyn SubTrait. I guess it's an open question what to in that case.

What do you think? Are there any other complications that I'm missing?

cc @withoutboats @arielb1 @eddyb @nikomatsakis

With specialization this could even be implemented with a proc_macro_attribute on the trait:

#[derive_with_receiver_static_methods(template = "call_{}")]
trait Trait {
    fn static_method ()
    where
        Self : Sized,
    ;
}

would expand to:

trait Trait {
    fn static_method ()
    where
        Self : Sized,
    ;

    fn call_static_method (self: &'_ Self)
    ;
}

default impl<T> Trait for T {
    #[inline]
    fn call_static_method (self: &'_ Self)
    {
        Self::static_method()
    }
}

And if one does not want to pollute the namespace of Trait, something along the following lines could be implemented by the compiler:

#![feature(specialization)]

trait Trait {
    fn static_method ()
    where
        Self : Sized,
    ;
    
    #[doc(hidden)]
    #[allow(bad_style)]
    fn __vtable__call_static_method (self: &'_ Self)
    ;
}

default impl<T> Trait for T {
    fn __vtable__call_static_method (self: &'_ Self)
    {
        <Self as Trait>::static_method()
    }
}

impl dyn Trait + '_ {
    #[inline]
    fn static_method (self: &'_ Self)
    {
        <Self as Trait>::__vtable__call_static_method(self)
    }
}

It sounds like you're describing Rust's pre-RFC 255 behavior.

Interesting... it would be nice to hear what @nrc, the RFC author, thinks, and anyone else who was around at that time.

Actually, take a look at this paragraph in the alternatives section:

This is pretty much exactly what I'm proposing we do.