Trait Objects
Static vs Dynamic Dispatch
Section titled “Static vs Dynamic Dispatch”Rust gives you two ways to use trait polymorphism:
- Static dispatch (via generics): the compiler knows the exact type at compile time and generates specialized code for each one. Zero runtime overhead.
- Dynamic dispatch (via
dyn Trait): the concrete type is not known until runtime. The compiler stores a vtable — a table of function pointers — alongside the value so the right method can be found at runtime.
The dyn Trait Syntax
Section titled “The dyn Trait Syntax”When you write dyn Animal, you are asking Rust to use dynamic dispatch. Because the size of the concrete type is unknown at compile time, you must put a dyn Trait behind a pointer. The three common forms are:
&dyn Trait— a borrowed trait object (no heap allocation)Box<dyn Trait>— an owned trait object stored on the heapRc<dyn Trait>— a shared, reference-counted trait object (multiple owners, single-threaded)
trait Animal { fn sound(&self) -> &str; fn name(&self) -> &str;}
fn make_sound(animal: &dyn Animal) { println!("{} says: {}", animal.name(), animal.sound());}This function accepts any type that implements Animal — you do not need to know the concrete type at compile time.
What the Compiler Says: dyn Trait Without a Pointer
Section titled “What the Compiler Says: dyn Trait Without a Pointer”dyn Trait on its own is not a type you can hold by value — its size depends on which concrete type is behind it, and that is not known until runtime:
// @expect-error E0277trait Animal { fn sound(&self) -> &str;}
fn make_sound(animal: dyn Animal) { println!("{}", animal.sound());}
fn main() {}error[E0277]: the size for values of type `(dyn Animal + 'static)` cannot be known at compilation time --> src/main.rs:5:23 |5 | fn make_sound(animal: dyn Animal) { | ^^^^^^^^^^ doesn't have a size known at compile-time | = help: the trait `Sized` is not implemented for `(dyn Animal + 'static)`help: you can use `impl Trait` as the argument type |5 - fn make_sound(animal: dyn Animal) {5 + fn make_sound(animal: impl Animal) { |help: function arguments must have a statically known size, borrowed types always have a known size |5 | fn make_sound(animal: &dyn Animal) { | +Every function parameter needs a statically known size by default (every generic parameter has an implicit Sized bound), and dyn Animal fails that — a Dog and a much larger struct implementing Animal are different sizes, so “a dyn Animal” cannot be a fixed-size stack value. A pointer (&dyn Animal, Box<dyn Animal>) always has a fixed size regardless of what it points to, which is exactly why dyn Trait must always sit behind one.
dyn Trait + 'a: The Hidden Lifetime
Section titled “dyn Trait + 'a: The Hidden Lifetime”Every dyn Trait type actually carries a lifetime bound, usually elided: Box<dyn Animal> is really Box<dyn Animal + 'static>, and &'a dyn Animal is really &'a (dyn Animal + 'a). The default is “as long as the container needs it to be” — 'static for an owning Box/Rc (nothing borrowed can outlive the box unless told otherwise), and the pointer’s own lifetime for a borrowed &dyn Trait. You only write + 'a explicitly when the default is wrong for your case, most often when a trait object needs to borrow data that is not 'static:
trait Animal { fn sound(&self) -> &str;}
struct Dog<'a> { tag: &'a str,}
impl<'a> Animal for Dog<'a> { fn sound(&self) -> &str { self.tag }}
fn make_animal<'a>(tag: &'a str) -> Box<dyn Animal + 'a> { Box::new(Dog { tag }) // the trait object borrows `tag`, so it cannot be `+ 'static`}
fn main() { let tag = String::from("Woof"); let animal = make_animal(&tag); println!("{}", animal.sound());}Storing Mixed Types in a Collection
Section titled “Storing Mixed Types in a Collection”The most powerful use case for trait objects is storing values of different concrete types together in a Vec or other collection. With generics alone this is impossible — a Vec<T> can only hold one concrete type at a time.
trait Animal { fn sound(&self) -> &str; fn name(&self) -> &str;}struct Dog;struct Cat;impl Animal for Dog { fn sound(&self) -> &str { "Woof" } fn name(&self) -> &str { "Dog" }}impl Animal for Cat { fn sound(&self) -> &str { "Meow" } fn name(&self) -> &str { "Cat" }}
let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog), Box::new(Cat), Box::new(Dog),];Each element is a Box<dyn Animal> — an owned pointer to some type that implements Animal. The vtable stored alongside each value ensures the right sound() and name() implementations are called.
&dyn Trait vs Box<dyn Trait> vs Rc<dyn Trait>
Section titled “&dyn Trait vs Box<dyn Trait> vs Rc<dyn Trait>”All three are trait objects; they differ only in ownership, exactly like their non-dyn counterparts:
| Form | Ownership | When to use |
|---|---|---|
&dyn Trait | Borrowed, no allocation | A function only needs to call methods, doesn’t need to keep the value |
Box<dyn Trait> | Owned, single owner, heap-allocated | Storing one trait object that this scope owns (a Vec<Box<dyn Trait>>, a struct field) |
Rc<dyn Trait> | Shared, reference-counted, heap-allocated | Multiple owners need the same trait object, single-threaded (Arc<dyn Trait> is the multi-threaded equivalent) |
impl Trait vs dyn Trait
Section titled “impl Trait vs dyn Trait”Both &impl Animal and &dyn Animal accept “some type that implements Animal” as a function parameter, but they compile very differently:
trait Animal { fn sound(&self) -> &str;}struct Dog;impl Animal for Dog { fn sound(&self) -> &str { "Woof" } }
// Static dispatch -- compiler generates specialized code per concrete typefn notify_static(item: &impl Animal) { println!("{}", item.sound());}
// Dynamic dispatch -- resolved at runtime via vtablefn notify_dyn(item: &dyn Animal) { println!("{}", item.sound());}
fn main() { notify_static(&Dog); notify_dyn(&Dog);}&impl Animal is shorthand for a generic type parameter (fn notify_static<T: Animal>(item: &T)) — the compiler monomorphizes it, just like the generics you saw in the previous lesson. &dyn Animal is a trait object — one function, dispatched through a vtable at runtime. Use impl Trait when you know the type at compile time and want maximum performance; use dyn Trait when you need runtime flexibility, such as storing mixed types in a Vec or returning different types from different branches.
Measuring the Fat Pointer
Section titled “Measuring the Fat Pointer”A dyn Trait reference is a fat pointer: it carries a data pointer and a vtable pointer, so it is twice the size of an ordinary reference. This is not a claim you have to take on faith:
trait Animal { fn sound(&self) -> &str;}
struct Dog;impl Animal for Dog { fn sound(&self) -> &str { "Woof" }}
fn main() { let d = Dog; let r: &Dog = &d; let dyn_r: &dyn Animal = &d; println!("&Dog: {} bytes", std::mem::size_of_val(&r)); println!("&dyn Animal: {} bytes", std::mem::size_of_val(&dyn_r));}&Dog: 8 bytes&dyn Animal: 16 bytes&Dog is a single machine word (8 bytes on a 64-bit target) — just an address. &dyn Animal is two words: the same data address, plus a pointer to Dog’s Animal vtable. Box<dyn Animal> and Rc<dyn Animal> are fat in exactly the same way, for the same reason.
flowchart LR
subgraph FatPointer["&dyn Animal (16 bytes)"]
DataPtr["data pointer"]
VtablePtr["vtable pointer"]
end
DataPtr --> DogData["Dog value on the stack or heap"]
VtablePtr --> Vtable["Dog's Animal vtable"]
Vtable --> SoundFn["fn sound(&self) -> &str"]
Vtable --> DropFn["fn drop_in_place"] Trait Objects and Dyn Compatibility
Section titled “Trait Objects and Dyn Compatibility”Not every trait can be used as a dyn Trait. A trait is dyn compatible if, roughly:
- It has no methods that return
Self. - It has no generic type parameters on its methods.
(Older material — including some of Rust’s own history — calls this object safety; current rustc uses the term dyn compatible / dyn compatibility in its diagnostics, so that’s what this lesson uses.)
The standard library traits Display, Debug, and Iterator are dyn compatible. Traits like Clone (whose clone method returns Self) are not — trying to use one as &dyn Trait is a compile error:
// @expect-error E0038trait Animal { fn sound(&self) -> &str; fn clone_animal(&self) -> Self; // returns Self -- not dyn compatible}
struct Dog;
impl Animal for Dog { fn sound(&self) -> &str { "Woof" } fn clone_animal(&self) -> Self { Dog }}
fn make_sound(animal: &dyn Animal) { println!("{}", animal.sound());}
fn main() { make_sound(&Dog);}error[E0038]: the trait `Animal` is not dyn compatible --> src/main.rs:14:24 |14 | fn make_sound(animal: &dyn Animal) { | ^^^^^^^^^^ `Animal` is not dyn compatible |note: for a trait to be dyn compatible it needs to allow building a vtable for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility> --> src/main.rs:4:31 | 2 | trait Animal { | ------ this trait is not dyn compatible... 3 | fn sound(&self) -> &str; 4 | fn clone_animal(&self) -> Self; // returns Self -- not dyn compatible | ^^^^ ...because method `clone_animal` references the `Self` type in its return type = help: consider moving `clone_animal` to another trait = help: only type `Dog` implements `Animal`; consider using it directly instead.Note the exact wording: rustc says “is not dyn compatible” — it never says “object safety” or “object-safe” anywhere in the diagnostic.
The where Self: Sized Escape Hatch
Section titled “The where Self: Sized Escape Hatch”You don’t have to give up on the whole trait just because one method is not dyn compatible. Adding a where Self: Sized bound to that one method removes it from the vtable requirement — the method is simply unavailable through dyn Trait, but the trait itself becomes dyn compatible again, and the method still works when called on a concrete, statically-known type:
trait Animal { fn sound(&self) -> &str; fn clone_animal(&self) -> Self where Self: Sized; // excluded from the vtable -- only callable when Self is statically known}
struct Dog;
impl Animal for Dog { fn sound(&self) -> &str { "Woof" } fn clone_animal(&self) -> Self { Dog }}
fn make_sound(animal: &dyn Animal) { println!("{}", animal.sound());}
fn main() { make_sound(&Dog); // fine: dyn dispatch only needs sound() let d2 = Dog.clone_animal(); // fine: called on the concrete type, not through dyn make_sound(&d2);}dyn Any and Downcasting
Section titled “dyn Any and Downcasting”std::any::Any is a special trait every 'static type implements automatically. It lets you type-erase a value completely, then ask at runtime “is this actually a Dog?” via downcast_ref::<T>():
use std::any::Any;
struct Dog;struct Cat;
fn describe(animal: &dyn Any) { if let Some(_d) = animal.downcast_ref::<Dog>() { println!("it's a dog"); } else if let Some(_c) = animal.downcast_ref::<Cat>() { println!("it's a cat"); } else { println!("unknown animal"); }}
fn main() { describe(&Dog); describe(&Cat);}it's a dogit's a catThis has a legitimate use — genuinely type-erased systems like a plugin registry or a heterogeneous event bus, where the set of possible types is not fixed by this code at all. It is a smell the moment the full set of types is known and fixed, as it is here: Dog and Cat are two hard-coded structs in this same file, and the if/else if chain of downcasts is doing, by hand and without exhaustiveness checking, exactly what a match on an enum Animal { Dog, Cat } would do automatically and safely.
Performance Framing
Section titled “Performance Framing”A dyn Trait call costs one extra pointer indirection to look up the function in the vtable, plus that the call usually cannot be inlined (the compiler does not know at compile time which function it will jump to). For most code this is genuinely negligible — comparable to one extra memory load — but it is not literally free the way a monomorphized generic call is. #[inline] on a concrete method has no effect on a call made through dyn Trait, since inlining requires knowing the exact function at compile time; it only helps the direct, statically-dispatched call path.
When to Use Each
Section titled “When to Use Each”| Approach | Type known at compile time? | Runtime cost | Heterogeneous collection? |
|---|---|---|---|
Generic (impl Trait or T: Trait) | Yes | Zero | No |
Trait object (dyn Trait) | No | Small vtable indirection | Yes |
Use generics when you know the types at compile time and want maximum performance. Use trait objects when you need to work with a collection of mixed concrete types or when the type is determined by user input or configuration at runtime.
trait Animal { fn sound(&self) -> &str; fn name(&self) -> &str;}
struct Dog;struct Cat;
impl Animal for Dog { fn sound(&self) -> &str { "Woof" } fn name(&self) -> &str { "Dog" }}
impl Animal for Cat { fn sound(&self) -> &str { "Meow" } fn name(&self) -> &str { "Cat" }}
fn make_sound(animal: &dyn Animal) { println!("{} says: {}", animal.name(), animal.sound());}
fn main() { let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog), Box::new(Cat), Box::new(Dog), ];
for animal in &animals { make_sound(animal.as_ref()); }}Compiling…
Mental Model
Section titled “Mental Model”A trait object is a value that has deliberately forgotten its concrete type, in exchange for a fixed, uniform shape: two words (data pointer, vtable pointer) no matter what is actually behind it. Every method call through it is a lookup — “go to this vtable, find this method’s slot, jump there” — instead of a direct call the compiler resolved ahead of time. That lookup is the entire runtime cost, and it is also the entire reason a Vec<Box<dyn Animal>> can hold a Dog and a Cat side by side when a Vec<Dog> fundamentally cannot.
Trade-offs
Section titled “Trade-offs”dyn Trait buys genuine flexibility — heterogeneous collections, plugin-style extensibility, one compiled copy of code regardless of how many types eventually implement the trait — for a small, real cost: a vtable indirection per call, no inlining across that call, and a size and dyn-compatibility restriction that generics never have to think about. dyn Any pushes this further, trading away the type system’s exhaustiveness checking entirely in return for genuine type erasure — worth it for a plugin system, a real liability when the types were known all along.
Common Misconceptions
Section titled “Common Misconceptions”- “
dyn Traitandimpl Traitare basically the same syntax sugar for the same thing.” They compile to opposite strategies:impl Traitis resolved per call site at compile time (monomorphization);dyn Traitis resolved once at runtime, through one shared vtable-dispatching function. - “A
dyn Traitvalue can be passed by value like any other type.” It cannot —dyn Traitalone has no compile-time-known size (error[E0277], shown above), so it always needs a pointer around it. - “
dyn Anyand downcasting are the ‘real’ Rust way to check a value’s type at runtime.” They are a deliberate escape hatch for genuine type erasure, not a general substitute for pattern matching on an enum — reach for an enum first whenever the full set of types is actually known. - “Object safety and dyn compatibility are two different concepts.” They are the same requirement under two names — “object safety” is the older term; current rustc diagnostics only ever say “is not dyn compatible.”
Reading AI-generated code
Section titled “Reading AI-generated code”Generated code sometimes reaches for dyn Any plus a chain of downcast_ref calls to distinguish between a handful of hardcoded types, instead of using the enum that was available the whole time.
🐛 Spot the bug
use std::any::Any;
struct Cat;struct Dog;
fn feed(animal: &dyn Any) { if animal.downcast_ref::<Cat>().is_some() { println!("give it fish"); } else if animal.downcast_ref::<Dog>().is_some() { println!("give it kibble"); }}
fn main() { feed(&Cat); feed(&Dog);}Show the answer
This compiles and runs correctly — give it fish then give it kibble — which is exactly why it survives review. The problem is what it gives up: dyn Any type-erases everything, then downcast_ref walks it back one hardcoded type at a time with if/else if, exactly the shape of a match the compiler could exhaustiveness-check. Nothing here catches a third animal type added later and forgotten in feed — the if chain just silently does nothing for it. A plain enum Animal { Cat, Dog } with a match in feed behaves identically today and turns “forgot to handle the new variant” into a compile error tomorrow. dyn Any is for genuinely type-erased situations — a plugin system storing arbitrary, unrelated types — not a substitute for an enum whose shape you already know.