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:

  1. Compile-time polymorphism (static dispatch), written with generics (<T: Trait> or impl Trait), resolved during compilation via monomorphization.
  2. Runtime polymorphism (dynamic dispatch), written with trait objects (&dyn Trait or Box<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