The Vec that will not type-check
Here is a problem you have hit in every language with a UI. You have a screen made of widgets: a button, a slider, a label. They are different types with different fields, but they all answer one request, draw. You want to keep them in one list and draw them in a loop. In Go you would put them in a []Drawer of interface values; in Java a List<Drawable>; in TypeScript an array typed by a shared interface. The list holds a mix of concrete types, united only by what they can do.
Reach for the tool from the last two chapters and it does not fit. A generic Vec<T> holds many values of one type T: a Vec<Button> is all buttons, a Vec<Slider> is all sliders. impl Trait does not rescue you either, because, as III.10 noted, a function returning impl Draw must return one single concrete type on every path. Generics are decided at compile time, and at compile time a Vec must commit to exactly one element type. A button and a slider are two types, so they cannot share a generic Vec.
The construct that fits is the trait object, written dyn Draw: a value addressed not by its concrete type but by the trait it satisfies. Put it behind a pointer, here a Box, and Box<dyn Draw> becomes a single type, "an owned pointer to something that can draw". One single type, so a Vec of it is happy, and the different widgets line up inside. Run it:
// The problem: one Vec holding different concrete types that share a trait.
// impl Trait cannot do this, because a Vec needs every element to be ONE type.
trait Draw {
fn draw(&self) -> String;
}
struct Button {
label: String,
}
struct Slider {
value: u8,
}
impl Draw for Button {
fn draw(&self) -> String {
format!("[ {} ]", self.label)
}
}
impl Draw for Slider {
fn draw(&self) -> String {
let filled = self.value as usize / 10;
format!("|{}{}|", "=".repeat(filled), "-".repeat(10 - filled))
}
}
fn main() {
// dyn Draw behind Box is one type, "a pointer to something that draws",
// so a Button and a Slider can sit side by side in the same Vec.
let screen: Vec<Box<dyn Draw>> = vec![
Box::new(Button { label: "OK".to_string() }),
Box::new(Slider { value: 70 }),
Box::new(Button { label: "Cancel".to_string() }),
];
for widget in &screen {
println!("{}", widget.draw());
}
}The Vec<Box<dyn Draw>> holds two buttons and a slider, three different concrete types, and the loop calls draw on each without knowing or caring which is which. The word dyn in front of the trait name is the marker: it says "the concrete type behind this is not known here, decide the method at run time". That single keyword is the whole subject of this chapter, and the rest of it is just unfolding what it costs and what it buys.
A generic T: Draw is a hole the compiler fills with one known type. A trait object dyn Drawis a value whose concrete type is deliberately hidden, kept behind a pointer, and reached only through the trait’s methods. The first is "any one type that draws, picked at compile time". The second is "some type that draws, and I will not say which, picked at run time".
dyn always lives behind a pointer
There is a reason the last section wrapped the trait in a Box instead of writing Vec<dyn Draw> directly. A Button and a Slider are different sizes in memory: one holds a String, the other a single u8. The compiler lays out a Vec by knowing the exact size of one element, and dyn Draw has no fixed size, because the thing behind it could be any of them. A type whose size is not known at compile time cannot sit directly in a variable, a field, or a Vec slot. The fix is indirection: a pointer has a known size (one machine word, or as you will see, two), whatever it points at.
So a trait object always appears behind a pointer. The two you will meet constantly are &dyn Trait, a borrowed trait object, and Box<dyn Trait>, an owned one on the heap. Box is what you reach for when the value must outlive the current scope or be stored in a collection; &dyn is the lighter choice when you only need to borrow something for the length of a call. Here is the borrowed form: one function that accepts anything drawable by reference. Run it:
// A trait object does not need a Box. A plain reference, &dyn Trait, is the
// lightest form: one function that takes any drawable thing by reference.
trait Draw {
fn draw(&self) -> String;
}
struct Button {
label: String,
}
struct Label {
text: String,
}
impl Draw for Button {
fn draw(&self) -> String {
format!("[ {} ]", self.label)
}
}
impl Draw for Label {
fn draw(&self) -> String {
self.text.clone()
}
}
// &dyn Draw means "a reference to some type that is Draw", chosen at run time.
// Compare it to fn render_generic<T: Draw>(item: &T): that one is decided at
// compile time, with one specialized copy per type. This one is a single copy.
fn render(item: &dyn Draw) {
println!("{}", item.draw());
}
fn main() {
let ok = Button { label: "OK".to_string() };
let hint = Label { text: "press any key".to_string() };
// The same render function, two different concrete types, no Box in sight.
render(&ok);
render(&hint);
}render takes &dyn Draw, so it can be handed a &Button on one line and a &Label on the next without a Box or an allocation anywhere. The coercion from &Button to &dyn Draw is automatic: you pass an ordinary reference and the compiler turns it into a trait object at the call. There is one function in the compiled program, and it works for every drawable type because it reaches the method through the trait, not through a known concrete type.
Try to take a bare dyn Draw by value, as in fn take(x: dyn Draw), and the compiler rejects it: the size for values of type `dyn Draw` cannot be known at compilation time (error E0277). A bare dyn Trait is what Rust calls unsized: it has no fixed width, so it needs a pointer to give it one. You will almost always see it as &dyn Trait or Box<dyn Trait>, never naked. The same is true of str versus &str, which you met long ago without knowing it was the same rule.
How the call finds its method
When you call widget.draw() on a trait object, the program does not know at compile time which draw to run, because the concrete type was deliberately hidden. So how does the right one get called? The answer is the single mechanical idea you need to carry out of this chapter, and it is worth seeing plainly. A trait object is not an ordinary pointer; it is a fat pointer, two machine words instead of one. The first word points at the data, the actual Button or Slider sitting in memory. The second word points at a vtable: a small, compiler-built table holding the address of each of that type’s trait methods.
With those two words in hand, widget.draw() is a short recipe. Follow the second pointer to the vtable, read the slot for draw, and that slot holds the address of Button’s draw (or Slider’s, depending on what the first word points at). Jump there, passing the data pointer as self. The lookup is one extra memory read compared to a direct call, and it is the same machinery a Go interface value or a C++ virtual call uses under the hood. You can see the doubling of the pointer for yourself with size_of. Run it:
// A trait object is a "fat pointer": two machine words, not one. The first
// points at the data; the second points at a vtable, a small table of the
// type's method addresses. size_of lets us see the doubling directly.
use std::mem::size_of;
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
impl Speak for Dog {
fn speak(&self) -> String {
"woof".to_string()
}
}
fn main() {
// A thin pointer: one word, just the address of the Dog.
let thin = size_of::<&Dog>();
// A fat pointer: two words, the data address plus the vtable address.
let fat = size_of::<&dyn Speak>();
println!("&Dog is {thin} bytes (one pointer)");
println!("&dyn Speak is {fat} bytes (data + vtable)");
println!("fat is {} pointers wide", fat / thin);
// The call goes: follow the vtable pointer, find speak's address, jump to it.
let d = Dog;
let obj: &dyn Speak = &d;
println!("through the vtable: {}", obj.speak());
}On a 64-bit target a &Dog is 8 bytes, one pointer, and a &dyn Speak is 16 bytes, two. That second word is the vtable pointer, and it is what lets a single &dyn Speak value carry not just where the data is but how to call its methods. The vtable itself is built once, at compile time, one per (type, trait) pair, and every trait object of that type shares it. So the run-time cost is not building the table, only the one indirect read on each call.
An ordinary reference is one arrow pointing at data. A trait object is two arrows side by side: one to the data, one to the vtable of method addresses for that data’s type. A method call follows the second arrow to find the function, then runs it on the first. That second arrow is the whole price and the whole power of dyn.
One detail smooths this in practice and gets its full treatment in the next chapter. When you call a trait method on a Box<dyn Draw> or even a &Box<dyn Draw>, Rust quietly peels the layers down to the fat pointer it needs through a mechanism called deref coercion. That is why &screen in section 01 iterated cleanly and widget.draw() just worked. III.12 is where that coercion stops being magic.
Static dispatch versus dynamic dispatch
You now have two ways to write "this code works for any type that has a given behaviour", and they are genuinely different machines. The generic form, fn f<T: Trait>(x: &T), is static dispatch. As III.09 taught, the compiler monomorphizes it: for each concrete type you actually call it with, it stamps out a separate, specialized copy of the function with the method calls wired directly to that type. The trait-object form, fn f(x: &dyn Trait), is dynamic dispatch: one copy of the function for all types, with each method call routed through the vtable at run time. Put them side by side and run it:
// Two ways to write "take anything that is Greet". They look almost identical
// and behave the same here, but the compiler builds them very differently.
trait Greet {
fn hello(&self) -> String;
}
struct English;
struct Spanish;
impl Greet for English {
fn hello(&self) -> String {
"hello".to_string()
}
}
impl Greet for Spanish {
fn hello(&self) -> String {
"hola".to_string()
}
}
// STATIC dispatch (III.09's monomorphization): the compiler stamps out a fresh,
// specialized copy of greet_static for each concrete T it is called with. The
// call to .hello() is wired straight to the right function, and can be inlined.
fn greet_static<T: Greet>(who: &T) {
println!("static: {}", who.hello());
}
// DYNAMIC dispatch: one copy of greet_dynamic serves every type at once. It
// receives a fat pointer and looks up .hello() through a vtable at run time.
fn greet_dynamic(who: &dyn Greet) {
println!("dynamic: {}", who.hello());
}
fn main() {
let en = English;
let es = Spanish;
// greet_static gets two compiled copies: one for English, one for Spanish.
greet_static(&en);
greet_static(&es);
// greet_dynamic is a single function; the right hello() is found at run time.
greet_dynamic(&en);
greet_dynamic(&es);
}Both print the same thing, but the compiled program differs. greet_static becomes two functions, one specialized for English and one for Spanish, each with its hello call resolved directly and eligible to be inlined (the call replaced by the called body, erasing even the function-call overhead). greet_dynamic stays a single function that reaches hello through the vtable, the same indirection from the last section. Same source-level idea, two different costs.
The trade-offs are real and worth stating without spin. Static dispatch is faster at the call: the target is known, the optimizer can inline and specialize, and there is no vtable hop. The price is code size: every concrete type you instantiate the generic with produces another copy of the machine code, which can bloat the binary and pressure the instruction cache (the phenomenon III.09 called code bloat). Dynamic dispatch inverts both. It is one copy of the code no matter how many types flow through it, kinder to binary size, but each call pays for the vtable lookup and is usually not inlined across it.
There is a third axis that often decides the matter, and it is not about speed at all: flexibility. Static dispatch must know every type at compile time, so the set of types is closed. Dynamic dispatch does not, so the set is open: a Vec<Box<dyn Draw>> can hold a type that did not exist when render was written, even one loaded from a plugin. That is precisely the capability section 01 needed and generics could not give. When you genuinely require a heterogeneous collection or a run-time-chosen type, dynamic dispatch is not the slower option, it is the only option.
Static dispatch (generics): fastest calls, inlinable, but one machine code copy per type and the type set fixed at compile time. Dynamic dispatch (dyn): one copy of the code, a small per-call vtable cost, and the type set open, including types unknown when the code was written. Neither is "better"; they buy different things, and most programs use both.
"Dynamic dispatch is slow" is folklore worth deflating. The vtable hop is a single predictable indirect call, often a handful of cycles, and irrelevant next to any real I/O or allocation. The cost that actually bites is the lost inlining: the optimizer cannot see through the call to specialize around it. For a method doing real work, that is noise. For a tiny method called in a tight inner loop, it can matter, and that is the one case where reaching for generics is a measured decision rather than a reflex. Measure before you assume.
Object safety: which traits can be made dyn
Not every trait can become a dyn trait object, and the rule that decides it has a name: object safety (current stable Rust, including 1.96.0, has renamed this to dyn compatibility in its diagnostics, but the older "object safe" is still the term you will read everywhere). The intuition is simple once you remember the vtable. To make a trait object, the compiler must build a vtable, a table of concrete method addresses. So every method the trait object exposes has to be one the compiler can put a single, fixed address into. Methods that fail that test make the trait impossible to turn into an object.
Two cases cover almost everything you will hit. First, a generic method: fn save<T>(&self, v: T). There is no one address for it, because the compiler would need a separate monomorphized copy per T, and a vtable slot holds exactly one address. Second, a method that mentions Self by value in a way that needs its size, classically a method returning Self, like fn clone(&self) -> Self: through a trait object the concrete Self is hidden, so the caller cannot know how big the returned value is or where to put it. Here is a dyn-compatible trait, and a comment showing the generic method that would sink it. Run it:
// This trait is dyn-compatible because none of its methods are generic and none
// return Self. The generic version that FAILS object safety is in the comment;
// uncomment it to watch the compiler reject the whole trait with error E0038.
trait Store {
// FAILS: a generic method has no single address to put in a vtable, since the
// compiler would need one entry per T it is ever called with.
// fn save<T: std::fmt::Debug>(&self, value: T);
// OK: a concrete signature has exactly one address, so it fits in the vtable.
fn save(&self, value: &str);
fn count(&self) -> usize;
}
struct MemStore {
items: Vec<String>,
}
impl Store for MemStore {
fn save(&self, value: &str) {
println!("saving {value:?}");
}
fn count(&self) -> usize {
self.items.len()
}
}
fn main() {
// Because Store is dyn-compatible, this Box<dyn Store> compiles.
let store: Box<dyn Store> = Box::new(MemStore { items: vec!["a".to_string()] });
store.save("hello");
println!("count is {}", store.count());
}As written, every method on Store has a concrete signature, so each gets a fixed vtable slot and Box<dyn Store> compiles. Uncomment the generic save and the trait stops being dyn-compatible: the whole Box<dyn Store> line fails, not just the offending method. The compiler error is precise about why.
On Rust 1.96.0 that error reads error[E0038]: the trait `Store` is not dyn compatible, and it points at the exact cause: ...because method `save` has generic type parameters. It even suggests the standard fix, "consider moving save to another trait", because a trait can keep its generic, non-object methods as long as the part you want behind dyn is split out clean. The error names the problem in plain language, which is the whole point of the rule existing.
Most traits you write are object safe without effort, because most methods take &self and return ordinary values. The two triggers to recognize are generic methods and Self-by-value returns. When you need both a generic method and a trait object, the cure is the one the compiler suggests: keep the object-safe methods in the trait you make dyn, and move the generic ones elsewhere. Here is a trait that is object safe and works as a real Vec of objects, for contrast. Run it:
// Not every trait can become a dyn trait object. The rule is object safety (the
// compiler now calls it "dyn compatibility"), and this trait passes: every method
// takes &self and names only types known without knowing the concrete Self.
trait Animal {
fn name(&self) -> String;
fn legs(&self) -> u32;
// A default method that calls the others is fine; it goes in the vtable too.
fn describe(&self) -> String {
format!("{} has {} legs", self.name(), self.legs())
}
}
struct Spider;
struct Hen;
impl Animal for Spider {
fn name(&self) -> String {
"spider".to_string()
}
fn legs(&self) -> u32 {
8
}
}
impl Animal for Hen {
fn name(&self) -> String {
"hen".to_string()
}
fn legs(&self) -> u32 {
2
}
}
fn main() {
// Object-safe, so we can build a Vec of trait objects and walk it.
let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Spider), Box::new(Hen)];
for animal in &zoo {
println!("{}", animal.describe());
}
}Animal has only &self methods returning String and u32, plus a default describe built from them. Every one has a single address, the vtable builds, and the Vec<Box<dyn Animal>> holds a spider and a hen together. A default method is no obstacle: it calls the others through the same vtable, so it slots in like any other.
Object safety is not an arbitrary restriction; it is the vtable’s shape made into a rule. A vtable is a fixed list of one-address-each method pointers. A generic method is not one address, it is a family of them, so it has no slot. A -> Self return needs a size the caller cannot know once the concrete type is erased. The trait is "not dyn compatible" precisely when the compiler cannot draw the table. Learn to see the rule that way and you will predict the error before the compiler reports it.
Returning a trait: impl Trait or Box dyn Trait
III.10 showed one answer to "return something that implements a trait": impl Trait in return position, the function promising "some single type that is Shape" without naming it. This chapter gives the second answer, Box<dyn Trait>, and the two are not interchangeable. The difference is exactly the one from section 01, now in return position. impl Trait resolves to one concrete type, fixed at compile time, the same on every path through the function. Box<dyn Trait> is a trait object, so different branches may return different concrete types. Run it:
// Two answers to "return something that implements a trait". impl Trait returns
// ONE hidden concrete type, fixed at compile time. Box<dyn Trait> returns a
// trait object, so different branches may return different concrete types.
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
r: f64,
}
struct Square {
side: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.r * self.r
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
// impl Trait: always the SAME concrete type, so a circle here and a square in
// another branch would not compile. Good when there is exactly one return type.
fn unit() -> impl Shape {
Circle { r: 1.0 }
}
// Box<dyn Shape>: the return type is "a pointer to some Shape", so the two arms
// may hand back different concrete types. This is the case impl Trait cannot do.
fn from_spec(spec: &str) -> Box<dyn Shape> {
if spec == "circle" {
Box::new(Circle { r: 2.0 })
} else {
Box::new(Square { side: 3.0 })
}
}
fn main() {
println!("unit circle: {:.4}", unit().area());
println!("from \"circle\": {:.4}", from_spec("circle").area());
println!("from \"square\": {:.1}", from_spec("square").area());
}unit returns impl Shape and always builds a Circle: one concrete type, zero allocation, calls on the result statically dispatched and inlinable. from_spec returns Box<dyn Shape> and chooses between a Circle and a Square at run time. Try rewriting from_spec to return impl Shape with those same two branches and the compiler refuses it: `if` and `else` have incompatible types (error E0308), because impl Trait demands one type and you handed it two. That single failure is the cleanest way to feel the boundary between the two.
Return impl Trait when the function always produces the same concrete type and you just want to hide its name (an iterator, a closure, one builder). Return Box<dyn Trait> when the concrete type genuinely varies at run time across branches or callers. impl Trait is "I have one type, hidden"; Box<dyn Trait> is "I have any type, chosen as I go".
Do not confuse impl Trait in an argument with impl Trait in a return. In argument position, fn f(x: impl Draw) is just sugar for a generic <T: Draw>, and the caller picks the type. In return position, -> impl Draw is the opposite: the function picks one hidden type and the caller must accept it. Same two words, mirror-image meanings, depending on which side of the arrow they sit on. III.10 covered the argument side; this is the return side.
Which one, and when
With both machines on the table, the choice usually makes itself once you ask one question: do you know all the types at compile time? If yes, and especially if the call is hot, prefer generics and static dispatch. You pay in code size but gain inlinable, fully specialized calls, and the closed set of types is no loss because you had them all anyway. This is the default for ordinary library functions: fn announce<T: Summary>(item: &T) from III.10 is the shape you reach for first.
Reach for dyn when the type set is open or heterogeneous. A collection of mixed concrete types that share a trait, the screen of widgets, needs Vec<Box<dyn Draw>>; there is no generic that holds two different element types at once. A function that returns different concrete types from different branches needs Box<dyn Trait>. A plugin boundary, where types arrive at run time and were unknown when the host was compiled, can only be dyn. The worked pipeline below is the canonical shape: the steps are Box<dyn Transform> values in a Vec, assembled like data and run in order, with the runner blind to the concrete types. Run it:
// A worked example: a text pipeline whose steps are chosen at run time. Each
// step is a Box<dyn Transform>, so the set of steps is just data in a Vec, built
// from config rather than fixed in the types. This is where dyn earns its keep.
trait Transform {
fn apply(&self, input: &str) -> String;
// A name for logging, so we can see the pipeline that was assembled.
fn label(&self) -> &str;
}
struct Upper;
struct Reverse;
struct Exclaim {
times: usize,
}
impl Transform for Upper {
fn apply(&self, input: &str) -> String {
input.to_uppercase()
}
fn label(&self) -> &str {
"upper"
}
}
impl Transform for Reverse {
fn apply(&self, input: &str) -> String {
input.chars().rev().collect()
}
fn label(&self) -> &str {
"reverse"
}
}
impl Transform for Exclaim {
fn apply(&self, input: &str) -> String {
format!("{}{}", input, "!".repeat(self.times))
}
fn label(&self) -> &str {
"exclaim"
}
}
// The pipeline owns its steps as trait objects and runs them in order. It does
// not know or care which concrete types it holds, only that each is Transform.
fn run(steps: &[Box<dyn Transform>], input: &str) -> String {
let mut value = input.to_string();
for step in steps {
value = step.apply(&value);
println!("after {:>7}: {value}", step.label());
}
value
}
fn main() {
// Assembled like data: swap, reorder, or extend without touching run.
let pipeline: Vec<Box<dyn Transform>> = vec![
Box::new(Reverse),
Box::new(Upper),
Box::new(Exclaim { times: 3 }),
];
let out = run(&pipeline, "hello");
println!("result: {out}");
}run takes &[Box<dyn Transform>] and walks the steps, calling apply through the vtable on each. The set of steps, their order, even their count, is data in a Vec, not structure baked into the types. You could read that Vec from a config file, reorder it at run time, or push a new Transform that did not exist when run was written, and run would not change by a line. That openness is the thing generics cannot give and the reason dyn exists.
A pragmatic note to close the decision. The two are not a wall you choose once; real programs mix them freely. A generic function can take a &dyn Trait argument; a struct can hold a Vec<Box<dyn Trait>> field while its methods stay generic. When you are unsure, start with generics, because they are the zero-overhead default, and switch a specific spot to dyn the moment you need a heterogeneous collection, a run-time-chosen return, or a smaller binary. Let the requirement, not the fear of a vtable, decide.
Know every type at compile time and want the fastest call? Generics. Need a mixed collection, a branch-varying return, or a type not known until run time? dyn. Unsure? Default to generics and reach for dyn at the exact point a requirement forces it.
Read it again
Scroll back to the screen of widgets in section 01 and read it once more. Every piece has a name now. The Vec<Box<dyn Draw>> is a list of trait objects, each a fat pointer carrying its data and its vtable. dyn Draw lives behind Box because it is unsized and needs a pointer to gain a size. Each widget.draw() is a dynamic-dispatch call that follows the vtable to the right method at run time. The whole thing works only because Draw is object safe: every method has a single address the vtable can hold. You can read it now.
A trait object dyn Trait is a value seen through its trait, kept behind a pointer (&dyn or Box<dyn>), carrying a vtable so its methods resolve at run time. It buys a heterogeneous, open set of types and one copy of the code; it costs an indirect call and lost inlining. Generics are the static mirror image: closed, specialized, inlinable, one copy per type.
One thread runs ahead of this chapter and gets its own home next. The reason &Box<dyn Draw> and widget.draw() worked so smoothly, peeling a Box down to the fat pointer the method needs, is deref coercion, and III.12 is where that mechanism, with Deref, From, and the operator traits, stops being something that "just works" and becomes something you can write yourself.
The whole chapter in six lines
- A trait object
dyn Traitis a value addressed through its trait, letting different concrete types share oneVec, one argument, or one return. dyn Traitis unsized, so it always lives behind a pointer:&dyn Traitto borrow,Box<dyn Trait>to own.- A trait object is a fat pointer: one word to the data, one to a vtable of method addresses, and a call follows the vtable at run time (dynamic dispatch).
- Static dispatch (generics, monomorphized) is faster and inlinable but one copy per type; dynamic dispatch is one copy, a small per-call cost, and an open set of types.
- Object safety(now "dyn compatibility") is the vtable’s shape as a rule: no generic methods, no
-> Selfreturns, or the trait cannot becomedyn(errorE0038). - Return
impl Traitfor one hidden concrete type;Box<dyn Trait>when the type genuinely varies at run time. Default to generics; reach fordynwhen a requirement forces it.