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
dyneverywhere "to be flexible". Every value needs aBox(a heap allocation) and the compiler can't inline the calls. If a function only ever gets one type at a time, a generic is simpler and faster.- Generics for a mixed collection.
fn total<T: Shape>(shapes: &[T])can't take circles and squares together; that's the E0308 from step 1. Mixing needsdyn. - Optimizing the vtable call before measuring. It's almost never the bottleneck; allocations and algorithms usually are (the performance module shows how to measure).
Your task
An adventurer's backpack. Write both versions:
fn backpack_weight(items: &[Box<dyn Item>]) -> u32: the total weight of a mixed backpack (dynamic dispatch).fn stack_weight<T: Item>(items: &[T]) -> u32: the total weight of a stack of one kind of item (static dispatch).
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