Skip to content

Trait Objects

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.

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 heap
  • Rc<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 E0277
trait 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.

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());
}

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:

FormOwnershipWhen to use
&dyn TraitBorrowed, no allocationA function only needs to call methods, doesn’t need to keep the value
Box<dyn Trait>Owned, single owner, heap-allocatedStoring one trait object that this scope owns (a Vec<Box<dyn Trait>>, a struct field)
Rc<dyn Trait>Shared, reference-counted, heap-allocatedMultiple owners need the same trait object, single-threaded (Arc<dyn Trait> is the multi-threaded equivalent)

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 type
fn notify_static(item: &impl Animal) {
println!("{}", item.sound());
}
// Dynamic dispatch -- resolved at runtime via vtable
fn 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.

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"]
What a trait object's fat pointer actually points to

Not every trait can be used as a dyn Trait. A trait is dyn compatible if, roughly:

  1. It has no methods that return Self.
  2. 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 E0038
trait 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.

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);
}

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 dog
it's a cat

This 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.

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.

ApproachType known at compile time?Runtime costHeterogeneous collection?
Generic (impl Trait or T: Trait)YesZeroNo
Trait object (dyn Trait)NoSmall vtable indirectionYes

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());
}
}

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.

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.

  • “dyn Trait and impl Trait are basically the same syntax sugar for the same thing.” They compile to opposite strategies: impl Trait is resolved per call site at compile time (monomorphization); dyn Trait is resolved once at runtime, through one shared vtable-dispatching function.
  • “A dyn Trait value can be passed by value like any other type.” It cannot — dyn Trait alone has no compile-time-known size (error[E0277], shown above), so it always needs a pointer around it.
  • “dyn Any and 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.”

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.

What does dyn Trait mean in Rust?
What is a vtable?
Why must dyn Trait be placed behind a pointer such as Box<dyn Trait> or &dyn Trait?
Which approach allows storing values of different concrete types together in a Vec?
What do these two lines print?
trait Animal { fn sound(&self) -> &str; }
struct Dog;
impl Animal for Dog { fn sound(&self) -> &str { "Woof" } }
fn main() {
    let d = Dog;
    println!("{}", std::mem::size_of_val(&(&d)));
    let dyn_r: &dyn Animal = &d;
    println!("{}", std::mem::size_of_val(&dyn_r));
}
Why does this fail to compile?
trait Animal { fn sound(&self) -> &str; }
fn make_sound(animal: dyn Animal) {
    println!("{}", animal.sound());
}
Why is a hand-written if/else if chain of downcast_ref calls on dyn Any a smell when the full set of types (like Cat and Dog) is already known?