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