Should I prefer `impl/& impl/&mut impl` or `Box<dyn>/& dyn/& mut dyn` by default?

When both options are possible, should I prefer to use dyn or impl? I know that dyn has a (small, and possibly optimized out) runtime cost while impl has no runtime cost, but can generate code bloat and may slow the compilation. What should be the rule of thumb?

When choosing the return value of a function, I almost always go for impl Trait as the return type when it is possible.

And for input parameters, especially for references?

For input parameters I prefer generics over dynamic dispatch.

So impl in all cases. This make sense.

I prefer to write it using generics instead of impl trait, but that is just a style difference.

Except for the syntax itself, fn <T: SomeTrait>(t: &mut T) and fn(t: &mut impl SomeTrait) are exactly the same, aren't they?

There is one difference, actually: You can only use turbofish syntax with the former. That is not why I prefer it though — I think &mut impl Trait is misleading and looks too much like &mut dyn Trait.

1 Like

If input by reference then use &[mut] (impl ?Sized + Trait)
(or <T : ?Sized + Trait> ... &[mut] T if turbofish matters). This way you support both the ergonomics and runtime performance of static dispatch, while keeping the possibility for downstream users to monomorphise to T = dyn Trait exclusively when compile time and code bloat are an issue.

  • Given that closures are unnameable (except for fn pointers), including them for turbofish is actually counter productive, so in that case I personally favor the impl syntax;

  • Depending on the trait and its usage, I may directly use dyn in some cases, such as when dealing with Display or Write (&'_ dyn Display and &'_ mut dyn Write);

  • In return position, like @alice said, one ought to favor impl Trait (or Self::AssocType with AssocType : Trait when implementing a trait's function / method), since Box<dyn Trait> involves an allocation (except when the type being boxed is zero-sized), and that people wanting a Box<dyn Trait + '_> can simply box the return type themselves.

  • When the trait is not object-safe, only generics can be used (although the main reason for a trait not to be object safe is for it to be using geenric parameters in its methods to begin with).

  • In practice the runtime performance of virtual dispatch is very often negligible, so resorting to & [mut ] dyn Trait by default for input parameters is a strategy some developers advise.

    • For instance, many async / Future-heavy libraries and applications get away with using #[async_trait], despite its leading to more Boxing and virtual dispatch (all the async fn methods in a trait get transformed to:

      ) -> Box<dyn Future...>
      {
          Box::new(async move { ... })
      }
      
4 Likes

Thanks a lot for the details.

1 Like

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