rust-syntax-trait-objects
Trait objects (dyn Trait) provide runtime polymorphism through type erasure and a vtable. They are the dynamic-dispatch counterpart to impl Trait (static dispatch via monomorphization). This skill covers the vtable layout, the dyn-compatibility rules formerly known as object safety, the decision between dyn and impl, trait upcasting stabilized in 1.86, and precise capturing inside trait definitions stabilized in 1.87.
Quick Reference
Core rules (deterministic)
- ALWAYS use
impl Traitwhen the function returns or accepts ONE concrete type per call site. NEVER usedyn Traitjust to "be flexible"; monomorphization costs binary size but is faster at the call site. - ALWAYS use
dyn Traitwhen storing heterogeneous values in a single collection (Vec<Box<dyn Drawable>>) or when a function must accept values of different concrete types at the same call site. - ALWAYS prefer
&dyn TraitoverBox<dyn Trait>when ownership transfer is not required.Box<dyn Trait>allocates on the heap;&dyn Traitis two stack words (data pointer + vtable pointer). - NEVER add a generic method to a trait that must remain dyn-compatible. A generic method requires a separate vtable entry per instantiation; the vtable has fixed shape, so the compiler rejects it (E0038).
- NEVER include
where Self: Sizedon a method you want dispatched through the vtable; that bound opts the method out of the vtable, making it callable only on concrete types. - ALWAYS exploit trait upcasting (
&dyn Subto&dyn Super) directly in Rust 1.86+ . NEVER write anas_supertrait()workaround method; it was the pre-1.86 workaround and is now obsolete. - ALWAYS add
+ use<'a, T>to an RPIT inside a trait definition in Rust 1.87+ when you need to constrain or exclude captured generics. NEVER rely on pre-1.87 syntax that did not allowuse<>in trait positions.
Decision tree: dyn Trait vs impl Trait
Does the function or storage need to hold values of MULTIPLE different concrete types simultaneously?
YES -> dyn Trait (with Box, Arc, Rc, or & as appropriate pointer)
NO -> Is the type expressible in the signature (no generic explosion)?
YES -> impl Trait (return position) or generic <T: Trait> (argument)
NO -> dyn Trait
Are you returning from a closure / iterator / async chain producing ONE inferred concrete type?
YES -> impl Trait
Are you returning DIFFERENT concrete types from different match arms?
YES -> Box<dyn Trait> (or Either crate for two-arm cases)
Pointer choice for dyn Trait
| Pointer | When to use | Cost |
|---|---|---|
&dyn T |
Borrowed, scope-limited use, fastest | 2 words on stack, no allocation |
&mut dyn T |
Borrowed mutable access | 2 words on stack |
Box<dyn T> |
Single owner, heap-allocated | 1 heap alloc + 1 word for box ptr |
Rc<dyn T> |
Shared ownership, single-threaded | 1 heap alloc + atomic-free refcount |
Arc<dyn T> |
Shared ownership, multi-threaded (requires T: Send + Sync if shared across threads) |
1 heap alloc + atomic refcount |
Dyn-compatibility rules (verbatim from the Reference)
A trait is dyn-compatible if and only if ALL of the following hold (https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility):
- All supertraits MUST be dyn-compatible.
SizedMUST NOT be a supertrait. The trait MUST NOT requireSelf: Sized.- The trait MUST NOT have any associated constants.
- The trait MUST NOT have any associated types with generics.
- All associated functions MUST be either dispatchable from a trait object or be explicitly non-dispatchable.
A method is dispatchable if it satisfies all of:
- It has no type parameters (lifetime parameters are allowed).
- Its receiver is one of
&Self,&mut Self,Box<Self>,Rc<Self>,Arc<Self>,Pin<P>wherePis one of these. - It does not use
Selfoutside of the receiver type (no-> Self, nofn foo(other: Self)). - It is not an
async fnand does not return-> impl Trait.
A method is non-dispatchable if it has an explicit where Self: Sized bound, which omits it from the vtable but allows it to exist as a generic method on concrete types.
AsyncFn,AsyncFnMut,AsyncFnOnceare NOT dyn-compatible.
If any rule fails the compiler emits E0038 with the precise reason.
Vtable layout (informational, not stable ABI)
&dyn Trait
+-----------------+
| data pointer | --> the value itself (any concrete type)
+-----------------+
| vtable pointer | --> static vtable for (Trait, ConcreteType)
+-----------------+
vtable for (Trait, ConcreteType)
+--------------------+
| destructor (drop) | --> ConcreteType::drop_in_place
+--------------------+
| size of value |
+--------------------+
| align of value |
+--------------------+
| method 0 fn ptr | --> ConcreteType::method_0
+--------------------+
| method 1 fn ptr |
+--------------------+
| ... |
+--------------------+
The vtable is generated once per (Trait, ConcreteType) pair, lives in static memory, and is shared by every trait object of that pairing. The data pointer is the only varying field at runtime.
Pattern: basic dyn Trait storage
ALWAYS reach for Box<dyn Trait> when a Vec must hold heterogeneous implementors.
trait Drawable {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Drawable for Circle { fn draw(&self) { println!("circle r={}", self.radius); } }
impl Drawable for Square { fn draw(&self) { println!("square s={}", self.side); } }
fn main() {
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }
}
Pattern: &dyn Trait argument avoids allocation
NEVER allocate for a borrowed-only callsite. &dyn Trait is two stack words and one vtable indirection per method call.
fn render(d: &dyn Drawable) {
d.draw();
}
let c = Circle { radius: 1.0 };
render(&c); // no Box, no heap allocation
Pattern: opt-out non-dispatchable method with where Self: Sized
When a generic method must coexist with a dyn-compatible trait, opt it OUT of the vtable.
trait Storage {
fn save(&self, data: &[u8]); // dispatchable, in vtable
// NOT dispatchable; only callable on concrete types, NOT via &dyn Storage
fn typed_save<T: serde::Serialize>(&self, value: &T) where Self: Sized {
let bytes = serde_json::to_vec(value).unwrap();
self.save(&bytes);
}
}
This pattern keeps &dyn Storage valid while letting concrete impls use the generic helper.
Pattern: trait upcasting (Rust 1.86+)
ALWAYS use direct upcasting in 1.86+ for converting &dyn Sub into &dyn Super. The release note (https://blog.rust-lang.org/2025/04/03/Rust-1.86.0/) : "Trait upcasting enables coercing trait object references to supertrait references." This works for &dyn, &mut dyn, Box<dyn>, Rc<dyn>, Arc<dyn>, and raw *const dyn / *mut dyn.
trait Animal {
fn name(&self) -> &str;
}
trait Dog: Animal {
fn bark(&self);
}
struct Labrador;
impl Animal for Labrador { fn name(&self) -> &str { "Buddy" } }
impl Dog for Labrador { fn bark(&self) { println!("woof"); } }
fn print_name(a: &dyn Animal) { println!("{}", a.name()); }
fn use_dog(d: &dyn Dog) {
d.bark();
let a: &dyn Animal = d; // upcasting in 1.86+ : implicit coercion
print_name(a);
}
Pre-1.86 the standard workaround was an as_animal(&self) -> &dyn Animal method on Dog. NEVER add such a method in 1.86+ code; it is dead weight.
Pattern: precise capturing in trait definitions (Rust 1.87+)
ALWAYS write + use<...> on RPITIT (return-position impl Trait in trait) returns in Rust 1.87+ when generic-capture must be constrained. The 1.87 release (https://blog.rust-lang.org/2025/05/15/Rust-1.87.0/) lifted the restriction that prevented use<> in trait definitions; the syntax was already stable for free functions since 1.82.
trait Producer {
// Captures only 'a, NOT T or other in-scope generics
fn items<'a>(&'a self) -> impl Iterator<Item = u32> + use<'a, Self>;
}
struct Repo<T> { inner: Vec<T> }
impl<T: Copy + Into<u32>> Producer for Repo<T> {
fn items<'a>(&'a self) -> impl Iterator<Item = u32> + use<'a, Self> {
self.inner.iter().copied().map(Into::into)
}
}
Without use<>, edition-2024 RPITIT captures all in-scope generics including unused ones, which can over-constrain callers. The impl_trait_overcaptures lint flags these and cargo fix --edition inserts use<> where preservation is needed (see [[rust-syntax-edition-2024]]).
Cross-references
[[rust-syntax-traits]]for trait definition syntax, blanket impls, marker traits.[[rust-syntax-generics]]forimpl Traitargument position and generic bounds.[[rust-syntax-gats]]for generic associated types and lending iterators.[[rust-syntax-lifetimes]]for variance ofdyn Traitin'aandT,'staticbound, precise capturing rules.
Reference Links
- Dyn-compatibility rules:
references/methods.md(vtable layout + complete rule enumeration). - End-to-end examples:
references/examples.md(upcasting, RPITIT use<>, hot-path benchmark hint). - Anti-patterns and fixes:
references/anti-patterns.md(E0038 cases, allocation waste, lifetime defaulting traps).
Sources verified:
https://doc.rust-lang.org/reference/items/traits.html(dyn-compatibility)https://doc.rust-lang.org/reference/types/trait-object.html(trait-object types)https://doc.rust-lang.org/std/keyword.dyn.html(thedynkeyword)https://blog.rust-lang.org/2025/04/03/Rust-1.86.0/(trait upcasting stabilization)https://blog.rust-lang.org/2025/05/15/Rust-1.87.0/(precise capturing in trait definitions)https://blog.rust-lang.org/2024/10/17/Rust-1.82.0/(precise capturing for free functions)https://doc.rust-lang.org/error_codes/E0038.html(object-safety errors)