A trait object (dyn Trait) erases a value's concrete type, keeping only
that it implements a given trait — and resolves which implementation's
methods to call at runtime rather than compile time. Since its size isn't
known at compile time, a trait object almost always appears behind a
pointer: &dyn Trait, Box<dyn Trait>, Rc<dyn Trait>. For instance, a
Vec<Box<dyn Shape>> can hold Circles and Squares side by side, and
calling .area() on each element resolves to the right implementation at
runtime via a vtable.
This is what lets a single collection hold values of genuinely different
concrete types, as long as they all implement the same trait — something
generics alone can't do, since a
generic function/type is monomorphized into one specialized version per
concrete type, not a single version that handles several types
interchangeably at once.
The mechanism is a vtable: a small table of function pointers built for
each concrete type's implementation, attached alongside the data pointer
whenever a trait object is created. Calling a method through dyn Trait
means an extra indirection through that table — a small, real runtime
cost compared to static dispatch,
which is the tradeoff being made in exchange for the ability to mix
different concrete types behind one interface. This is Rust's nearest
analogue to interface-typed references in Java/C#, though notably
without null being a possible value.
Basic usage example
trait Shape {
fn area(&self) -> f64;
}
struct Circle;
impl Shape for Circle { fn area(&self) -> f64 { 3.14 } }
struct Square;
impl Shape for Square { fn area(&self) -> f64 { 4.0 } }
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle), Box::new(Square)];
// <- `dyn Shape` erases the concrete type; each element can be a different one
for s in &shapes {
println!("{}", s.area()); // resolved at runtime via a vtable
}
Best practices & deeper information
Scenario
Runtime polymorphism
An audio effect chain needs to apply an arbitrary, user-configured mix of
effects in sequence — the exact set and order aren't known until runtime,
which rules out a fixed enum or a single generic function.
Why this way: the chain's length and composition are decided at
runtime (config, user input), so no single generic instantiation could
cover it — this is exactly the heterogeneous-collection case the
Rust Book
uses trait objects for.
Scenario
Designing a public API
A plugin-style registry is a natural Vec<Box<dyn Trait>> API: the crate
owns the collection and dispatch logic, while callers register any type
implementing the trait.
Why this way: designing Command to stay object-safe (no generic
methods, no returning Self) is what makes this API possible at all —
the API Guidelines' C-OBJECT
calls out object safety as a deliberate design goal for traits meant to be
used this way, and the shape matches the
command pattern
in the Rust Design Patterns book.
Explanation
Embedded support: Full
For the mechanics of &dyn Trait needing no heap, Box<dyn Trait>
needing alloc, and the flash-vs-dispatch-predictability tradeoff against
generics, see dyn — that page covers
the mechanism in full and this page doesn't re-derive it. What's worth
covering here is the design-level question: given that generics and
monomorphization are the default choice in embedded Rust (predictable
inlined calls, no vtable indirection), when does embedded code reach for
dynamic dispatch at all?
Two situations come up genuinely often. The first is a heterogeneous
collection of different peripheral types that must live together in one
array or list — a board-support struct holding several distinct sensor
types that all need to be read in a loop, where a generic function can't
help because a single monomorphized instantiation only ever handles one
concrete type, not a mix. The second is plugin-style, configuration-driven
registration — a board's revision or a runtime configuration decides
which optional peripherals are actually present, so the code iterating
over them can't be generic over a fixed, compile-time-known set of types.
In both cases, &dyn Trait (or, for owned storage, Box<dyn Trait> once
alloc is configured) gives one uniform type to store and iterate,
without requiring every element to be the same concrete type. Outside
those two shapes, embedded code defaults to generics — the common case
by a wide margin, since most driver code is written against one concrete
type parameter per call site, decided once at compile time.
Basic usage example
trait Sensor {
fn read_raw(&self) -> u16;
}
struct Thermistor;
impl Sensor for Thermistor { fn read_raw(&self) -> u16 { 512 } }
struct Photodiode;
impl Sensor for Photodiode { fn read_raw(&self) -> u16 { 128 } }
let thermistor = Thermistor;
let photodiode = Photodiode;
let sensors: [&dyn Sensor; 2] = [&thermistor, &photodiode]; // <- heterogeneous, heap-free: fixed-size array of trait objects
for s in sensors {
let _raw = s.read_raw();
}
Best practices & deeper information
Scenario
Runtime polymorphism
A board's set of installed sensors varies by revision — one revision
carries a thermistor and a photodiode, another adds a humidity sensor —
so the firmware that polls "whatever sensors this board has" needs one
type it can iterate over, without a fixed enum or a generic function per
board revision.
trait Sensor {
fn read_raw(&self) -> u16;
}
fn poll_all(sensors: &[&dyn Sensor]) -> u32 { // <- one signature, any mix of sensor types, no heap
sensors.iter().map(|s| s.read_raw() as u32).sum()
}
Why this way: the exact sensor mix is a board-configuration fact, not
something a single generic instantiation can express — &dyn Sensor
gives one type for the collection while costing nothing but the vtable
indirection on each read, with no allocator involved at all.
Scenario
Designing a public API
A board-support crate that lets applications register an arbitrary
number of optional peripheral drivers at startup needs a fixed-capacity,
allocator-free registry — a plain array of trait objects, sized to the
board's maximum, rather than a heap-growing Vec.
trait Peripheral {
fn init(&mut self);
}
pub struct Registry<'a> {
peripherals: [Option<&'a mut dyn Peripheral>; 4], // <- fixed-capacity, no allocator: registry sized at compile time
}
impl<'a> Registry<'a> {
pub fn init_all(&mut self) {
for p in self.peripherals.iter_mut().flatten() {
p.init();
}
}
}
Why this way: a bare-metal target commonly has no #[global_allocator]
configured at all, so a registry API built around Vec<Box<dyn Trait>>
would be unusable there; sizing the registry to a fixed maximum at
compile time keeps the API allocator-free while still accepting any
concrete Peripheral implementation.
Scenario
Boxing and heap allocation
On a target that does have alloc configured (an ESP32 running
esp-idf, for instance), a one-time, build-at-startup plugin list is a
reasonable place for Box<dyn Trait> — the allocation happens once
during init, not on a hot path, so the flexibility is worth the one-time
heap cost.
// AVOID: Box<dyn Trait> on a bare-metal target with no configured allocator — this won't compile
// let drivers: Vec<Box<dyn Peripheral>> = vec![Box::new(imu), Box::new(display)];
// PREFER: on a target with `alloc` configured, a startup-only Box<dyn Trait> list is fine
extern crate alloc;
use alloc::{boxed::Box, vec::Vec};
fn build_drivers(imu: impl Peripheral + 'static, display: impl Peripheral + 'static) -> Vec<Box<dyn Peripheral>> {
let mut drivers: Vec<Box<dyn Peripheral>> = Vec::new();
drivers.push(Box::new(imu)); // <- heap allocation, but only once, at startup
drivers.push(Box::new(display));
drivers
}
Why this way: whether Box<dyn Trait> is available at all is a
per-target fact, not a matter of taste — on targets without alloc,
&dyn Trait over a fixed-size array is the only option; where alloc is
configured, restricting Box<dyn Trait> use to one-time startup code
avoids paying an allocation cost on any latency-sensitive path.