Concept · Rust

Static vs dynamic dispatch

Generics compile a copy per type and call directly; trait objects share one copy and call through a vtable. Pick per call site.

3 minmonomorphizationdyn Traittrait objectsvtable

When code calls a method through an abstraction, the program has to decide which implementation runs. It can decide at compile time (static dispatch) or at runtime (dynamic dispatch).

Static: generics and monomorphization

rust
fn total_area<S: Shape>(shapes: &[S]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

The compiler generates a separate copy of total_area for every concrete S it's used with (monomorphization). Each call is direct and can be inlined, so the abstraction costs nothing at runtime. C++ templates and Rust generics work this way. The costs: longer compile times and larger binaries, and one call site handles one type at a time.

Dynamic: trait objects and vtables

rust
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

A Box<dyn Shape> is a fat pointer: one pointer to the data, one to a vtable — a table of function pointers for that concrete type. Each call loads the function pointer and jumps through it. One copy of the code handles any mix of types, decided at runtime. Interfaces in Java, C# and Go dispatch this way for every call.

Trade-offs

StaticDynamic
Call costDirect, inlinableIndirect call, no inlining
Mixed types in one collectionNoYes
Code sizeA copy per typeOne copy
RestrictionsNoneTrait must be dyn compatible (no generic methods, no returning Self)

Default to generics in Rust; reach for dyn Trait for heterogeneous collections, plugin registries, or cold code where compile time and binary size matter more than a few nanoseconds. The indirect call itself is cheap; the lost inlining is what costs in hot loops.

Covered with examples in the traits and generics lesson. The design principle behind both is dependency inversion.

Learn it in depth