When to use dynamic dispatch?

Hi, all,

As this post said:

To summarize, trait objects are an advanced feature that should only be attempted by people who need dynamic dispatch.

It seems we can always using generics instead of trait object, i'm wondering in which case one must use dynamic dispatch?

Any time when a type may need to be heterogeneous.

In the simple case where Foo is a struct you might do:

fn print_all(to_print: Vec<Foo>)

But if there is no such type Foo and the method is generic with respect to multiple types you might write:

fn print_all<T>(to_print: Vec<T>)

If on the other hand Foo is a trait, then you might write:

fn print(to_print: impl Foo)

This is generic with respect to which implementation of Foo is passed but it can only be a single type.

If there are multiple different structs which all implement the trait Foo you can write:

fn print_all(to_print: Vec<Box<dyn Foo>>)

This is generally not preferable to the other approaches when they can be used. However, if you need to accept/create a data structure which contains multiple different items which all implement the same trait but are not the same type, then dyn is the only option

3 Likes

A real-life example recently came up in connection with my work. I'm designing an EDSL for database schemas, and for implementing the Active Record pattern, I'll need to have a reference to the database connection in each entity object.

However, if I were to use generics, the implementation detail as to what specific kind of database engine I'm using would leak through the interface. Since I don't want my schema and domain types to depend on the backing DB engine, I'm putting an Arc<dyn DatabaseBackend> into my entities, instead of making them generic over DB: DatabaseBackend.

Often if you want to receive a closure as argument and store it in a struct, it can be much more convenient to use dynamic dispatch as you avoid the struct becoming generic.

1 Like
  1. Reduce code size
  2. Where static (generics) is impossible (e.g. you want to be able inject implementation at runtime)
1 Like

That being said, there are also cases where you know all types that implement a trait. That's when I usually recommend using an enum, if someone asks for performance optimization tips.

In that situation, you may also store each individual type in a separate collection or sort them by type in a single collection. Those are good ways to optimize for performance, but at a cost of added complexity. As always, do not optimize too early.

The trade-offs are as follows:

  • Performance:
    1. Generics
    2. Enums
    3. Trait Objects
  • Complexity (least first):
    1. Trait Objects
    2. Enums
    3. Generics
  • Binary Size (smallest first):
    1. Trait Objects
    2. Enums
    3. Generics
  • Flexibility:
    1. Generics
    2. Enums / Trait Objects¹
  • Privacy:
    1. Trait Objects
    2. Enums
    3. Generics

¹) Enums allow calling generic methods, but require a fixed amount of types. Trait objects aren't limited to a fixed amount of implementing types, but can't call generic methods and are limited in other ways, as well. See: Object Safety

4 Likes

That's not universally applicable. Trait objects have a lot of limitations compared to generics (e.g. the lack of associated types and the inability to combine trait bounds), so some problems that are easily solved with generics would require more work if you wanted to solve them with trait objects.

Complexity is never universally applicable, because it is project-specific. The bullet points are supposed to provide a general overview. Usually the code complexity rises with an increased number of generic parameters or having to explicitly handle each possible result of an enum manually. Trait objects, on the other hand, just work for what they were made for.

If you're trying to work around the restrictions of object safety, but you really need to keep a feature, that would otherwise become unusable by adding where Self: Sized to restore object safety, then you're probably in for a lot of unsafe, e.g.