Skip to content
Trait Objects (dyn Trait)
step 1/7

Reading — step 1 of 7

Learn

~3 min readLifetimes, Trait Objects, and Async

Generics give compile-time polymorphism (monomorphization — one specialized version per type). Trait objects give RUNTIME polymorphism via dynamic dispatch — like virtual functions in C++ or interface references in Java.

The Rust Book Ch 17 contrasts the two approaches.

Static vs dynamic dispatch

Static (generic):

fn announce<T: Display>(item: T) {
    println!("{}", item);
}

The compiler creates a separate copy of announce for each concrete type used. Direct calls, fully optimized.

Dynamic (trait object):

fn announce(item: &dyn Display) {
    println!("{}", item);
}

One announce exists. &dyn Display is a fat pointer: data pointer + vtable pointer. Each method call goes through the vtable.

Box<dyn Trait> for owned heterogeneous collections

Generics can't hold mixed types in a Vec — Vec<T> requires all elements to be the same T. Trait objects can:

trait Shape {
    fn area(&self) -> f64;
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.radius.powi(2) } }
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } }

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];
    
    for s in &shapes {
        println!("{}", s.area());
    }
}

A Vec<Circle> can only hold Circles. A Vec<Box<dyn Shape>> can hold ANY type implementing Shape.

Object safety

Not all traits can be made into trait objects. The trait must be object-safe:

  • All methods take &self, &mut self, or Box<Self> (no self by value)
  • No methods return Self
  • No generic methods
  • No Sized super-trait
trait Bad {
    fn clone(&self) -> Self;             // returns Self — not object-safe
    fn process<T>(&self, x: T);          // generic — not object-safe
}

// Box<dyn Bad> — compile error: not object-safe

Object-safe traits work as dyn Trait; non-object-safe traits can only be used with generics.

When to use which

Generics (T: Trait):

  • Performance matters — direct calls inline
  • Heterogeneous types not needed
  • Code size acceptable (one copy per type)

Trait objects (dyn Trait):

  • Need to store mixed concrete types in one collection
  • Need to choose implementation at runtime (plugin systems)
  • Performance penalty acceptable (vtable indirection)
  • Want smaller binaries (one definition, not N specializations)

Most Rust code uses generics. Trait objects appear in real code mainly for plugin/strategy patterns.

impl Trait — between the two

impl Trait in return position is generic-like with extra ergonomics:

fn make_iter() -> impl Iterator<Item = i32> {
    (1..10).filter(|x| x % 2 == 0)
}

The caller sees "some iterator over i32" — the concrete type is hidden but fixed. No vtable cost. Different from Box<dyn Iterator<Item = i32>> which has dynamic dispatch.

Common mistakes

  • Reaching for trait objects when generics work — pays the vtable cost for no benefit.
  • Confusing dyn Trait with impl Trait — impl is static-typed (one type), dyn is dynamic (any type implementing the trait).
  • Forgetting Box for owned trait objectsVec<dyn Shape> is unsized; needs Vec<Box<dyn Shape>>.
  • Trying to use generic methods through dyn — not object-safe. Either drop the generic or move the method out of the trait.
  • Cloning dyn Trait — Clone returns Self, which isn't object-safe. Use a workaround like dyn-clone crate or DynClone.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…