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 Trait or Box<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.