Rust allows the programmer to choose between static and dynamic dispatch pretty much on a case by case basis. In real world terms, the performance difference between the two is practically invisible; a branch is a branch is a branch. When should we choose one over the other and why?

Yeah, I know. More talk about Rust. You know, I barely even feel like a member of the Mozilla/Rust community, but I still like the language, so sue me. Let's just get to topic at hand.

How are dynamic and static dispatch represented?

The answer is that they are implied more than anything else. Here, I'll cut you a pair of quick examples.

Static dispatch

Static is really Rust's default setting; we normally write code that looks like this. Here we have a state object (used to track the average of some value, I guess?) which can add values either one at a time or from some other state object. These values are added via a tagged or discriminated union (known as an enum in Rust), meaning we statically branch based on the discriminant of the union using the match expression found inside the apply_static function.

#[derive(Debug, Default)]
struct State {
    count: usize,
    value: usize,
}

impl State {
    fn average(&self) -> usize {
        match self.count {
            0 => self.value,
            n => self.value / n,
        }
    }
}

enum TaggedUnion {
    A(usize),
    B(State),
}

impl State {
    fn apply_static(&mut self, increment: TaggedUnion) {
        use self::TaggedUnion::*;

        match increment {
            A(value) => {
                self.count += 1;
                self.value += value;
            }

            B(State { count, value }) => {
                self.count += count;
                self.value += value;
            }
        }
    }
}

fn main() {
    let mut state = State::default();
    state.apply_static(TaggedUnion::A(5));
    state.apply_static(TaggedUnion::B(State { count: 3, value: 13 }));
    println!("{}", state.average());
}

Pretty cool, right? Ok, like I said, it's perfectly normal... Whatever.

Dynamic dispatch

Rather than repost all of the code above, I'll just post what I've added. First off, there's this trait and these two implementations:

trait Increment {
    fn update(&self, state: &mut State);
}

impl Increment for usize {
    fn update(&self, state: &mut State) {
        state.count += 1;
        state.value += *self;
    }
}

impl Increment for State {
    fn update(&self, state: &mut State) {
        state.count += self.count;
        state.value += self.value;
    }
}

Next, there's this block that I just added to the bottom of main:

let mut state = State::default();
state.apply_dynamic(&5);
state.apply_dynamic(&State { count: 3, value: 13 });
println!("{}", state.average());

Of course, this block does exactly what the previous block did, except that we are calling apply_dynamic using an Increment trait object (a reference to either a usize or State) rather than using the tagged union.

Again, we're talking about code that compiles down to, effectively, a single branch. Do we do A or B? It's just a matter of how A and B are represented.

What's the big deal?

Let's change one thing. Here is our new, new block in main:

let mut state = State::default();
state.apply_dynamic(&5);
println!("{}", state.average());

See what I did? I removed the second call to apply_dynamic, which means that this code is now dead:

impl Increment for State {
    fn update(&self, state: &mut State) {
        state.count += self.count;
        state.value += self.value;
    }
}

Now, this may be some kind of linting option I'm just not aware of, but the significant fact here is that, because this is a trait implementation, by default our linter does not detect this as dead code in spite of the fact that it never gets used now. If you legitimately don't need to use this, ok, fine... But why did you write the code if it doesn't matter?

A real case

I recently rewrote a lexer for a particular file type. (Editor's note: boring details redacted. You're welcome!) It now spits out the following lexemes:

  • Comments
  • Headings
  • Text
  • Whitespace

When I got done, the first thing I noticed was that I had a dead code warning regarding headings. After a few minutes of digging, I realized I had never written the code that handled headings and emitted them. Had I written this in a dynamic fashion (returning some boxed LexemeTrait rather than returning Lexeme as a tagged union), the compiler wouldn't have noticed, and neither would I, and fixing this bug would have taken upwards of an hour instead of ten minutes.

tl;dr: when possible, err on the side of static vs. dynamic, because the compiler is your friend and wants to help you.

Full code. Playground.