all_lessons/Rust/13 · Trait objects — dynamic dispatch, pricedlesson 14 / 23

Trait objects — dynamic dispatch as a priced choice

Lesson 12 removed the run-time mechanism from abstraction by fixing every type at compile time, which leaves nothing for a type chosen while the program runs — a scene file that says circle, rectangle, circle. This lesson erases the type behind a pointer that also carries a table of its behavior, dyn Trait. Which traits can be erased is what such a table can hold; E0038 is that rule speaking. It buys one collection for an open set of types, at a price — an indirect call, a pointer, usually an allocation — written in the source and measured here.

The thesis, here
Rust never dispatches a trait method dynamically behind your back: the call is resolved at compile time unless a type says dyn. A trait object is type erasure made explicit: the value stays as it was, and the pointer to it grows a second word, the address of a table of the erased type's functions. Every rule about trait objects asks what a caller knowing only the trait must find there.
Linear position
Forced by: Generics fix each type at compile time: fast, but one concrete type per instantiation. When the type is chosen at run time — plugins, mixed collections, a list of different shapes — what is the alternative, and what is its price?
New idea: erase the type behind a pointer that carries a table of its behavior, &dyn Shape = (address, vtable), paying an indirect call only where the source says dyn. Which traits can be erased is decided by what one finite table of fixed entries can hold (E0038).
Forces next: Static and dynamic dispatch abstract over types. What about abstracting over behavior passed as a value — a piece of code together with the variables it uses? What can the variables it uses mean when every variable has an owner?
The plan
Seven moves. (1) Put circles and rectangles in one vector; every road but one closes. (2) Take &dyn Shape apart: two words, a table, an indirect call. (3) Derive which traits can be erased from what the table must hold. (4) Test the rules against rustc's verdicts. (5) Pay the bill: one loop, measured four ways. (6) Choose an open set or a closed one. (7) Translate the object-oriented habits.

1 · A list of different shapes

A drawing program reads a scene file — circle 1.0, rect 2 3, circle 0.5 — and reports the total area. Lesson 11 gave us a Shape trait with area. Holding the shapes fails at once:

trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }
impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } }

fn main() {
    // the scene file said: circle 1.0, rect 2 3
    let shapes = vec![Circle { r: 1.0 }, Rect { w: 2.0, h: 3.0 }];
}
error[E0308]: mismatched types
 --> src/main.rs:9:42
  |
9 |     let shapes = vec![Circle { r: 1.0 }, Rect { w: 2.0, h: 3.0 }];
  |                                          ^^^^^^^^^^^^^^^^^^^^^^^ expected `Circle`, found `Rect`

The error is not about traits. A Vec<T> is one buffer of equal slots (Lesson 03), and T fixes their size: a Circle is 8 bytes, a Rect 16 (§2). Generics do not help: total<S: Shape> is stamped out per S, each copy for one type (Lesson 12). We need one type that stands for any shape. Three roads:

RoadHow it gets one typeWhat it costs
an enum of every shape (Lesson 09)slots sized for the largest varianta closed set: no plugin can add a shape
a bare pointerall pointers have one sizethe address alone forgets which area to call
a pointer plus the behaviorbeside the address, a table of the type's functionsa second word; each call goes through the table

The third road keeps the set open. Rust calls it a trait object: dyn Shape means "some type that implements Shape, chosen at run time", and Box<dyn Shape> owns one. Only the annotation changes:

trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }
impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } }

fn main() {
    // one element type: an owning pointer to some Shape, decided at run time
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { r: 1.0 }), Box::new(Rect { w: 2.0, h: 3.0 })];
    let mut total = 0.0;
    for s in &shapes {
        total += s.area();          // Circle's area, then Rect's
    }
    println!("{total:.2}");
}
9.14

Each box became a Box<dyn Shape> because the annotation asked for one. So we need to know what the pointer holds and what its table must contain.

Road not taken · a table pointer in every object
The C++ track's Lesson 10 puts a hidden table pointer in every object of a class with virtual functions, so any plain pointer can dispatch and every such object pays for the field. Rust's budget forbids a cost the type does not show, so the table pointer lives in the pointer: a Circle stays 8 bytes. The Go track's Lesson 05 describes a Go interface value as a similar two-word (type, data) pair.

2 · A pointer that carries its type's table

use std::mem::size_of;
trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }

fn main() {
    println!("Circle         {}", size_of::<Circle>());          // just the radius
    println!("Rect           {}", size_of::<Rect>());
    println!("&Circle        {}", size_of::<&Circle>());
    println!("&dyn Shape     {}", size_of::<&dyn Shape>());      // address + table
    println!("Box<dyn Shape> {}", size_of::<Box<dyn Shape>>());
}
Circle         8
Rect           16
&Circle        8
&dyn Shape     16
Box<dyn Shape> 16

The circle has no hidden field. A &dyn Shape is the data's address plus that of a vtable (virtual-function table), generated once per implementation (Circle's Shape, Rect's): a fat pointer like Lesson 02's &[T], "currently" twice a usize per the Reference, not to be relied on. Since "some Shape" has no single size, dyn Shape alone is unsized: it lives only behind a pointer (&, Box, Rc, Arc), which Lesson 12's ?Sized admits.

What must the table hold? Whatever a caller with only the two words needs: to call a method, its code address, one per method of the trait and its supertraits; to end the value, the type's drop (Lesson 03); to free it, size and alignment. This program asks for all three (&**s is the &dyn Shape in each box):

use std::mem::{align_of_val, size_of_val};
trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Polygon { pts: Vec<(f64, f64)> }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }
impl Shape for Polygon { fn area(&self) -> f64 { 0.0 } }
impl Drop for Polygon {
    fn drop(&mut self) { println!("drop Polygon, {} points", self.pts.len()); }
}
fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { r: 1.0 }), Box::new(Polygon { pts: vec![(0.0, 0.0); 3] })];
    for s in &shapes {
        // asked through the table: how big is the value, how aligned?
        println!("size {} align {}", size_of_val(&**s), align_of_val(&**s));
    }
    drop(shapes);                     // each Box ends its value with that type's drop
    println!("dropped");
}
size 8 align 8
size 24 align 8
drop Polygon, 3 points
dropped

The standard library's unstable documentation of trait-object metadata lists these contents and promises no layout; the widget draws a concept, not a memory map. The drop is never optional: a C++ destructor dispatches only if declared virtual, and deleting through a base pointer without one is undefined behavior (the C++ track's Lesson 10), while a Box<dyn Shape> always finds its value's drop. The call itself is indirect:

// #[inline(never)] fn area_static(c: &Circle) -> f64 { c.area() }
// #[inline(never)] fn area_dyn(s: &dyn Shape) -> f64 { s.area() }
area_static:                 area_dyn:              ; x1 = the table's address
  ldr  d0, [x0]    ; r         ldr x1, [x1, #24]    ; load area's entry
  ...              ; π         br  x1               ; jump there: an indirect call
  fmul d1, d0, d1  ; π·r
  fmul d0, d0, d1  ; r·π·r
  ret              ; area inlined

area_static knows its callee, so the call vanished into two multiplications. area_dyn loads a code address 24 bytes into the table, past three words whose meaning and order no document fixes, and jumps to it. That jump is the price of dynamic dispatch; §5 measures it.

What erasure forgets

Erasure also forgets two facts the compiler read off the type: whether the value borrows (its lifetime, Lesson 06's region as the source spells it), and whether it may cross threads (Send). The object type restates both, with defaults. A shape that borrows its size:

trait Shape { fn area(&self) -> f64; }
struct Scaled<'a> { base: &'a f64, k: f64 }       // a shape that borrows its size
impl Shape for Scaled<'_> { fn area(&self) -> f64 { self.base * self.k } }

fn keep(s: Box<dyn Shape>) -> f64 { s.area() }
fn main() {
    let unit = 1.5;
    let s = Scaled { base: &unit, k: 2.0 };
    println!("{}", keep(Box::new(s)));
}
error[E0597]: `unit` does not live long enough
  = note: due to object lifetime defaults, `Box<dyn Shape>` actually means `Box<(dyn Shape + 'static)>`

The compiler's note is the rule: in a signature, Box<dyn Shape> means Box<dyn Shape + 'static>, which holds no borrow of anything that ends (Lesson 08). It is a default: write the bound and the shape is accepted, and a plain &dyn Shape takes it as is:

trait Shape { fn area(&self) -> f64; }
struct Scaled<'a> { base: &'a f64, k: f64 }
impl Shape for Scaled<'_> { fn area(&self) -> f64 { self.base * self.k } }

fn keep(s: Box<dyn Shape + '_>) -> f64 { s.area() }   // "+ '_": it may borrow
fn peek(s: &dyn Shape) -> f64 { s.area() }             // no bound written
fn main() {
    let unit = 1.5;
    println!("{}", keep(Box::new(Scaled { base: &unit, k: 2.0 })));
    println!("{}", peek(&Scaled { base: &unit, k: 2.0 }));
}
3
3

Likewise a Box<dyn Shape> is Send only if its type says so, dyn Shape + Send or a Send supertrait (Lesson 16). We know what the table is; next, which traits can have one.

3 · Which traits can be erased?

Give the trait the natural "copy yourself" method, and the vector of boxes stops compiling:

trait Shape {
    fn area(&self) -> f64;
    fn dup(&self) -> Self;          // "give me a copy of whatever you are"
}
fn total(shapes: &[Box<dyn Shape>]) -> f64 {
    let mut t = 0.0;
    for s in shapes { t += s.area(); }
    t
}
Reading the error
error[E0038]: the trait `Shape` is not dyn compatible
 --> src/lib.rs:5:24
note: for a trait to be dyn compatible it needs to allow building a vtable
3 |     fn dup(&self) -> Self;          // "give me a copy of whatever you are"
  |                      ^^^^ ...because method `dup` references the `Self` type in its return type
  = help: consider moving `dup` to another trait
The first line names the property: a trait is dyn compatible when a vtable can be built for it (formerly "object safety"); each "…because" names an offending item and why. What it cannot know is which side you would rather give up: its help moves dup out, but you could keep it for concrete types only (where Self: Sized, below), return a box, or make total generic and lose the mixed list. Nor does it list everything: while a supertrait requires Sized, that is the only reason given.

Play the caller. total holds a &dyn Shape and calls s.dup(). The result has the erased type — 8 bytes for a circle, 16 for a rectangle — but total is compiled once, its frame (room for the result included) laid out at compile time (Lesson 02), while the table knows the size only at run time. C++ answers the same question with the base class's size, and calls the result slicing:

struct Shape { double r = 1; virtual double area() const { return 3.14159 * r * r; } };
struct Ring : Shape {
    double inner = 0.5;
    double area() const override { return Shape::area() - 3.14159 * inner * inner; }
};
double copy_area(const Shape& s) {
    Shape copy = s;       // copies only the Shape part: `inner` and Ring's behavior are cut off
    return copy.area();   // for a Ring: 3.14, where ring.area() is 2.36
}

The copy is defined, and silently wrong (a failure mode in the C++ track's Lesson 10). Rust refuses the trait instead. Ask the same question of every kind of item — what would a caller holding only (address, table) need? — and the rules fall out:

ItemWhat that caller would needVerdict
&self, &mut self, self: Box<Self> methodsa code address, called with the data address✓ an entry
-> Self; other: &Selfroom for a result of unknown size; a second value of the same unknown type, unverifiable✗ breaks it
fn scale<T>(&mut self, k: T)an entry per T a caller might pick: no finite table✗ breaks it
async fn; -> impl Displaya result whose hidden type differs per implementation✗ breaks it
fn load(path: &str) -> Selfa pointer to reach the table: none✗ breaks it
const SIDES: u32a compile-time value for a type known at run time✗ breaks it
trait Shape: Clone (or : Sized)dyn Shape: Sized, never true✗ breaks it
type Out;nothing: the object type names it, dyn Shape<Out = f64>✓ fine

Each row is one fact met from another side: finitely many fixed-size entries, reached through a pointer. rustc rejects all four dyn uses below, one reason each:

trait Scale { fn scale<T: Into<f64>>(&mut self, k: T); }  // one table entry per T?
trait Sides { const SIDES: u32; }                         // a compile-time value
trait Load { fn load(path: &str) -> Self; }               // no pointer, no table
trait Copyable: Clone { fn area(&self) -> f64; }          // Clone requires Self: Sized
fn draw(_: &dyn Scale, _: &dyn Sides, _: &dyn Load, _: &dyn Copyable) {}

Often the offending method is not needed through the pointer. where Self: Sized says so — "only for types whose size is known", which dyn Shape never is — so the method stays off the table, the trait is dyn compatible, and concrete types keep the method:

trait Shape {
    fn area(&self) -> f64;
    fn dup(&self) -> Self where Self: Sized;   // not in the table; still in the trait
}
#[derive(Clone)]
struct Circle { r: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.r * self.r }
    fn dup(&self) -> Self { self.clone() }
}
fn main() {
    let c = Circle { r: 1.0 };
    let copy = c.dup();                  // fine: the concrete type is known
    let s: &dyn Shape = &c;              // fine: Shape is dyn compatible again
    let again = s.dup();                 // rejected: dup is not in the table
}
error: the `dup` method cannot be invoked on a trait object

The Reference counts by-value self as the same case without the words: declaring it is fine, calling it through the pointer would move a value of unknown size. The dispatchable form is self: Box<Self>:

trait Shape { fn area(&self) -> f64; fn consume(self) -> f64; }
fn finish(b: Box<dyn Shape>) -> f64 { b.consume() }   // move the value out: how big is it?

When a copy is needed through the pointer, return something of known size, a box. That also answers "why is Clone not dyn compatible?": clone returns Self, and Clone requires Sized. The copy that works:

trait Shape {
    fn area(&self) -> f64;
    fn clone_box(&self) -> Box<dyn Shape>;   // returns a pointer: its size is known
}
#[derive(Clone)]
struct Circle { r: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.r * self.r }
    fn clone_box(&self) -> Box<dyn Shape> { Box::new(self.clone()) }
}
fn main() {
    let a: Box<dyn Shape> = Box::new(Circle { r: 2.0 });
    let b = a.clone_box();                   // a deep copy, through the table
    println!("{:.2} {:.2}", a.area(), b.area());
}
12.57 12.57
Road not taken · silently leave the bad methods out
Why reject the whole trait instead of quietly leaving dup out of the table? Because every dyn Shape implements Shape: generic code bounded by T: Shape + ?Sized may call anything Shape promises, and with T = dyn Shape a missing dup would be a call into nothing. where Self: Sized is the honest "leave it out", written where every caller reads it.

4 · Try it: which traits get a table?

The widget asks §3's question of any trait you build. Checked against rustc 1.98.1 on 450 random traits, and calls through them: no disagreement.

Can this trait be a dyn?
Left: the fat pointer. Middle: each implementing type's table (contents, not layout). Right: the items — ✓ in the table, ○ left out, harmless; ✗ breaks the trait. The slider declares them one by one.
rustc says
—
table entries
—
in / left out / breaking
—
reasons listed
—
Show the core JS
// What one method does to the table: the reasons rustc gives, in rustc's order.
function methodReasons(it) {
  if (it.sized) return [];                                   // excluded: no table entry needed
  var n = '`' + it.name + '`';
  if (it.recv === 'none') return ['associated function ' + n + ' has no `self` parameter'];
  var r = [];
  if (it.selfArg) r.push('method ' + n + ' references the `Self` type in this parameter');
  if (it.ret === 'self' && !it.async) r.push('method ' + n + ' references the `Self` type in its return type');
  if (it.async) r.push('method ' + n + ' is `async`');
  if (it.ret === 'impl' && !it.async) r.push('method ' + n + ' references an `impl Trait` type in its return type');
  if (it.generic) r.push('method ' + n + ' has generic type parameters');
  return r;
}
// analyze(): a Sized supertrait, else the first GAT, is reported alone; else E0191 or all reasons.

What to try. On the first preset, drag the slider from 0 to 3: the table grows from 3 entries to 6. "+ dup() -> Self": one reason, no table; "dup … where Self: Sized": 4 entries, dup marked ○. "trait Shape: Clone" also declares dup and scale<T>, yet rustc lists 1 reason; set the supertrait to none and 2 others appear. Then add a method with other: &Self, async and a Self return: see which reasons stack, and which one rustc drops.

5 · The price

Dynamic dispatch has four costs, each visible in the source: an indirect call the optimizer usually cannot see through, so nothing is inlined across it; a pointer chase; usually an allocation per value; and no per-type specialization — one copy of the code for every type, which is also the upside: no Lesson 12 copies. How big is the first? One loop, four ways:

use std::{hint::black_box, time::Instant};
trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }
impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } }
enum Kind { C(Circle), R(Rect) }
fn total<S: Shape>(v: &[S]) -> f64 { let mut t = 0.0; for s in v { t += s.area(); } t }
fn total_dyn(v: &[Box<dyn Shape>]) -> f64 { let mut t = 0.0; for s in v { t += s.area(); } t }
fn total_enum(v: &[Kind]) -> f64 {
    let mut t = 0.0;
    for k in v { t += match k { Kind::C(c) => c.area(), Kind::R(r) => r.area() }; }
    t
}
fn ns_per_shape(run: impl Fn() -> f64) -> f64 {   // `impl Fn`, `|| …`: closures (Lesson 14)
    for _ in 0..20 { black_box(run()); }           // warm up; then 15 timed runs, keep the median
    let mut ns: Vec<u128> = (0..15).map(|_| { let t = Instant::now(); black_box(run()); t.elapsed().as_nanos() }).collect();
    ns.sort();
    ns[7] as f64 / 1e6                             // each run visits 1,000,000 shapes
}
fn main() {
    let mut x = 88172645463325252u64;              // xorshift: circle or rect, in random order
    let circle: Vec<bool> = (0..1_000_000).map(|_| { x ^= x << 13; x ^= x >> 7; x ^= x << 17; x & 1 == 0 }).collect();
    let c = |i: usize| Circle { r: (i % 100) as f64 };
    let r = |i: usize| Rect { w: (i % 100) as f64, h: 2.0 };
    let circles: Vec<Circle> = (0..1_000_000).map(c).collect();
    let boxed: Vec<Box<dyn Shape>> = (0..1_000_000).map(|i| Box::new(c(i)) as Box<dyn Shape>).collect();
    let mixed: Vec<Box<dyn Shape>> =
        (0..1_000_000).map(|i| if circle[i] { Box::new(c(i)) as Box<dyn Shape> } else { Box::new(r(i)) }).collect();
    let kinds: Vec<Kind> = (0..1_000_000).map(|i| if circle[i] { Kind::C(c(i)) } else { Kind::R(r(i)) }).collect();
    println!("generic, circles only  {:.2} ns", ns_per_shape(|| total(black_box(&circles))));
    println!("dyn,     circles only  {:.2} ns", ns_per_shape(|| total_dyn(black_box(&boxed))));
    println!("dyn,     random mix    {:.2} ns", ns_per_shape(|| total_dyn(black_box(&mixed))));
    println!("enum,    random mix    {:.2} ns", ns_per_shape(|| total_enum(black_box(&kinds))));
}
Loopns per shaperange over 12 runs of the program
generic total::<Circle>, circles only0.610.56 – 0.64
total_dyn, the same circles, boxed0.750.66 – 0.75
total_dyn, circles and rectangles in random order3.423.28 – 3.55
total_enum, the same random order2.472.46 – 2.49

(rustc 1.98.1, -C opt-level=3, aarch64, Apple M5, 2026-09-30; each number a median of 15 timed runs with black_box. One machine, one loop, not a law.) Read it in pairs. Rows 1 and 2 hold the same values, so their gap is the table and the box: 0.75 − 0.61 ≈ 0.14 ns per call, partly hidden because both loops wait on one running total, one addition after another. Rows 2 and 3 differ only in mixing rectangles in at random, which cost far more: an indirect jump whose target changes at random is hard to predict (priced in the C++ track's Lesson 10). Row 4 is the fair comparison for a mixed list: over row 1 the enum paid 1.86 ns where dyn paid 2.81, about two thirds; its match is a branch a random order defeats too.

So "dyn is slow, generics are fast" is half a sentence: generics pay in code size and compile time (Lesson 12), a dyn call a toll that matters when the method is as small as area, and the shape of the data can cost more than either. Each cost is spelled dyn, Box or match in the source, so measure the loop you have. The last two rows differ by more than speed: an enum and a trait object let a program grow in different directions.

6 · Open sets and closed sets

For a list of different shapes the real choice is an enum or a dyn, and they break in opposite directions: a new shape breaks every match, a new operation every impl:

enum Shape { Circle(f64), Rect(f64, f64), Triangle(f64) }   // a new variant...
fn area(s: &Shape) -> f64 {
    match s { Shape::Circle(r) => 3.14159 * r * r, Shape::Rect(w, h) => w * h }   // ...breaks every match
}
trait Shape { fn area(&self) -> f64; fn perimeter(&self) -> f64; }   // a new operation...
struct Circle { r: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }   // ...breaks every impl

This is the expression problem: programs grow by new types and new operations, and each design makes one free. With the enum, a new operation is one function, its match checked for exhaustiveness (Lesson 09); values sit inline. With dyn, a new type is one impl anywhere, even in a crate the trait's author never sees; a new operation touches every impl unless it has a default body (Lesson 11); values sit behind pointers.

Lesson 10 pointed here for Box<dyn Error>: the errors an application meets form an open set, so it holds them as trait objects. The standard library converts every E: Error into Box<dyn Error>, so ? boxes any error on the way out, and printing one is a call through its table:

use std::error::Error;
fn radius(line: &str) -> Result<f64, Box<dyn Error>> {
    let r: f64 = line.trim().parse()?;                      // a ParseFloatError, boxed by `?`
    if r < 0.0 { return Err("negative radius".into()); }    // a &str, boxed by .into()
    Ok(r)
}
fn main() {
    for line in ["1.5", "x", "-2"] {
        match radius(line) { Ok(r) => println!("ok {r}"), Err(e) => println!("error: {e}") }  // Display: a table call
    }
}
ok 1.5
error: invalid float literal
error: negative radius

A library whose callers react to each failure returns a closed enum; an application that mostly reports failures takes the open box. An open set can still answer one question about the concrete type, because a table can carry the type's identity: dyn Any's does, and downcast_ref compares it:

use std::any::Any;
struct Circle { r: f64 }
fn describe(x: &dyn Any) {
    match x.downcast_ref::<Circle>() {         // compares the type id the table carries
        Some(c) => println!("a circle of radius {}", c.r),
        None => println!("not a circle"),
    }
}
fn main() { describe(&Circle { r: 1.0 }); describe(&2.5f64); }
a circle of radius 1
not a circle

Reach for it rarely: a chain of downcast_ref tests is a match nobody checks for exhaustiveness, usually a sign the set was closed after all. An open set is also where a Java or C++ programmer expects a class hierarchy.

7 · "Where is my inheritance?"

Programmers from Java or C++ reach for a class hierarchy; the gap is measurable: in a 2024 study of the Rust Book's quizzes (Crichton and Krishnamurthi), the item named "Rust lacks inheritance" rose from 29% to 74% correct after the authors added one paragraph on a fact the text had never stated. So, plainly: Rust has no inheritance of fields or behavior between types. Instead:

In Java or C++ you would writeIn Rust, write
a base class with fieldsa struct field (composition) plus a trait; you cannot add data to a trait object
a virtual method, overridden; an abstract classa trait method per type, with a default body where one fits
instanceof, a downcast; a visitoran enum and match; Any for a truly open set
an interfacea trait with explicit impls (Lesson 11); dyn only for run-time choice

A warning: impl Deref<Target = Base> for Derived makes derived.base_method() compile, since Deref also drives method lookup. But the standard library advises Deref only for types that transparently behave like their target, and imitating a base class with it is a documented anti-pattern. Write the field; forward the calls you mean.

Type inference is the other habit that breaks: the Book's quiz item named "trait objects and type inference" scored 9% before an intervention and 18% after. One trap is §1's vector again, boxed, its type left to inference:

trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }
impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } }

fn main() {
    // no annotation this time
    let shapes = vec![Box::new(Circle { r: 1.0 }), Box::new(Rect { w: 2.0, h: 3.0 })];
}
error[E0308]: mismatched types
9 |     let shapes = vec![Box::new(Circle { r: 1.0 }), Box::new(Rect { w: 2.0, h: 3.0 })];
  |                                                    -------- ^^^^^^^^^^^^^^^^^^^^^^^ expected `Circle`, found `Rect`
  |                                                    arguments to this function are incorrect

Inference took the element type from the first element, Box<Circle>, then expected a Circle in the second Box::new; it did not look for a shared trait. In this lesson a value became a trait object only where a dyn type was expected: an annotation (§1), a parameter (peek), a return type (clone_box), an as cast (§5).

All of this abstracts over types; programs also pass behavior as a value, with the variables it uses.

Common mistakes / failure modes

"dyn is slow, generics are fast"
Both have a price. Measured here: 0.14 ns more per call through the table and box, while mixing types at random cost more, enum included. Generics pay in code size and compile time, and hold no mixed list (§5).
"A trait object is a Java interface, and Deref gives me inheritance"
No fields, no inherited state: a table beside an address, joined by an explicit impl. Deref only redirects method lookup; imitating a base class with it is a documented anti-pattern (§2, §7).
"Any trait can be a trait object"
Only a dyn-compatible one (E0038): no Self in arguments or return, no generic methods, no associated consts, no receiver-less functions, no Sized supertrait. Even Clone fails; write clone_box (§3).
"Box<dyn Trait> is always 'static"
Only by default, in a signature, as the compiler's note says; Box<dyn Shape + '_> may borrow, and a plain &dyn Shape took a borrowing shape as is (§2).
"Downcasting is how you get the type back"
Usually it shows you wanted an enum: Any answers only "is it exactly this type?", one test at a time, unchecked for exhaustiveness (§6).

Checkpoint exercise

Try it
On "area, grow, clone_box", add scale (&mut self, returns nothing, <T> ticked), then press "+ const". Predict how many reasons rustc lists, and which. Then find the smallest change that makes the trait dyn compatible while a concrete Circle can still call scale, and predict the table's entries. Check in the widget, then in a scratch file (an impl for Circle, a function taking &mut dyn Shape). (Answer: two reasons, scale's type parameter and the associated const. Both must go: scale gets where Self: Sized and the const leaves the trait, so the table has 6 entries, drop, size, alignment and area, grow, clone_box; rustc accepts scale on the Circle, not through the pointer.)

Where this points next

Static and dynamic dispatch both vary the type behind a trait. The next abstraction varies the code: a sort wants an ordering, a button a handler, a thread its work — behavior handed over as a value, usually with some of the caller's variables. A Box<dyn Fn> can store it; the box is the easy part (Lesson 14). Code that carries variables must hold each one somehow, yet each already has an owner: so what can the variables it uses mean, when every variable has an owner?

Takeaway
When the type is chosen at run time, Rust erases it explicitly: dyn Trait is "some type implementing Trait", usable only behind a pointer that carries two addresses, the data's and a compiler-generated table's (methods, drop, size, alignment). No trait call is dynamic unless a type says dyn. A trait can be erased only if one finite table behind a pointer serves every caller — no Self in arguments or return, no generic methods, no associated consts, no receiver-less functions, no Sized supertrait (E0038); where Self: Sized keeps a method off the table. Erasure forgets lifetimes and Send, so the type restates them. The price, an indirect call the optimizer usually cannot see through plus usually a box, was 0.14 ns per call in one measured loop, less than a random order of types. enum for closed sets, dyn for open ones.

Interview prompts

Companion reads: C++ · 10 Polymorphism and the vtable (vptr, slicing), Go · 05 Interfaces (the two-word interface value), C++ · 12 Templates (the static side), FP · 14 Effects and type classes (behavior as an instance).