Trait Objects and Dynamic Dispatch

Dynamic Dispatch

Dynamic dispatch resolves a trait method call at runtime by looking up the function pointer in a virtual method table (vtable) attached to a type-erased pointer or reference (&dyn Trait, &mut dyn Trait, or Box<dyn Trait>).

How Dynamic Dispatch Works at Runtime

Unlike static dispatch, which generates a separate copy of a function for each concrete type at compile time, a function accepting &dyn Notifier is compiled once. At runtime, calling notifier.send(msg) reads the send function pointer from the vtable carried alongside the data pointer and jumps to that address:

Caller: notify_user(n: &dyn Notifier)
           │
           ├── 1. Reads function pointer from n.vtable.send
           └── 2. Calls (n.vtable.send)(n.data_ptr, msg)
trait Notifier {
    fn send(&self, message: &str);
}

struct EmailNotifier {
    address: String,
}

impl Notifier for EmailNotifier {
    fn send(&self, message: &str) {
        println!("[Email -> {}] {message}", self.address);
    }
}

struct SmsNotifier {
    phone: String,
}

impl Notifier for SmsNotifier {
    fn send(&self, message: &str) {
        println!("[SMS -> {}] {message}", self.phone);
    }
}

// Compiled once; dispatches `notifier.send(...)` dynamically at runtime:
fn send_alert(notifier: &dyn Notifier, alert: &str) {
    notifier.send(alert);
}

fn main() {
    let email = EmailNotifier {
        address: String::from("ops@example.com"),
    };
    let sms = SmsNotifier {
        phone: String::from("+8801700000000"),
    };

    send_alert(&email, "Disk usage > 90%");
    send_alert(&sms, "Disk usage > 90%");
}
[Email -> ops@example.com] Disk usage > 90%
[SMS -> +8801700000000] Disk usage > 90%

When Dynamic Dispatch Is Required

Dynamic dispatch is used when the concrete type cannot be fixed at compile time:

  1. Heterogeneous collections: Storing multiple distinct types that implement the same trait inside a single Vec<Box<dyn Trait>> or &[&dyn Trait].
  2. Runtime configuration / plugin systems: Choosing which concrete implementation to instantiate based on user input, config files, or environment variables.
  3. Reducing binary code bloat: Compiling a large function once over &dyn Trait instead of duplicating it across dozens of generic instantiations.
  4. Recursive or cyclic data structures over trait boundaries where a concrete nested generic type would have infinite size.