Nugget 8

Home

Storing Different Types in One Collection

You want a Vec that holds both Dogs and Cats. With generics, every element must be the same type. Trait objects change that.

The Problem

A Vec<Dog> can only hold Dogs. A Vec<Cat> can only hold Cats. What if you need both?

trait Speak {
    fn make_sound(&self) -> String;
}

struct Dog { name: String }
struct Cat { name: String }

impl Speak for Dog {
    fn make_sound(&self) -> String {
        format!("{} says Woof!", self.name)
    }
}

impl Speak for Cat {
    fn make_sound(&self) -> String {
        format!("{} says Meow!", self.name)
    }
}

// This won't compile:
let animals: Vec<???> = vec![
    Dog { name: "Rex".into() },
    Cat { name: "Whiskers".into() },  // ❌ expected Dog, found Cat
];

Generic collections (Vec<T>) require a single concrete type. But you want heterogeneous items that share a trait.

The Solution: Box<dyn Speak>

Wrap each value in Box and type the collection as Vec<Box<dyn Speak>>:

let animals: Vec<Box<dyn Speak>> = vec![
    Box::new(Dog { name: "Rex".into() }),
    Box::new(Cat { name: "Whiskers".into() }),
];

// All animals share the Speak interface
for animal in &animals {
    println!("{}", animal.make_sound());
}
// Rex says Woof!
// Whiskers says Meow!

This is dynamic dispatch — at runtime, each make_sound() call looks up which type's implementation to run.

How It Works: The Vtable

A trait object (dyn Speak) stores two pointers: one to the data, and one to a vtable — a table of function pointers for each trait method:

// What Box<dyn Speak> stores internally:
//
// ┌──────────────┐    ┌─────────────────────┐
// │ data pointer  │───→│ Dog { "Rex" }       │
// ├──────────────┤    └─────────────────────┘
// │ vtable ptr   │───→┌─────────────────────┐
// └──────────────┘    │ make_sound → Dog::… │
//                      │ drop → Dog::drop   │
//                      └─────────────────────┘

// Every make_sound() call goes through the vtable:
// That's one pointer indirection at runtime.

📐 Static vs dynamic dispatch

Generic functions (Nugget 6) use static dispatch — the compiler creates a separate function for each type. Trait objects use dynamic dispatch — one function, looked up at runtime. Static dispatch is faster; dynamic dispatch is more flexible.

Trait Object Safety

Not every trait can be made into a trait object. The compiler enforces object safety rules:

// ✅ Object-safe — no Self return type
trait Speak { fn make_sound(&self) -> String; }

// ❌ Not object-safe — returns Self
trait Clone { fn clone(&self) -> Self; }

// ❌ Not object-safe — generic method
trait IntoIterator {
    fn into_iter(self) -> IntoIter<Self::Item>;
}

// The error:
// let v: Vec<Box<dyn Clone>> = vec![];
// error: the trait `Clone` cannot be made into an object

💡 Rule of thumb

If a trait's methods take self by value or return Self, or have generic type parameters, it can't be a trait object. Most "interface-like" traits (that take &self) are safe.

Real-World Usage

Trait objects are everywhere in Rust. Here are two common patterns:

// GUI components
let widgets: Vec<Box<dyn Widget>> = vec![
    Box::new(Button::new("Click me")),
    Box::new(TextInput::new("Type here")),
    Box::new(Checkbox::new(true)),
];

for widget in &widgets {
    widget.render();  // each one renders differently
}
// Error handling (Box<dyn Error>)
use std::error::Error;

fn do_stuff() -> Result<(), Box<dyn Error>> {
    let _ = std::fs::read_to_string("file.txt")?;  // io::Error
    let _ = "hello".parse::<i32>()?;               // ParseIntError
    Ok(())
}
// Box<dyn Error> wraps any error type — the most flexible return type

Quick Comparison

Generics (static) Trait Objects (dynamic)
Syntax <T: Speak> Box<dyn Speak>
Dispatch At compile time At runtime (vtable lookup)
Performance Zero-cost (inlined) Small indirection cost
Collection types Homogeneous only Heterogeneous
Return types Concrete type per call Single type erased
When to use Most of the time When types vary at runtime

Key Takeaways