TL;DR:
dyn Traitis a fat pointer (data pointer + vtable pointer) that enables dynamic dispatch, calling trait methods on a value whose concrete type is not known at compile time. UseBox<dyn Trait>to heap-allocate a trait object with a single owner, or&dyn Traitfor borrowed trait objects. The trade-off vs generics (impl Trait/T: Trait): dynamic dispatch has a small vtable lookup cost but allows heterogeneous collections and shorter compile times. A trait must be "object-safe" to be used asdyn Trait.
What Is dyn Trait?
dyn Trait lets you hold a value whose concrete type is unknown at compile time but is guaranteed to implement a specific trait.
trait Animal {
fn speak(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn speak(&self) -> &str { "woof" } }
impl Animal for Cat { fn speak(&self) -> &str { "meow" } }
fn make_sound(animal: &dyn Animal) {
println!("{}", animal.speak());
}
fn main() {
let dog = Dog;
let cat = Cat;
make_sound(&dog); // woof
make_sound(&cat); // meow
}&dyn Animal is a fat pointer: 16 bytes = pointer to data + pointer to vtable.
How Do You Store Mixed Types in a Collection?
Use Vec<Box<dyn Trait>> to store values of different concrete types in the same collection.
trait Draw {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Draw for Circle {
fn draw(&self) { println!("Circle r={}", self.radius); }
}
impl Draw for Rectangle {
fn draw(&self) { println!("Rect {}x{}", self.width, self.height); }
}
fn render(shapes: &[Box<dyn Draw>]) {
for shape in shapes {
shape.draw();
}
}
fn main() {
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 10.0, height: 3.0 }),
];
render(&shapes);
}Without dyn, you cannot put Circle and Rectangle in the same Vec, they are different types.
What Is the Difference Between dyn Trait and impl Trait?
impl Trait is static dispatch (monomorphized at compile time). dyn Trait is dynamic dispatch (vtable lookup at runtime).
// Static dispatch; compiler generates separate code for each T
fn process_static(item: &impl Serialize) -> String {
serde_json::to_string(item).unwrap()
}
// Dynamic dispatch; one function, runtime vtable lookup
fn process_dynamic(item: &dyn Serialize) -> String {
serde_json::to_string(item).unwrap()
}impl Trait / generics | dyn Trait | |
|---|---|---|
| Dispatch | Static (compile-time) | Dynamic (vtable at runtime) |
| Performance | Faster, no indirection | Small overhead (~1ns vtable lookup) |
| Binary size | Larger, one copy per type | Smaller, one function |
| Heterogeneous collections | ❌ Not possible | ✅ Vec<Box<dyn Trait>> |
| Compile time | Slower | Faster |
What Is Object Safety?
A trait is object-safe if it can be used as dyn Trait. Traits with methods that return Self or have generic parameters are not object-safe.
// Object-safe; can be used as dyn
trait Drawable {
fn draw(&self);
fn bounding_box(&self) -> (f64, f64);
}
// NOT object-safe; Clone returns Self
// trait MyClone: Clone {} // ❌ dyn MyClone won't compile
// NOT object-safe; generic method
trait NotSafe {
fn process<T>(&self, item: T); // ❌ generic method
}Common object-safety violations:
- Method returns
Self - Method has generic type parameters
- Trait requires
Sized
Work-arounds: use where Self: Sized to exclude non-object-safe methods, or redesign using associated types.
How Do You Use Box<dyn Error>?
Box<dyn std::error::Error> is the simplest way to return any error type from main or utility functions.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("config.toml")?;
let port: u16 = content.trim().parse()?;
println!("Listening on port {port}");
Ok(())
}Box<dyn Error> accepts any type that implements std::error::Error, the ? operator uses From to box the error automatically.
Frequently Asked Questions
dyn Trait alone is unsized, the compiler doesn't know how many bytes it takes. Box<dyn Trait> stores the object on the heap with a known pointer size (16 bytes). You can also use Arc<dyn Trait> for shared ownership or &dyn Trait for borrowed references.
Yes, Box<dyn Trait + Send> or Box<dyn Trait + Send + Sync> adds the auto-trait bounds. Required when sending trait objects across threads.
By a tiny, usually unmeasurable amount. A vtable lookup is an indirect function call, typically 1–5 nanoseconds. Only matters in extremely hot loops. In most web/application code, dyn Trait is perfectly fine.
A vtable (virtual dispatch table) is a struct of function pointers, one per trait method. The dyn Trait fat pointer includes a pointer to the vtable, so calling a method means: load vtable pointer → load function pointer from vtable → call it. One level of indirection vs direct call.
Sources
Related Glossary Terms
- Trait: The foundation that
dyn Traituses - impl Trait: Static dispatch alternative to
dyn - Box:
Box<dyn Trait>is the most common trait object form - Generic: The compile-time alternative to dynamic dispatch
- Associated Types: Associated types make trait objects more complex and more powerful
- async-trait: async-trait is often introduced when teams need dynamic dispatch for async trait methods
Keep Reading
- Rust Generics and Traits Explained: dyn Trait is one of two ways to use traits; generics are the other
- Rust Lifetimes Deep Dive: dyn Trait objects have implicit lifetime bounds
- Rust Ownership and Borrowing Explained:
Box<dyn Trait>and ownership of trait objects
