Dynamic Dispatch


Sometimes, we want to take the decision of which implementation to use at runtime instead of letting the compiler monomorphize the code.

There's two approaches.

Dispatch through Enums

If the number of possible choices is limited, an Enum can be used:

#![allow(unused)]
fn main() {
enum Operation {
    Get,
    Set(String),
    Count
}

fn execute(op: Operation) {
    match op {
        Operation::Get => { }
        Operation::Set(s) => { }
        Operation::Count => { }
    }
}
}

Alternative Form

#![allow(unused)]
fn main() {
enum Operation {
    Get,
    Set(String),
    Count
}

impl Operation {
    fn execute(&self) {
        match &self {
            &Operation::Get => { }
            &Operation::Set(s) => { }
            &Operation::Count => { }
        }
    }
}
}

Recommendation

For best performance, try to minimize repeated matches on the enum.

See https://godbolt.org/z/8Yf4751qh

Note:

It takes multiple instructions to extract the tag from the enum and then jump to the appropriate block of code based on the value of that tag. If you use the Trait Objects we describe later, the kind of thing is encoded in the pointer to the dynamic dispatch table (or v-table) and so the CPU can just do two jumps instead of 'if this is 0, do X, else if this is a 1, do Y, else ...'.

Trait Objects

We can make references which do not know the type of the value but instead only know one particular trait that the value implements.

This is a trait object.

Internally, trait objects are a pair of pointers - one to a vtable and one the value itself.

Note:

The term vtable is short for virtual dispatch table, and it's basically a struct full of function pointers that is auto-generated by the compiler.

Usage

fn print(thing: &dyn std::fmt::Debug) {
    // I can call `std::fmt::Debug` methods on `thing`
    println!("{:?}", thing);
    // But I don't know what the *actual* type is
}

fn main() {
    print(&String::from("hello"));
    print(&123);
}

Limitations

  • You can only use one trait per object
    • Plus auto traits, like Send and Sync
  • This trait must fulfill certain conditions

Rules for dyn-compatible traits (abbreviated)

  • Must not have Self: Sized
  • No associated constants or GATs
  • All methods must:
    • Have no type parameters
    • Not use Self, only &self etc
    • Not return impl Trait

See the docs for details.

Note that these used to be called "object safety" rules before 1.83.

Performance

There is a small cost for jumping via the vtable, but it's cheaper than an enum match.

See https://godbolt.org/z/cheWrvM45

Trait Objects and Closures

Closure traits are dyn-compatible.

#![allow(unused)]
fn main() {
fn factory() -> Box<dyn Fn(i32) -> i32> {
    let num = 5;

    Box::new(move |x| x + num)
}
}

Is this a reference to a String?

Any type that is 'static + Sized implements std::any::Any.

We can use this to ask "is this reference actually a reference to this specific type?"

fn print_if_string(value: &dyn std::any::Any) {
    if let Some(s) = value.downcast_ref::<String>() {
        println!("It's a string({}): '{}'", s.len(), s);
    } else {
        println!("Not a string...");
    }
}

fn main() {
    print_if_string(&0);
    print_if_string(&String::from("cookie monster"));
}

Note:

Be sure to check the documentation because Any has some important restrictions.