Rust On-Stack Dynamic Dispatch
A borrowed trait object, written &dyn Trait, puts different types behind one name without a box. When the candidates already live in your function, the borrowed object points at one of them directly, with nothing allocated and nothing moved.
The boxed way
From Rust Trait Objects: a Box<dyn Speak> owns the chosen value on the heap.
let pet: Box<dyn Speak> = Box::new(Dog);The borrowed way: no box at all
Both animals stay in the function. One &dyn Speak name points at whichever was picked.
let dog = Dog;
let cat = Cat;
let pick_dog = true;
let chosen: &dyn Speak = if pick_dog { &dog } else { &cat };
println!("{}", chosen.speak());
println!("{}", dog.speak());The annotation does the erasing
Leave the type off and the compiler refuses (error E0308): the two arms have different types, Dog and Cat. Naming &dyn Speak is the instruction to erase both into one trait object.
let chosen = if pick_dog { &dog } else { &cat };Example
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String {
String::from("woof")
}
}
impl Speak for Cat {
fn speak(&self) -> String {
String::from("meow")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
let raining = true;
let pet: &dyn Speak = if raining { &cat } else { &dog };
println!("the pet says {}", pet.speak());
println!("dog still says {}", dog.speak());
}Key points
- A trait object does not need a Box.
- &dyn Trait and &mut dyn Trait borrow a value.
- The value stays exactly where it was.
- Use on-stack dispatch when the candidates are created in the same function and one is picked at run time
- Write the type down: let x: &dyn Trait = if ...
- The annotation does the work.
- It lets two different types become one borrowed trait object.
- Nothing is allocated and nothing is moved: when the borrow ends, both original values are still fully yours
- Use it when the value must be stored in a struct.
- Use it when the value must be returned or outlive the function.
- Borrowing only works while the owner is in scope.
In this example, Dog and Cat are two different types that both implement Speak. The binding pet is declared as &dyn Speak. So the if can hand it &cat on one side and &dog on the other. Here raining is true. So pet borrows the cat and prints meow. Nothing was boxed or moved: the last line calls dog.speak() on the original dog, which is still sitting in main, untouched.
Note
Box<dyn Trait> is still the right tool in some cases.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Two local values of different types implement Speak, and a flag picks one at run time — with no heap allocation. Which binding works?