Allowing calling static methods through trait objects

Right now apparently static methods cannot be called from trait objects. Is there any inherent reason why this is the case? It seems like trait objects could easily grow vtable slots to include static methods?

My motivating use case is a size_hint() method used in Askama Template trait. It used to take &self, but someone reasonably pointed out that, since the size was computed at compile time, the parameter wasn’t needed. However, I then got an issue that templates can now no longer be used as trait objects, which is desirable in some use cases to prevent code size blowup.

I’m happy to write up an RFC if this turns out to be non-controversial, but maybe there’s something I’m overlooking.

1 Like

Quite a lot of previous discussion of the issues in Idea: object-safe static trait methods

EDIT: It also briefly mentions a workaround, provide both a static and dynamic method to allow access either way:

trait Template {
    fn size_hint() -> usize;
    fn dyn_size_hint(&self) -> usize {
        Self::size_hint()
    }
}
1 Like

This seems to indicate that it could be safely done. It seems like if you are able to implement manually this way, then, the compiler could relatively easily grow a vtable to support it.

So, my idea of having a default delegating implementation for dyn_size_hint doesn’t actually work, there might be some way to structure this so that you don’t need to implement both methods for every implementation, but best I could come up with quickly is

trait Template {
    fn size_hint() -> usize where Self: Sized;
    fn dyn_size_hint(&self) -> usize;
}

I think that not being able to use a default implementation maybe indicates the issue with the compiler automatically deriving it.

2 Likes

You can make it slightly less painful with an additional trait. (This still impacts public API.)

pub trait Template {
    fn size_hint() -> usize where Self: Sized;
}

// the proper trait to use in trait objects
pub trait TemplateDyn: Template {
    fn dyn_size_hint(&self) -> usize;
}

impl<T: Template> TemplateDyn for T {
    fn dyn_size_hint(&self) -> usize {
        Self::size_hint()
    }
}
2 Likes

This two-trait solution looks like an improvement, in particular it means that size_hint can be called for all T: Sized + Template, and dyn_size_hint can be called on any T: TemplateDyn, including unsized types like str and [u8] if they implement both traits.

There’s one case that the above doesn’t support: you can’t call size_hint (the static version) on unsized types other than the trait object type, such as str and [u8]. However, with a couple tweaks this is possible:

trait Template {
  fn size_hint() -> usize;
}

trait TemplateDyn {
  fn dyn_size_hint(&self) -> usize;
}

impl<T: Template + ?Sized> TemplateDyn for T {
  fn dyn_size_hint(&self) -> usize {
    Self::size_hint()
  }
}

Here is a playground link using str and i32 as examples of unsized and sized types implementing Template: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f35eb24390d1ee8f27b284f9dfa73c67

If the Template trait has other non-static methods, you can either copy them into TemplateDyn and delegate to the Template implementation in the impl<T: Template + ?Sized> TemplateDyn for T block, or use a common subtrait with a name like TemplateBase. The first option would be more work for the library maintainer but would provide a slightly nicer API for implementors.

1 Like