Advanced Type Features
dyn Trait vs. impl Trait
dyn Trait (dynamic dispatch via a runtime trait-object fat pointer) and impl Trait (static dispatch via compile-time monomorphization or opaque types) are the two mechanisms for polymorphic abstraction over traits in Rust.
Architectural & Performance Comparison
Static Dispatch (`impl Trait` / `<T: Trait>`):
Compile Time: Generates specialized machine code per concrete type `T`.
Stack / Heap: Value stored inline with exact `size_of::<T>()`; 0 pointer indirection.
Method Call: Direct call `ConcreteType::method(&val)` (eligible for inlining!).
Dynamic Dispatch (`&dyn Trait` / `Box<dyn Trait>`):
Compile Time: Generates ONE function taking a 16-byte `(data_ptr, vtable_ptr)` fat pointer.
Stack / Heap: Requires pointer indirection (`&` or `Box` / `Arc`).
Method Call: Indirect call `(*(vtable_ptr.method))(data_ptr)` (blocks inlining).
| Dimension | impl Trait (Static Dispatch) |
dyn Trait (Dynamic Dispatch) |
|---|---|---|
| Dispatch resolution | Compile time (monomorphized) | Runtime (indirect vtable lookup) |
| Inlining & LLVM optimization | Yes (compiler sees concrete callee body) | No (opaque function pointer in vtable) |
| Heap allocation required? | No (stored inline on the stack) | Only if owned (Box<dyn Trait>, Arc<dyn Trait>); &dyn Trait borrows on stack |
Heterogeneous collections (Vec<...>) |
No (Vec<impl Trait> is illegal; all elements must share one concrete type) |
Yes (Vec<Box<dyn Trait>> or &[&dyn Trait] can mix arbitrary types!) |
| Conditional return of different types | No (if and else must return the exact same concrete type) |
Yes (if cond { Box::new(A) } else { Box::new(B) }) |
| Binary code size | Larger if instantiated with many distinct types | Smaller (single compiled function body for all types) |
| Dyn-compatibility required? | No (works with GATs, generic methods, Clone, Self by value) |
Yes (must be dyn-compatible) |
Demonstrating When dyn Trait Is Required vs. When impl Trait Wins
trait Formatter {
fn format_text(&self, input: &str) -> String;
}
struct UppercaseFormatter;
impl Formatter for UppercaseFormatter {
fn format_text(&self, input: &str) -> String {
input.to_uppercase()
}
}
struct BracketFormatter;
impl Formatter for BracketFormatter {
fn format_text(&self, input: &str) -> String {
format!("[{input}]")
}
}
// 1. Static dispatch (`impl Formatter`): zero indirection, inlined per concrete caller type:
fn format_single(fmt: &impl Formatter, text: &str) -> String {
fmt.format_text(text)
}
// 2. Dynamic dispatch (`Box<dyn Formatter>`): required when runtime config selects
// between DIFFERENT concrete types or stores a heterogeneous `Vec`:
fn select_formatter(loud: bool) -> Box<dyn Formatter> {
if loud {
Box::new(UppercaseFormatter)
} else {
Box::new(BracketFormatter)
}
}
fn main() {
println!("static: {}", format_single(&UppercaseFormatter, "fast path"));
// Heterogeneous collection of trait objects:
let pipeline: Vec<Box<dyn Formatter>> = vec![select_formatter(false), select_formatter(true)];
let mut text = String::from("dynamic pipeline");
for step in &pipeline {
text = step.format_text(&text);
}
println!("dynamic: {}", text);
}
static: FAST PATH
dynamic: [DYNAMIC PIPELINE]
Decision Guide
- Default to
impl Trait(or<T: Trait>) for hot loops, iterator combinators, closures, futures, and traits that are not dyn-compatible. - Choose
&dyn TraitorBox<dyn Trait>when you need a heterogeneous collection (Vec<Box<dyn Plugin>>), runtime-selected implementations (select_formatter(flag)), recursive trait structures, or deliberately smaller binary size at cold API boundaries.