---
title: 'Object Safety and Dynamic Dispatch'
source: 'https://academia.sh/en/courses/rust-abstraction/object-safety-and-dynamic-dispatch'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes the syntax of static and dynamic dispatch (a trait bound vs. a dyn trait object)'
    - "Compares the two dispatch forms' run counts to show the count does not change"
    - 'Describes the object safety condition in prose and demonstrates an accepted design that satisfies it by running it'
---

# Object Safety and Dynamic Dispatch

A call written with a trait bound resolves at compile time; a call written through a trait object (dyn) resolves at run time — in the measurement kernel both run twice; the difference is not in the count, it is in how the call is found, and this is only possible for traits that satisfy the object safety condition.

The Associated Types lesson showed that both ways of using a contract (associated type,
generic parameter) are resolved **at compile time**. There is a third way to use a trait, and
it is resolved not at compile time but **at run time**: the trait object. The kernel that ran in
the course's first lesson already had this distinction — `static_dispatch<T: Label>` and
`dynamic_dispatch(x: &dyn Label)` did the same job in two different call forms. This lesson
opens up that difference: whenever the call is resolved, how many times does the body run?

## Two Call Forms

The `static_dispatch<T: Label>(x: &T)` syntax is familiar from the previous two lessons: `T` is
a type parameter, bound to `Label`. The compiler generates this function separately for every
concrete type it is called with — as the Trait Bounds lesson showed. The
`dynamic_dispatch(x: &dyn Label)` syntax is different: `x`'s type is no longer a type
parameter, it is `dyn Label` — "a reference to any value implementing `Label`, whose type will
be determined at run time." The `dyn` keyword is how you tell the compiler "I do not yet know
the concrete type behind this reference, but I know I can call `Label`'s methods on it."

These two forms exist because they answer different questions. Static dispatch generates a
separate body for every concrete type called — this generation happens at compile time, as the
Trait Bounds lesson showed, and never enters the run ledger. But this generation carries a
cost too: when a function writes `<T: Label>`, every place that calls that function has to know,
at compile time, which concrete `T` is used. If a list, a field, or a return value wants to say
"could be either `Plain` or `Custom`," static dispatch cannot express this — a type parameter has
to resolve to a single concrete type. Dynamic dispatch fills exactly this gap: `dyn Label`, by
saying "it does not matter which concrete type it is, as long as it implements `Label`," makes it
possible to hold different types in the same place. This lesson shows that the two do not
**replace** each other, they **complete** each other.

These two notations also differ at the binary level. When `static_dispatch` is called, the
compiler reads which method will run directly from the source and embeds it into the call.
When `dynamic_dispatch` is called, `x` has to carry along a **method table** — a trait object
is actually made of two parts: a reference to the value itself, and a table holding the
addresses of methods belonging to that value's concrete type. Which body runs is **found** from
this table at call time; the single line written in the source (`x.name()`) can open onto two
different bodies depending on the type behind `x`.

`Box<dyn Label>` is built from two words, and both were already introduced in a separate
course. `Box` is a container that establishes single ownership on the heap: it places the value
it holds on the heap and keeps a single owner. `dyn Label` does not name **which** concrete type
sits inside this container, it only says which contract it satisfies. Combined, the resulting
`Box<dyn Label>` means "a value sitting on the heap, implementing `Label`, but whose exact
concrete type has been forgotten." This forgetting is deliberate: `Box<Plain>` and `Box<Custom>`
are two different types to the compiler and cannot go in the same array; `Box<dyn Label>` can go
in the same array because it is the common denominator of both.

## The Run Count Does Not Change

```rust
// times.rs — slice: Label, Plain, Custom for static and dynamic dispatch.

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

pub type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

pub fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

pub fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

pub trait Label {
    fn ledger(&self) -> &Ledger;
    fn name(&self) -> String {
        String::from("default")
    }
}

pub struct Plain {
    pub ledger: Ledger,
}

impl Label for Plain {
    fn ledger(&self) -> &Ledger {
        &self.ledger
    }
}

pub struct Custom {
    pub ledger: Ledger,
}

impl Label for Custom {
    fn ledger(&self) -> &Ledger {
        &self.ledger
    }
    fn name(&self) -> String {
        String::from("custom")
    }
}

pub fn static_dispatch<T: Label>(x: &T) -> String {
    record(x.ledger(), "static-dispatch");
    x.name()
}

pub fn dynamic_dispatch(x: &dyn Label) -> String {
    record(x.ledger(), "dynamic-dispatch");
    x.name()
}
```

```rust
// dispatch.rs — compares the run count of static and dynamic dispatch.
mod times;
use times::{times, record, Ledger, Label, Custom, Plain, dynamic_dispatch, static_dispatch};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let plain = Plain { ledger: Rc::clone(&ledger) };
    let custom = Custom { ledger: Rc::clone(&ledger) };

    let _ = static_dispatch(&plain);
    let _ = static_dispatch(&plain);

    let _ = dynamic_dispatch(&plain as &dyn Label);
    let _ = dynamic_dispatch(&custom as &dyn Label);

    // The same trait object can hold different concrete types in one list - static
    // dispatch could not do this, because a Vec<T> is locked to one concrete T.
    let list: Vec<Box<dyn Label>> = vec![
        Box::new(Plain { ledger: Rc::clone(&ledger) }),
        Box::new(Custom { ledger: Rc::clone(&ledger) }),
        Box::new(Custom { ledger: Rc::clone(&ledger) }),
    ];
    // The loop itself knows how many elements there are, but not which body will
    // run on each call - that is resolved inside name() based on the element's real type.
    let names: Vec<String> = list
        .iter()
        .map(|e| {
            record(e.ledger(), "list-body");
            e.name()
        })
        .collect();

    let mut bodies: BTreeMap<&str, u32> = BTreeMap::new();
    bodies.insert("static-dispatch", times(&ledger, "static-dispatch"));
    bodies.insert("dynamic-dispatch", times(&ledger, "dynamic-dispatch"));
    bodies.insert("list-body", times(&ledger, "list-body"));

    println!("{:<18}{}", "body", "how many times");
    for (name, n) in &bodies {
        println!("{:<18}{}", name, n);
    }
    println!("list-names={:?}", names);
}
```

```
body              how many times
dynamic-dispatch  2
list-body         3
static-dispatch   2
list-names=["default", "custom", "custom"]
```

**TG9.** `static-dispatch` ran twice, `dynamic-dispatch` also twice — the exact same number.
The difference is not in the count: both call forms trigger the body unavoidably, both fall into
the **every time** class. The Object-Oriented Java course measured *which type made the
decision* — the declared type or the runtime type. What's measured here is not that question;
what's measured is whether the two dispatch forms **change the body count** — they do not. No
matter how many times `Plain`'s and `Custom`'s `name` methods are called, whether the call form
is static or dynamic never increases or decreases this number.

The `list-body` row shows the practical consequence of this: `Vec<Box<dyn Label>>` can hold
different concrete types (`Plain` and two separate `Custom`s) **in a single collection** — the
`Box` built in the Ownership and the Memory Model course's Heap-Allocated Single Ownership
lesson, here holds a trait object inside it. `static_dispatch<T: Label>` could not do this: a
`Vec<T>` is locked to a single concrete `T`, it cannot hold `Plain` and `Custom` together in the
same array. The three-element list's body ran three times too — this also falls into the
**every time** class, because the conversion is unavoidable for every element of the array.
What's worth noting is that the closure inside `map` never knows `e`'s real type at all:
the `e.name()` call does not say which body will run until reaching the list's third element —
only that element's own method table knows. This shows that even though the two call forms'
run count is equal, they do not do the same job — a trait object can carry diversity inside a
collection, static dispatch cannot.

## Object Safety

Whether `dyn Label` compiles depends on `Label` being **object safe**. If a trait is not object
safe, a method table cannot be built for it, and the `dyn` syntax is rejected by the compiler.
There are two common obstacles: if a method returns `Self` **by value**
(something like `fn copy(&self) -> Self`), the table cannot know that method's return size in
advance — because the concrete type behind `dyn Label` can change at run time, and every type's
size can differ. If a method carries its own **type parameter** (something like
`fn convert<U>(&self, u: U)`), the table cannot hold a separate entry for infinitely many `U`s.
Both of these forms, described in prose here, are rejected — since they do not compile, they
are not put in a code block.

Both obstacles share the same root cause: every entry in a method table has to be a fixed,
known-size set of pointers at call time. If a method that returns `Self` by value were put in
the table, the caller could not answer, at compile time, the question "how much space will this
call's return value take up" — because behind `dyn Label` there could be `Plain`, or there
could be a type of another size entirely. A method carrying its own type parameter does not fit
into the table as a single entry either, because a separate body is needed for every different
`U`, and the table cannot enumerate infinitely many `U`s in advance. Both obstacles come down to
the same principle: a trait object is a reference that has **forgotten** the identity of the
implementing type; if a method depends on that forgotten identity (on `Self`'s exact size, or
on a `U` the caller chooses), the table cannot hold that method.

`Label` carries neither obstacle: `ledger` and `name` both take `&self` (not `Self` by value),
neither carries its own type parameter. This is why it is object safe, and the
`dynamic_dispatch` function above, with its `&dyn Label` parameter, compiles without a problem
— the accepted design's counterpart has already been run. Deciding which methods you want to
be usable as a trait object when writing a trait means shaping those methods' signatures around
these two rules from the start.

This condition sits in a place similar to the associated type constraint from the previous
lesson: both show that a contract might not be compatible with **every** way of using it. The
associated type constraint limited how many times a type could implement a contract; object
safety limits **which call forms** a contract can be used with. `Label` itself is not affected by
either, because it was kept simple from the start: it does not need a single associated type, and
it stays limited to object-safe methods. A trait writing a broader contract could run into one
or both of these constraints; which one it runs into can be read from the trait's signature,
before ever running it.

For example, a method that compares two values and returns the larger one **by value** —
`fn larger_of(&self, other: Self) -> Self` — would not be object safe: `Self` appears here as
both a parameter and a return type, and since the real type behind `dyn Label` can change at
run time, the table cannot fix this method's input and output size in advance. This method was
never added to `Label`; if it had been, `&dyn Label` would not compile. The same comparison
request could stay object safe by returning not a value but a **reference**, or an **ordering
result** (something fixed-size, like less, equal, greater) — and this ties into this course's
general observation too: a contract's boundaries are drawn not by the caller of that contract,
but by the **table** that is forced to work with the implementer's forgotten identity.

## The Measurement's Limit

The same count does not mean the same cost. `static-dispatch` and `dynamic-dispatch` both running
twice does not mean the two call forms are **identical at run time** — the count only measures
how many times the body was **triggered**, not how the call was **found**. In a `static` call,
which body will run is determined by reading the source at compile time; in a `dynamic` call,
the same information is looked up from a method table at run time. Whether this lookup itself
is a step or not is a question the run counter cannot measure, because the counter only counts
how many times the `record` call passed, not how many steps were taken to reach that call. This
question was already addressed in the Object-Oriented Java course; this lesson does not ask it
again, it only shows that the two forms leave the **body count** equal.

This distinction has a concrete consequence: you cannot answer "does this source use dynamic
dispatch or static dispatch" just by looking at the run ledger. When the `static-dispatch` and
`dynamic-dispatch` rows sit side by side, both being two could lead a reader to a wrong
conclusion — "if they are doing the same thing, it does not matter which I choose." But it does
matter: the `list-body` row could only exist because the `dyn Label` form satisfies the object
safety condition; the same list could never be built with the bound `static` uses. Even though
the run count does not distinguish the two forms, which **notations** are possible still
diverges along a separate axis — this lesson shows that axis is seen in the code itself, not in
the count.

The course's first four lessons each asked, in turn, whether everything placed around a body —
a default body, a bound, an associated type, a call form — touches the run count. The answer
came out the same all four times: it does not. However the body is found (from a type parameter
or from a method table), it runs unavoidably everywhere it is called, and this is independent of
whichever syntax decision is layered on top of it.

## Summary

- Static dispatch (`<T: Label>`) resolves at compile time, dynamic dispatch (`&dyn Label`)
  resolves at run time from a method table.
- Both forms fall into the every-time class and give the exact same run count: two and two —
  the call form does not change how many times the body is triggered.
- A trait object (`Box<dyn Label>`) can hold different concrete types in a single collection;
  static dispatch cannot, because a type parameter locks to a single concrete type.
- Object safety is the condition required for a trait to be usable with `dyn`: methods that
  return `Self` by value or carry their own type parameter break this condition.
- The same run count does not mean the same cost; whether the call is found at compile time or
  at run time is a separate question that cannot be measured by the run counter.

## Next Step

In this lesson, the body itself was always hand-written — the `name` method's body came either
from the trait's default or from `Custom`'s own writing. A body can also be produced without
ever being hand-written. The next lesson shows how a body whose lines never appear in the
source runs at run time, and why this running cannot be counted by the run counter.
