🎓 A1Learner
🔍

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?