RustArena

Step 3 of 7

Static vs dynamic dispatch

You now have two ways to write code that works with "any Shape":

use std::mem::size_of;

trait Shape {
    fn area(&self) -> f64;
}

struct Square {
    side: f64,
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn total_static<T: Shape>(shapes: &[T]) -> f64 {
    let mut sum = 0.0;
    for s in shapes {
        sum += s.area();
    }
    sum
}

fn total_dynamic(shapes: &[Box<dyn Shape>]) -> f64 {
    let mut sum = 0.0;
    for s in shapes {
        sum += s.area();
    }
    sum
}

fn main() {
    let squares = vec![Square { side: 1.0 }, Square { side: 2.0 }];
    let boxed: Vec<Box<dyn Shape>> = vec![Box::new(Square { side: 3.0 })];
    println!("{} {}", total_static(&squares), total_dynamic(&boxed));
    println!("{} {} {}", size_of::<&Square>(), size_of::<&dyn Shape>(), size_of::<Box<dyn Shape>>());
}
5 9
8 16 16

Static dispatch (generics, impl Trait): the compiler writes a separate copy of total_static for each type it's used with (monomorphization, from the generics module). Inside each copy it knows exactly which area to call, so it can call it directly or even inline it.

Dynamic dispatch (dyn Trait): there's one copy of total_dynamic. Each call looks up the right area at run time in a vtable, a small table of function pointers the compiler builds for every (type, trait) pair. That's why a &dyn Shape is twice the size of a &Square: it's a fat pointer, holding the address of the data and the address of its vtable.

&dyn Shape = [ data pointer | vtable pointer ]
                    |               |
                    v               v
          Square { side: 3.0 }   vtable for (Square, Shape):
                                   area -> Square::area
                                   size, alignment, drop

Which one to use?

Generics (static) dyn Trait (dynamic)
Mixed types in one collection no: one T at a time yes
Type chosen at run time (config, user input, plugins) no yes
Call cost direct, can be inlined one indirect call per method
Compiled code one copy per type (bigger binary, longer builds) one copy
Extra requirements none the trait must be dyn compatible (step 5)

In practice: use generics by default for functions, especially in libraries and hot loops. Use dyn Trait when you need different types together or a choice made at run time, and don't worry about the cost: an indirect call takes a few nanoseconds and only matters in very tight loops. Some teams also switch to dyn to cut compile times and binary size when a generic is instantiated for many types.

Common mistakes

Your task

An adventurer's backpack. Write both versions:

Challenge 2

Fill in the blanks: area_of should use static dispatch (a generic written as impl Trait), and area_of_any dynamic dispatch. The program prints 4 9.

Challenge 3

The type is chosen at run time, which is exactly what dyn is for. The first line of stdin is metric or imperial; every following line is a distance in kilometers. Write a trait Units with fn format(&self, km: f64) -> String, two types Metric (12.5 km, one decimal) and Imperial (miles = km * 0.621371, 7.8 mi, one decimal), pick one as a Box<dyn Units> based on the first line, and print every distance with it. Any first line other than imperial means metric.

Previous: &dyn Trait and Box<dyn Trait> Next: Trait objects in structs: plugins