4 min read
On this page

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:

  1. No methods return Self — the compiler does not know the concrete type behind dyn Trait, so it cannot construct a Self.
  2. No generic methods — the vtable has a fixed size and cannot accommodate infinite generic instantiations.
  3. No Sized bound on Self — 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 Clone or traits with generic methods. The compiler error is clear but surprising the first time. Design traits for object safety upfront if you know you need dyn.
  • Performance assumptions — the vtable indirection cost is one pointer lookup per call. For most code this is irrelevant. Do not avoid dyn purely for performance unless you have profiled.
  • Forgetting Send + Sync — in async code, trait objects often need to be Box<dyn Trait + Send + Sync>. Missing these bounds causes confusing errors when spawning tasks.
  • Lifetime elision surprisesBox<dyn Trait> is actually Box<dyn Trait + 'static> by default. If your trait object needs to borrow data, use Box<dyn Trait + 'a>.
  • Overusing dyn everywhere — 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 Trait provides runtime polymorphism through vtable dispatch, at the cost of one pointer indirection per method call.
  • Use Box<dyn Trait> for owned trait objects, &dyn Trait for borrowed ones.
  • Generics are faster (static dispatch, inlining). dyn is more flexible (heterogeneous collections, runtime selection).
  • Object safety rules restrict which traits can be used as dyn: no Self returns, no generic methods.
  • Plugin systems, strategy patterns, and heterogeneous collections are the canonical use cases for trait objects.