
Understanding impl Trait vs dyn Trait in Rust
In Rust, you can write polymorphic function parameters in two main ways:
fn make_speak(speaker: impl Speak) {
println!("Speak {}", speaker.speak())
}
fn make_speak_(speaker: &dyn Speak) {
println!("Speak {}", speaker.speak())
}
Although both accept any type that implements Speak, they behave very differently.
Impl Speak — Static Dispatch (Generics)
The compiler generates a separate version of the function for each concrete type. This process is called monomorphization.
Example
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String {
"Woof".to_string()
}
}
impl Speak for Cat {
fn speak(&self) -> String {
"Meow".to_string()
}
}
make_speak(Dog);
make_speak(Cat);
The compiler generates something like:
make_speak_for_Dog(...)
make_speak_for_Cat(...)
Characteristics
- Resolved at compile time
- No runtime dispatch cost
- May increase binary size (multiple specialized versions)
- Concrete type is known This is ideal when performance matters and the concrete type does not need to vary at runtime.
&dyn Speak — Dynamic Dispatch (Trait Object)
fn make_speak_(speaker: &dyn Speak)
Here, the concrete type is erased at compile time. The function operates on a trait object, and method calls are resolved at runtime through a vtable.
Important: dyn Trait does not automatically mean heap allocation. It must be used behind a pointer (&, Box, Rc, etc.), but the data itself can live on the stack or heap depending on how it’s created.
Internally
&dyn Speak is a fat pointer containing:
- Pointer to the data
- Pointer to the vtable
Characteristics
- Resolved at runtime
- Small runtime cost (vtable lookup)
- Smaller binary size
- Enables heterogeneous collections (e.g., Vec<Box
>)
Memory Difference
impl Speak
- Concrete type
- Size known at compile time
- Stored directly on stack
&dyn Speak
- Pointer to some value
- Needs indirection
- Size is fixed (two pointers)
Conclusion:
Both impl Trait and dyn Trait enable polymorphism in Rust, but they serve different purposes.
impl Trait uses static dispatch, meaning the concrete type is known at compile time. This results in better performance and zero runtime overhead, but can increase binary size due to monomorphization. It’s ideal when performance matters and the type does not need to vary at runtime.
dyn Trait uses dynamic dispatch, where the concrete type is determined at runtime through a vtable. It enables true runtime polymorphism and allows storing different types together (e.g., in a Vec<Box
Reference:
https://en.wikipedia.org/wiki/Monomorphization https://doc.rust-lang.org/std/keyword.dyn.html