Choice between static and dynamic dispatch

Rust for Rustaceans book says use static dispatch in library and dynamic dispatch in binaries.

it says, if you use static dispatch, they can chose whether to use dynamic dispatch or not.

I did not get how library users can choose?

to test it I create small cargo project named rfr

// lib.rs file
pub trait ABC {}

pub fn compute<T: ABC>(name: T) {
    todo!();
}
// main.rs file
use rfr::{self, compute, ABC};

fn main() {
    let v: Vec<&dyn ABC> = vec![];
    for item in v {
        compute(item) // error: the trait bound `&dyn ABC: ABC` is not satisfied
    }
}

this does not compile.

here lib.rs is using static dispatch and main.rs wants to use dynamic dispatch.

Rust doesn't automatically implement traits for references. If you add a blanket impl, it compiles

Playground

pub trait ABC {}

impl<T: ?Sized> ABC for &T where T: ABC {}

pub fn compute<T: ABC>(name: T) {
    todo!();
}
6 Likes

I'm pretty sure this is not what it says. If it were this simple, then it could (and probably would) be automated away, and there'd be no need to think about this and choose at all.

You choose static vs dynamic dispatch based on what you want your code to do.

By default, static dispatch (ie. generics) are preferred. Generics allow you to encode sophisticated constraints in the type system; generic code is monomorphized, and so it does not incur the overhead of a dynamic call; and values of generic types can be statically-sized, so they can be stored by-value without allocation and indirection. Generic types can also have generic methods, which trait objects can't, so sometimes it's impossible to write a certain method on a trait object.

In contrast, you choose dynamic dispatch when you need it. For example, in a web server's router, you want to store function pointers or otherwise dynamic callables – otherwise, you couldn't store them in a uniform collection, due to the fact that every function item and closure has its own, unique type.

7 Likes

I somewhat disagree.

I believe the choice static vs dynamic dispatch is an optimization. If your function has a costly workload, like writing an object via Display to a file, I would always use dynamic dispatch, which might be even faster due to a smaller code size (if you use it for many different types). But if you need the function only for a handful of cases, and/or need the performance potential due to monomorphisation, use static dispatch.

In the end, my recommodation would be: Use dynamic dispatch if unsure. But the library/static vs binary/dynamic resonates also with me as a first order recommodation

Notr that one can also implement the trait just for a reference to the trait object.

No, it really isn't. They are semantically and substantially different, as I explained above.

You can't call generic methods or constructor-like methods that return Self by-value on trait objects. To provide a concrete example, traits like serde's Serialize and Deserialize can't possibly be dynamically dispatched. You can't put a bunch of differently-typed values in a collection, so uniform treatment of arbitrary callables is not possible using generics. They are used in different situations, and which one is at all possible to use can very well dictate which one you have to use. This is basically the exact opposite of "just an optimization".

It is possible to use one over the other as an optimization in certain situations, if both options are possible. Or maybe even for sheer convenience (I recently used a trait object in a side project of mine, purely in order to avoid infecting an internal helper type with yet another type parameter). But this is certainly not the principal reason why they exist in the first place.

Here I agree. But still, if you have a choice, the choice is an optimization. There is no clear rule, when to use which.
The OP might be actually confused by this. There is not always a choice (as you explaint in detail)

yes. adding blanket imply works.

is there any crate that derives this blanket impl from derive attributes? I guess it could be auto-generated.

I found one: https://crates.io/crates/blanket/0.2.0

Alternatively, define your function to take references:

// lib.rs file
pub fn compute<T: ABC + ?Sized>(name: &T) { … }
1 Like