Trait Objects and Dynamic Dispatch

Trait Objects in Collections

Standard collections such as Vec<T> require every element to have the same compile-time size (T: Sized). To store a heterogeneous collection of different concrete types that implement a common trait, store trait object pointers (Box<dyn Trait> or &dyn Trait) inside the collection.

Memory Layout of Vec<Box<dyn Trait>>

In a Vec<Box<dyn Widget>>, the vector's contiguous buffer stores uniform 16-byte fat pointers (Box<dyn Widget>), while each fat pointer points to its own separately sized concrete struct on the heap and its corresponding vtable:

Vec<Box<dyn Widget>> Buffer (Contiguous 16-byte Fat Pointers)
+------------------------+------------------------+
| [0] data_ptr | vtable  | [1] data_ptr | vtable  |
+-------+----------------+-------+----------------+
        |                        |
        v                        v
  +---------------+        +------------------------------+
  | Label (24 B)  |        | ProgressBar (16 B)           |
  +---------------+        +------------------------------+
trait HealthCheck {
    fn name(&self) -> &str;
    fn check(&self) -> bool;
}

struct PostgresCheck {
    timeout_ms: u64,
}

impl HealthCheck for PostgresCheck {
    fn name(&self) -> &str {
        "postgres"
    }

    fn check(&self) -> bool {
        self.timeout_ms <= 1_000
    }
}

struct RedisCheck {
    cluster_mode: bool,
}

impl HealthCheck for RedisCheck {
    fn name(&self) -> &str {
        "redis"
    }

    fn check(&self) -> bool {
        self.cluster_mode
    }
}

fn main() {
    // Heterogeneous vector holding two different concrete types:
    let checks: Vec<Box<dyn HealthCheck>> = vec![
        Box::new(PostgresCheck { timeout_ms: 250 }),
        Box::new(RedisCheck { cluster_mode: true }),
    ];

    for check in &checks {
        println!("{:<10} healthy = {}", check.name(), check.check());
    }
}
postgres   healthy = true
redis      healthy = true

Non-Allocating Slices of Borrowed Trait Objects (&[&dyn Trait])

When the concrete items already live on the stack and you only need to iterate over them temporarily, use an array or slice of borrowed trait objects ([&dyn Trait; N] or &[&dyn Trait]) to avoid heap allocations altogether:

use std::fmt::Display;

fn print_row(columns: &[&dyn Display]) {
    let formatted: Vec<String> = columns.iter().map(|col| col.to_string()).collect();
    println!("| {} |", formatted.join(" | "));
}

fn main() {
    let id = 1042_u32;
    let user = "alice";
    let active = true;

    // Coerces `[&u32, &&str, &bool]` into `&[&dyn Display]` on the stack:
    print_row(&[&id, &user, &active]);
}
| 1042 | alice | true |