16.9Trait objects: dyn Trait

Last updated June 13, 2026

Generics with trait bounds (lesson 16.3) are resolved at compile time: the compiler knows each concrete type and monomorphizes a copy per type (lesson 15.5). That's perfect when the types are known at compile time, but it can't express "a list holding a mix of different types that all share a trait." For that, Rust has trait objects, written dyn Trait, which choose the method at runtime. This is Rust's form of the runtime polymorphism that C++ gets from virtual functions, and it's one lesson here.

The problem generics can't solve

Say you're building a drawing app with several shapes, each implementing a Draw trait, and you want a list of shapes to draw in order. The shapes are different types. A generic Vec<T> won't do: a Vec<T: Draw> is a vector where every element is the same T, so it could hold all circles or all squares, but not a mix. You want a vector that holds "any Draw-implementing thing, of possibly different types, together."

trait Draw {
    fn draw(&self) -> String;
}

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

impl Draw for Circle {
    fn draw(&self) -> String { format!("circle r={}", self.radius) }
}
impl Draw for Square {
    fn draw(&self) -> String { format!("square s={}", self.side) }
}

Box holds a mix

A trait object is the answer. Box<dyn Draw> means "a heap pointer to some value that implements Draw, exact type not known until runtime" (Box is a heap pointer, chapter 21; for now read Box<dyn Draw> as "some Draw-thing"). A Vec<Box<dyn Draw>> can hold circles and squares side by side:

fn main() {
    let shapes: Vec<Box<dyn Draw>> = vec![
        Box::new(Circle { radius: 2.0 }),
        Box::new(Square { side: 3.0 }),
        Box::new(Circle { radius: 1.0 }),
    ];

    for shape in &shapes {
        println!("{}", shape.draw());
    }
}
circle r=2
square s=3
circle r=1

The vector mixes Circle and Square, because each is boxed as a dyn Draw that erases the specific type and keeps only "this implements Draw." When the loop calls shape.draw(), the program looks up at runtime which draw to call based on the actual type behind each box. This is dynamic dispatch: the method to call is decided while the program runs, not while it compiles.

Key insight

The dyn keyword marks the line between Rust's two kinds of polymorphism. Generics (<T: Trait>) are static dispatch: the compiler knows the type and bakes in the exact method call, with a specialized copy per type (fast, larger binary). Trait objects (dyn Trait) are dynamic dispatch: one shared copy of the code, and the method is looked up at runtime (slightly slower per call, smaller binary, and able to hold mixed types). Reach for generics by default; reach for trait objects when you genuinely need a collection of mixed types behind a shared trait, or to choose a type at runtime.

Static versus dynamic dispatch, the tradeoff

This is the other side of the monomorphization coin from lesson 15.5. With generics, the compiler stamps out a copy of the code for each type, so calls are direct and as fast as hand-written code, at the cost of binary size and the inability to mix types. With trait objects, there's one copy of the code, and each call goes through a small runtime lookup (a vtable, see the box) to find the right method. The per-call cost is tiny and rarely matters, but it's not zero, which is why generics are the default and trait objects are the tool for when you specifically need their flexibility: heterogeneous collections, or returning different types from different branches (the limitation lesson 16.4 ran into, which Box<dyn Trait> solves).

For advanced readers

Dynamic dispatch works through a vtable (virtual method table): a small table of function pointers, one per trait method, that each trait object carries alongside its data pointer. shape.draw() follows the data pointer to the value and the vtable pointer to the right draw. This is the same mechanism as C++ virtual functions, just opt-in (you write dyn) rather than triggered by a keyword on the class. There's also a rule called object safety: only traits whose methods can work through a vtable can become trait objects (roughly, methods must take self by reference and not be generic). You'll meet it as an error message ("the trait cannot be made into an object") rather than needing to memorize the rules; the compiler tells you when a trait isn't object-safe and usually why.

&dyn Trait too

You don't always need Box. A function can take &dyn Trait, a reference to a trait object, when it just needs to call trait methods on something without owning it:

fn describe(item: &dyn Draw) {
    println!("drawing: {}", item.draw());
}

describe accepts a reference to any Draw implementer, using dynamic dispatch. This is the borrowing version (lesson 9.1) and avoids the heap allocation Box implies. Use &dyn Trait for "borrow some trait-implementing thing for this call," and Box<dyn Trait> when you need to own and store trait objects, as in the mixed Vec.

Quiz time

Question #1

Why can't a Vec<T: Draw> hold both a Circle and a Square, and what can?

Show solution

A generic Vec<T> requires every element to be the same type T, so it's all circles or all squares, not a mix. A Vec<Box<dyn Draw>> (a vector of trait objects) can hold both, because each element is "some Draw-implementing thing" with its concrete type erased, allowing different types together.

Question #2

What's the difference between static dispatch (generics) and dynamic dispatch (dyn Trait)?

Show solution

Static dispatch (generics with trait bounds) resolves the exact method at compile time and monomorphizes a specialized copy per type: fast calls, larger binary, types fixed at compile time. Dynamic dispatch (dyn Trait) keeps one copy of the code and looks up the method at runtime via a vtable: a tiny per-call cost, smaller binary, and the ability to hold mixed types or choose types at runtime. Generics are the default; trait objects are for when you need their flexibility.

Question #3

When would you use &dyn Trait versus Box<dyn Trait>?

Show solution

&dyn Trait borrows a trait-implementing value for the duration of a call (no ownership, no heap allocation), good for a function that just needs to call trait methods on something. Box<dyn Trait> owns the value on the heap, needed when you must store trait objects, such as in a Vec<Box<dyn Draw>> that keeps a mixed collection alive.

Trait objects are how Rust does runtime polymorphism without inheritance. That raises the question C++ veterans will be asking through this whole chapter: if there's no inheritance, how do you reuse code and model "is-a" relationships? The last lesson answers it directly.