Dynamic Dispatch
Static dispatch through generics is Rust's default, but sometimes you need runtime polymorphism. When you do not know the concrete type at compile time, or when you need a heterogeneous collection of different types behind the same interface, dyn Trait provides dynamic dispatch through trait objects.
What Is dyn Trait?
A trait object is a fat pointer: one pointer to the data and one pointer to a vtable containing the trait's method implementations. The dyn keyword makes this explicit:
trait Drawable {
fn draw(&self);
fn name(&self) -> &str;
}
struct Circle {
radius: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing circle with radius {}", self.radius);
}
fn name(&self) -> &str {
"circle"
}
}
struct Square {
side: f64,
}
impl Drawable for Square {
fn draw(&self) {
println!("Drawing square with side {}", self.side);
}
fn name(&self) -> &str {
"square"
}
}
fn render(shapes: &[&dyn Drawable]) {
for shape in shapes {
shape.draw();
}
}
fn main() {
let c = Circle { radius: 5.0 };
let s = Square { side: 3.0 };
render(&[&c, &s]);
}
Drawing circle with radius 5
Drawing square with side 3
The render function does not know the concrete types at compile time. It dispatches through the vtable at runtime.
Box<dyn Trait> for Owned Trait Objects
References work for borrowed trait objects, but when you need ownership — storing trait objects in a struct, returning them from a function — use Box<dyn Trait>:
trait Handler {
fn handle(&self, request: &str) -> String;
}
struct EchoHandler;
impl Handler for EchoHandler {
fn handle(&self, request: &str) -> String {
format!("Echo: {}", request)
}
}
struct UpperHandler;
impl Handler for UpperHandler {
fn handle(&self, request: &str) -> String {
request.to_uppercase()
}
}
struct Pipeline {
handlers: Vec<Box<dyn Handler>>,
}
impl Pipeline {
fn new() -> Self {
Pipeline { handlers: vec![] }
}
fn add(&mut self, handler: Box<dyn Handler>) {
self.handlers.push(handler);
}
fn process(&self, request: &str) {
for handler in &self.handlers {
println!("{}", handler.handle(request));
}
}
}
fn main() {
let mut pipeline = Pipeline::new();
pipeline.add(Box::new(EchoHandler));
pipeline.add(Box::new(UpperHandler));
pipeline.process("hello world");
}
Echo: hello world
HELLO WORLD
Vec<Box<dyn Handler>> is the idiomatic way to store a heterogeneous collection of trait implementors.
When to Use dyn vs Generics
The decision comes down to two factors: performance and flexibility.
Use generics when:
- Performance matters — no vtable lookup, the compiler inlines and optimizes
- You know all concrete types at compile time
- You want the compiler to generate specialized code for each type
- You are writing a library with public APIs that users will monomorphize
Use dyn Trait when:
- You need a collection of different types implementing the same trait
- The concrete type is determined at runtime (config files, user input, plugins)
- You want to reduce binary size by avoiding monomorphization
- You are building a plugin system or strategy pattern
- The trait is used across crate boundaries and you want to avoid recompilation
In practice, most application code benefits from starting with generics and switching to dyn when the need arises. The performance difference is usually negligible outside of tight loops.
Object Safety Rules
Not every trait can be used as dyn Trait. A trait is object-safe only if:
- No methods return
Self— the compiler does not know the concrete type behinddyn Trait, so it cannot construct aSelf. - No generic methods — the vtable has a fixed size and cannot accommodate infinite generic instantiations.
- No
Sizedbound onSelf— trait objects are unsized by definition.
// NOT object safe — returns Self
trait Clonable {
fn clone_self(&self) -> Self;
}
// NOT object safe — generic method
trait Converter {
fn convert<T>(&self) -> T;
}
// Object safe
trait Processor {
fn process(&self, input: &str) -> String;
}
If you need a clone-like operation on trait objects, use a workaround:
trait CloneBox {
fn clone_box(&self) -> Box<dyn CloneBox>;
}
impl<T: Clone + CloneBox + 'static> CloneBox for T {
fn clone_box(&self) -> Box<dyn CloneBox> {
Box::new(self.clone())
}
}
Plugin Systems with Trait Objects
Trait objects are the natural tool for plugin architectures where implementations are selected at runtime:
trait Plugin {
fn name(&self) -> &str;
fn execute(&self, input: &str) -> Result<String, String>;
}
struct MarkdownPlugin;
impl Plugin for MarkdownPlugin {
fn name(&self) -> &str { "markdown" }
fn execute(&self, input: &str) -> Result<String, String> {
Ok(format!("<p>{}</p>", input))
}
}
struct JsonPlugin;
impl Plugin for JsonPlugin {
fn name(&self) -> &str { "json" }
fn execute(&self, input: &str) -> Result<String, String> {
Ok(format!("{{\"text\": \"{}\"}}", input))
}
}
struct PluginRegistry {
plugins: Vec<Box<dyn Plugin>>,
}
impl PluginRegistry {
fn new() -> Self {
PluginRegistry { plugins: vec![] }
}
fn register(&mut self, plugin: Box<dyn Plugin>) {
println!("Registered plugin: {}", plugin.name());
self.plugins.push(plugin);
}
fn find(&self, name: &str) -> Option<&dyn Plugin> {
self.plugins.iter().find(|p| p.name() == name).map(|p| p.as_ref())
}
}
fn main() {
let mut registry = PluginRegistry::new();
registry.register(Box::new(MarkdownPlugin));
registry.register(Box::new(JsonPlugin));
if let Some(plugin) = registry.find("json") {
match plugin.execute("hello") {
Ok(output) => println!("{}", output),
Err(e) => eprintln!("Plugin error: {}", e),
}
}
}
Registered plugin: markdown
Registered plugin: json
{"text": "hello"}
Strategy Pattern
The strategy pattern uses trait objects to swap algorithms at runtime. Define a trait like Compressor with a compress method, implement it for NoopCompressor and RleCompressor, then accept &dyn Compressor in your processing function. The caller decides the algorithm; the processor does not need to know the concrete type. This is the same pattern used for sorting strategies, serialization formats, and authentication methods.
Common Pitfalls
- Ignoring object safety — trying to use
dyn Cloneor traits with generic methods. The compiler error is clear but surprising the first time. Design traits for object safety upfront if you know you needdyn. - Performance assumptions — the vtable indirection cost is one pointer lookup per call. For most code this is irrelevant. Do not avoid
dynpurely for performance unless you have profiled. - Forgetting
Send + Sync— in async code, trait objects often need to beBox<dyn Trait + Send + Sync>. Missing these bounds causes confusing errors when spawning tasks. - Lifetime elision surprises —
Box<dyn Trait>is actuallyBox<dyn Trait + 'static>by default. If your trait object needs to borrow data, useBox<dyn Trait + 'a>. - Overusing
dyneverywhere — if you only have two concrete types and know them at compile time, an enum with two variants is simpler and faster than trait objects.
Key Takeaways
dyn Traitprovides runtime polymorphism through vtable dispatch, at the cost of one pointer indirection per method call.- Use
Box<dyn Trait>for owned trait objects,&dyn Traitfor borrowed ones. - Generics are faster (static dispatch, inlining).
dynis more flexible (heterogeneous collections, runtime selection). - Object safety rules restrict which traits can be used as
dyn: noSelfreturns, no generic methods. - Plugin systems, strategy patterns, and heterogeneous collections are the canonical use cases for trait objects.