Alright, let’s talk about getting dynamic. We’ve been dealing with generics and monomorphization, where the compiler stamps out specific, concrete types for you at compile time. It’s fast, it’s efficient, it’s wonderful. But sometimes, you don’t know what type you’re going to get until the program is actually running. You need a single, uniform interface that can handle multiple different concrete types. This is where trait objects come in, and they are simultaneously one of Rust’s most powerful features and a delightful source of head-scratching.

A trait object is Rust’s way of saying, “I don’t care about the specific type; I only care that it implements a certain set of behaviors.” It’s a pointer to some value and a pointer to a vtable (a virtual method table) that knows how to call the correct implementation of the trait methods for that specific type. This is the core of dynamic dispatch in Rust—figuring out which method to call at runtime.

You’ll primarily see them in two flavors: &dyn Trait and Box<dyn Trait>. The dyn keyword is our clue that we’re in dynamic dispatch territory.

The Two Flavors: Borrowed and Owned

The &dyn Trait is a borrowed trait object. You use this when you have something that implements your trait and you want to pass around a reference to it without caring about its concrete type.

trait Draw {
    fn draw(&self);
}

struct Circle;
struct Square;

impl Draw for Circle {
    fn draw(&self) {
        println!("Drawing a circle");
    }
}

impl Draw for Square {
    fn draw(&self) {
        println!("Drawing a square");
    }
}

// This function accepts any reference to a type that implements Draw.
fn draw_twice(item: &dyn Draw) {
    item.draw();
    item.draw(); // Because it's a reference, we can call it multiple times.
}

fn main() {
    let circle = Circle;
    let square = Square;

    draw_twice(&circle);
    draw_twice(&square);
}

The Box<dyn Trait>, on the other hand, is an owned trait object. This is your go-to when you need to store heterogeneous types that implement the same trait in a single collection, like a Vec. You’re moving the value into the box, and the box now owns it and manages its memory on the heap.

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

    for shape in shapes {
        shape.draw(); // Dynamic dispatch in action: the right method is called for each type.
    }
}

Object Safety: The Party Rules

Here’s the part the manual often undersells: not all traits can be turned into trait objects. A trait must be object-safe. The rules are a bit quirky but exist for soundness reasons. The main one is that all methods must be dispatchable, meaning:

  1. The return type cannot be Self. If a method returns Self, it’s promising to give you back a concrete type. A trait object has erased its concrete type, so it can’t possibly fulfill this promise. The compiler will stop you.
  2. There must be no generic type parameters in the methods. Generics are monomorphized at compile time (static dispatch). A trait object’s vtable is built at runtime and can’t possibly account for every conceivable generic type that could be passed in. It’s a total non-starter.

So, this trait is object-safe:

trait Safe {
    fn do_something(&self);
    fn do_something_else(&self, x: i32) -> String;
}

But this one is a mess and will get kicked out of the trait object club:

trait NotSafe {
    fn clone_me(&self) -> Self; // Nope, returns Self.
    fn process<T>(&self, value: T); // Nope, generic method.
}

If you need a trait object but also need functionality like cloning, the standard workaround is to use the Clone trait itself, which is designed to work behind a Box<dyn Clone>.

The Cost of Being Dynamic

Let’s be direct: you pay for this flexibility. Dynamic dispatch isn’t free.

  • Pointer Indirection: Every method call requires two pointer jumps: one to the object and one to the vtable entry for the method. This inhibits inline caching and optimization that the compiler can do with static dispatch.
  • No Inlining: The compiler can’t inline method calls across trait object boundaries because it doesn’t know the concrete type at compile time. This can sometimes be a performance killer.

The rule of thumb is simple: use generics and static dispatch by default for maximum speed. Reach for trait objects when you genuinely need runtime heterogeneity and are willing to trade a bit of performance for that capability. It’s a conscious design choice, not a default.

The Size Conundrum and Sized

This is the part that confuses everyone, so pay attention. A trait object, like dyn Draw, is a dynamically sized type (DST). Its size isn’t known at compile time because it could be pointing to a Circle, a Square, or any other type of any size that implements Draw.

Rust has a rule: variables on the stack must have a known size. This is why you can never have a bare dyn Trait. You must always put it behind a pointer of a known size, like a reference (&dyn Trait), a box (Box<dyn Trait>), or another smart pointer. These pointers are fat pointers—they’re twice as wide as a normal pointer because they contain both the data pointer and the vtable pointer.

This also explains the sometimes-seen impl Trait vs dyn Trait confusion. impl Trait is static dispatch and is resolved to a concrete, known-size type at compile time. dyn Trait is its dynamic, unknown-size cousin that requires a pointer chaperone. They solve similar problems but in fundamentally different ways with different trade-offs. Choose wisely.