tezvyn:

Static Dispatch: Zero-Cost Abstraction via Monomorphization

AI-drafted, machine-checkedSource: doc.rust-lang.orgintermediate

Static dispatch resolves function calls at compile time, avoiding runtime overhead. Rust does this via monomorphization, creating specialized code for each concrete type. This is the default for generics, but the trade-off is larger binary sizes.

WHY IT EXISTS: To provide powerful abstractions like generics and traits without a runtime performance penalty. The goal is to write flexible, reusable code that runs as fast as if you had manually written a specific version for each data type.

THE MENTAL MODEL: Think of it as a highly intelligent copy-paste operation at compile time. When you write a generic function foo<T>, you're giving the compiler a template. If you call foo with a String and a u64, the compiler creates two separate, specialized functions in the final binary, one for String and one for u64. The decision of which function to run is already made before your program starts.

HOW IT WORKS: The process is called monomorphization (from 'mono,' meaning one, and 'morph,' meaning form). For every generic function or type, the Rust compiler looks at where it's used in your code. For each concrete type that replaces the generic parameter (like i32 or MyStruct), the compiler generates a brand new implementation of that function with the generic parameter filled in. This means there's no generic code in the final executable, only the specialized versions. This allows the compiler to know the exact size and layout of every type at every function call.

WHEN TO USE IT: Static dispatch is the default and idiomatic approach in Rust. You get it for free whenever you use generics with trait bounds, like fn process<T: Display>(item: T). It's ideal when performance is paramount, as it enables the compiler to perform crucial optimizations like inlining the function call, which can eliminate the function call overhead entirely.

WHEN NOT TO USE IT: The main limitation is a lack of flexibility for heterogeneous collections. A Vec<T> can only store elements of a single, concrete type T. If you need a list containing different types that all share a common behavior (e.g., Circle and Square structs that both implement a Draw trait), you must opt into dynamic dispatch using trait objects, like Vec<Box<dyn Draw>>. The other downside is potential binary bloat; using a complex generic function with many different types can significantly increase the size of your compiled program.

ONE CANONICAL EXAMPLE: Consider a generic function: fn duplicate<T: Clone>(x: T) -> (T, T) { (x.clone(), x.clone()) }. If your code calls this function with two different types: let pair_of_ints = duplicate(10_i32); and let pair_of_strings = duplicate(String::from("hi"));. The compiler effectively rewrites this into two separate, non-generic functions: fn duplicate_i32(x: i32) -> (i32, i32) { ... } and fn duplicate_string(x: String) -> (String, String) { ... }. Each call site then directly invokes its specialized version, with no runtime lookup required.

Read the original → doc.rust-lang.org

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.