Trait Objects
A universal remote has one button labeled POWER and a list of devices it can talk to — a TV, a soundbar, a ceiling fan, a lamp. The remote does not know how any of those devices turn on. The TV uses an infrared code. The soundbar uses Bluetooth. The fan flips a relay. The lamp pulls a current. What the remote knows is that every device on its list ships with a tiny lookup table glued to its case, and one of the entries in that table is the POWER routine. Press the button, the remote reads the address of the device, jumps to the lookup table, and runs whatever is at the POWER slot. The remote stays one button. The devices stay different.
A trait object in Rust is the remote's list. The button is a trait method. The lookup table glued to each device is a vtable.

The pattern is older than Rust by 40 years. In 1980 a Norwegian named Bjarne Stroustrup was working at Bell Labs and trying to add classes to C without giving up the speed of C. He landed on virtual functions — methods that the caller invokes by name but that the compiler resolves at runtime by reading a hidden table of function pointers stored inside the object. C++ shipped this in 1983 and called the table a vtable, short for virtual table. Java built its entire method dispatch on the same trick a decade later. Smalltalk and Objective-C did it before either, with a different shape, but the idea is the same: when one piece of code needs to call a method on something whose type is not known at compile time, you store the function addresses next to the data and look them up at the call site. Rust took the trick, gave it a new name — dyn Trait — and made you opt into it explicitly so you can see exactly where you are paying for it.
Look at the trait. It is the button label.
trait Draw {
fn draw(&self) -> String;
}Draw is a contract with one method, draw, that takes a shared reference to whatever the type is and returns a String. Any device that wants to be on the remote must implement this method. That is the whole rule for joining the list.
Three devices follow. Each one is a different struct with different fields, and each one writes its own version of draw.
struct Circle {
radius: f64,
}
struct Square {
side: f64,
}
struct Triangle {
base: f64,
height: f64,
}
impl Draw for Circle {
fn draw(&self) -> String {
format!("circle r={:.1} area={:.2}", self.radius, std::f64::consts::PI * self.radius * self.radius)
}
}
impl Draw for Square {
fn draw(&self) -> String {
format!("square s={:.1} area={:.2}", self.side, self.side * self.side)
}
}
impl Draw for Triangle {
fn draw(&self) -> String {
format!("triangle b={:.1} h={:.1} area={:.2}", self.base, self.height, 0.5 * self.base * self.height)
}
}A Circle carries a radius. A Square carries a side length. A Triangle carries a base and a height. The numbers are different. The internal memory layout is different. A Circle is 8 bytes (one f64), a Square is 8 bytes (one f64), a Triangle is 16 bytes (two f64s). At compile time these are unrelated types — the compiler would refuse a Vec<Circle> that also held squares.
The whole point of trait objects is to dissolve that refusal. The remote needs to hold all of them in one list and call draw on each without caring which is which.
fn build_scene() -> Vec<Box<dyn Draw>> {
let mut scene: Vec<Box<dyn Draw>> = Vec::new();
scene.push(Box::new(Circle { radius: 2.0 }));
scene.push(Box::new(Square { side: 3.0 }));
scene.push(Box::new(Triangle { base: 4.0, height: 5.0 }));
scene.push(Box::new(Circle { radius: 1.5 }));
scene
}Read the type carefully — Vec<Box<dyn Draw>>. The Vec is the remote's list. The Box is a single heap allocation that holds one device. The dyn Draw is the type signature that says "I do not know which concrete type lives in this box, but whatever it is, it implements Draw and I am going to call methods through the vtable." Each Box::new(Circle { ... }) allocates space for that circle on the heap, then wraps the address in a fat pointer that knows where the vtable lives. Pushing the box into the vector is how the remote registers another device.
A Box<Circle> is one word — a single pointer to the heap. A Box<dyn Draw> is two words — the data pointer plus a pointer to the vtable. That second word is what makes the trait object work and what makes it cost a little more than a regular box. The reader can see this difference because the program prints both sizes side by side.

fn report_sizes() {
let circle_size = std::mem::size_of::<Circle>();
let square_size = std::mem::size_of::<Square>();
let boxed_dyn_size = std::mem::size_of::<Box<dyn Draw>>();
let plain_box_size = std::mem::size_of::<Box<Circle>>();
println!("size of Circle: {} bytes", circle_size);
println!("size of Square: {} bytes", square_size);
println!("size of Box<Circle>: {} bytes (one pointer)", plain_box_size);
println!("size of Box<dyn Draw>: {} bytes (two pointers: data + vtable)", boxed_dyn_size);
}The sibling concept here is generics. A generic function written as fn render<T: Draw>(thing: &T) lets the compiler bake a separate copy of render for every concrete T you ever pass in — one copy for Circle, one for Square, one for Triangle. The call site jumps straight to the right copy because the compiler knew the type when it compiled. That is static dispatch, and it is fast enough that the call can sometimes be inlined into nothing. The cost is binary size and the rule that every type has to be nailed down at compile time. If you wanted a vector that held both circles and squares, generics cannot help you — Vec<T> is one type per T, so you have to pick.
Trait objects solve the other half. They let you mix concrete types at runtime, store them together, and call methods on them through one uniform handle. You pay one extra pointer hop per call — the lookup through the vtable — and you give up inlining, because the compiler cannot know which function will run until the program is already running. Generics monomorphize. Trait objects dispatch dynamically. Two ways to use the same trait, two different costs.

Time to drive the remote. Walk the list, press POWER on each device.
fn main() {
report_sizes();
println!("---");
let scene = build_scene();
println!("scene holds {} shapes", scene.len());
for (i, shape) in scene.iter().enumerate() {
println!("shape {}: {}", i, shape.draw());
}
}The for loop iterates over scene.iter(), which hands back a &Box<dyn Draw> on each pass. Calling .draw() on it is the moment of dynamic dispatch — Rust reads the vtable pointer, finds the address of the right draw function for whatever concrete type sits in that box, jumps there, and runs it. The output proves it. Four shapes, three different types, one loop.
size of Circle: 8 bytes
size of Square: 8 bytes
size of Box<Circle>: 8 bytes (one pointer)
size of Box<dyn Draw>: 16 bytes (two pointers: data + vtable)
---
scene holds 4 shapes
shape 0: circle r=2.0 area=12.57
shape 1: square s=3.0 area=9.00
shape 2: triangle b=4.0 h=5.0 area=10.00
shape 3: circle r=1.5 area=7.07Look at the size line. A regular Box<Circle> is 8 bytes — just an address. A Box<dyn Draw> is 16 bytes — the address plus the vtable pointer. That second pointer is what carries the type information at runtime, and it is the only reason the loop can stay one line.
Not every trait can be used through dyn. The rule is called object safety, and it rules out methods that return Self (the trait object would not know how big the return value should be) and methods that take generic parameters (the vtable cannot hold an infinite list of monomorphized copies). If you try to write Box<dyn Clone> the compiler will refuse, because Clone::clone returns Self and there is no way to put that in a vtable. Most traits you write yourself will be object safe. When one is not, the compiler tells you why.
Next lesson — what happens when the borrow checker, lifetimes, and these dispatch rules collide inside an async function, and which of Rust's small list of escape hatches lets you ship working code anyway.