Generics
Compile-Time vs. Runtime Polymorphism
Polymorphism allows a single interface to operate on multiple concrete types. Rust provides two distinct mechanisms with explicit syntax and performance characteristics:
- Compile-time polymorphism (static dispatch), written with generics (
<T: Trait>orimpl Trait), resolved during compilation via monomorphization. - Runtime polymorphism (dynamic dispatch), written with trait objects (
&dyn TraitorBox<dyn Trait>), resolved at runtime through a virtual method table (vtable).
Side-by-Side Comparison
trait Formatter {
fn format_entry(&self, key: &str, value: i32) -> String;
}
struct JsonFormatter;
impl Formatter for JsonFormatter {
fn format_entry(&self, key: &str, value: i32) -> String {
format!("{{\"{key}\": {value}}}")
}
}
struct KvFormatter;
impl Formatter for KvFormatter {
fn format_entry(&self, key: &str, value: i32) -> String {
format!("{key}={value}")
}
}
// 1. Compile-time polymorphism (Static Dispatch):
// Monomorphized into two separate functions: render_static::<JsonFormatter> and render_static::<KvFormatter>
fn render_static<F: Formatter>(formatter: &F, key: &str, value: i32) -> String {
formatter.format_entry(key, value)
}
// 2. Runtime polymorphism (Dynamic Dispatch):
// Compiled once; looks up `format_entry` in the fat pointer's vtable at runtime
fn render_dynamic(formatter: &dyn Formatter, key: &str, value: i32) -> String {
formatter.format_entry(key, value)
}
fn main() {
let json = JsonFormatter;
let kv = KvFormatter;
println!("Static: {}", render_static(&json, "port", 8080));
println!("Static: {}", render_static(&kv, "port", 8080));
println!("Dynamic: {}", render_dynamic(&json, "port", 8080));
println!("Dynamic: {}", render_dynamic(&kv, "port", 8080));
}
Static: {"port": 8080}
Static: port=8080
Dynamic: {"port": 8080}
Dynamic: port=8080
Memory and Dispatch Representation
With compile-time polymorphism, &F is a single thin pointer (8 bytes on 64-bit targets) and the compiler knows the exact target function address at compile time. With runtime polymorphism, &dyn Formatter is a 16-byte fat pointer containing both a data pointer and a vtable pointer:
Compile-Time Polymorphism (&JsonFormatter)
+--------------+
| data_ptr (8B)| ---> [JsonFormatter instance]
+--------------+
Call: direct call to JsonFormatter::format_entry (inlinable)
Runtime Polymorphism (&dyn Formatter)
+--------------+--------------+
| data_ptr (8B)| vtable_ptr(8B)|
+------+-------+------+-------+
| |
v v
[Instance] [Vtable for JsonFormatter]
+------------------------------+
| drop_in_place fn ptr |
| size, align |
| format_entry fn ptr ---------+---> JsonFormatter::format_entry
+------------------------------+
Homogeneous vs. Heterogeneous Collections
A generic Vec<T> requires every element in the vector to have the exact same concrete type T so elements can be stored contiguously by value. To store values of different concrete types in the same collection, you need runtime polymorphism (Vec<Box<dyn Formatter>>) or a closed enum:
trait Job {
fn execute(&self) -> &'static str;
}
struct EmailJob;
impl Job for EmailJob {
fn execute(&self) -> &'static str {
"sent email"
}
}
struct BackupJob;
impl Job for BackupJob {
fn execute(&self) -> &'static str {
"completed backup"
}
}
fn main() {
// Heterogeneous vector enabled by runtime polymorphism:
let queue: Vec<Box<dyn Job>> = vec![Box::new(EmailJob), Box::new(BackupJob)];
for job in &queue {
println!("Job result: {}", job.execute());
}
}
Job result: sent email
Job result: completed backup
Architectural Trade-Off Matrix
| Attribute | Compile-Time Polymorphism (<T: Trait>) |
Runtime Polymorphism (dyn Trait) |
|---|---|---|
| Dispatch mechanism | Direct static call or inlined body | Indirect function pointer call via vtable |
| Pointer size | 1 word (usize, 8 bytes on 64-bit) |
2 words (2 * usize, 16 bytes on 64-bit) |
| Compiler optimizations | Full cross-function inlining & loop vectorization | Opaque indirect call prevents inlining across boundary |
| Heterogeneous collections | Not directly (Vec<T> holds one concrete T) |
Supported (Vec<Box<dyn Trait>> or &[&dyn Trait]) |
| Binary size impact | Larger (one copy per concrete T) |
Smaller (one shared function body) |
| Trait restrictions | Works with any trait (including generic methods and Self returns) |
Requires a dyn-compatible (object-safe) trait |