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