Trait Objects and Dynamic Dispatch
Static Dispatch
Static dispatch resolves a function or trait method call to a specific concrete implementation at compile time, emitting a direct jump (call) or inlining the function body directly into the caller.
How Static Dispatch Works via Monomorphization
When you write a generic function constrained by a trait bound (fn process<T: Hasher>(h: &mut T) or fn process(h: &mut impl Hasher)), the compiler performs monomorphization: it generates a specialized machine-code copy of the function for each concrete type T passed at any call site.
Generic Source Code:
fn area<S: Shape>(s: &S) -> f64 { s.area() }
Monomorphized Machine Code (Compile Time):
area::<Circle>(s: &Circle) ──> calls Circle::area(s) directly
area::<Rectangle>(s: &Rectangle) ──> calls Rectangle::area(s) directly
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
struct Square {
side: f64,
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
// Static dispatch: monomorphized separately for `Circle` and `Square`
fn print_area<S: Shape>(shape: &S) {
println!("Area: {:.2}", shape.area());
}
fn main() {
let c = Circle { radius: 2.0 };
let s = Square { side: 3.0 };
print_area(&c); // Calls print_area::<Circle>
print_area(&s); // Calls print_area::<Square>
}
Area: 12.57
Area: 9.00
Characteristics of Static Dispatch
| Property | Static Dispatch Behavior |
|---|---|
| Call resolution | Compile time |
| Runtime call overhead | Zero indirection; direct call or inlined instructions |
| Compiler optimizations | Enables inlining, constant propagation, and loop vectorization across trait boundaries |
Homogeneous collections (Vec<T>) |
All elements in Vec<T> must share the exact same concrete type T |
| Binary size impact | Increases code size if a large generic function is instantiated across many concrete types |
Static Polymorphism Over a Known Closed Set of Types (enum Dispatch)
When you need a single collection or variable to hold values from a fixed, closed set of types while keeping static dispatch and contiguous stack/vector layout, wrap the variants in an enum and dispatch with match:
enum PaymentMethod {
Cash,
Card { last_four: u16 },
}
impl PaymentMethod {
fn describe(&self) -> String {
match self {
PaymentMethod::Cash => String::from("Cash"),
PaymentMethod::Card { last_four } => format!("Card ending in {last_four:04}"),
}
}
}
fn main() {
let methods = [
PaymentMethod::Cash,
PaymentMethod::Card { last_four: 4242 },
];
for m in &methods {
println!("{}", m.describe());
}
}
Cash
Card ending in 4242