| name | rust-syntax-trait-objects |
| description | Use when the user needs `dyn Trait`, asks why a trait is not object-safe, chooses between `dyn Trait` and `impl Trait`, encounters upcasting (1.86), or uses precise capturing in trait definitions (1.87). Prevents picking `dyn Trait` when monomorphization is cheaper, missing object-safety rules, or using pre-1.86 upcasting work-arounds. Covers: `dyn Trait` type-erased dispatch, vtable representation, object-safety rules (no generic methods, no Sized self bound), `impl Trait` vs `dyn Trait` decision, trait upcasting `&dyn Sub` to `&dyn Super` (1.86), precise capturing `+ use<'a, T>` in trait definitions (1.87). Keywords: "dyn Trait", trait object, vtable, "object safety", "not object safe", E0038, "impl Trait", RPIT, "trait upcasting", "&dyn Sub", "as &dyn Super", "precise capturing", "+ use<>", "dyn dispatch", "static dispatch", "dynamic dispatch", "method resolution", "Box<dyn>", "Arc<dyn>", "&dyn", "monomorphization vs dyn", virtual.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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 Trait when the function returns or accepts ONE concrete type per call site. NEVER use dyn Trait just to "be flexible"; monomorphization costs binary size but is faster at the call site.
- ALWAYS use
dyn Trait when 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 Trait over Box<dyn Trait> when ownership transfer is not required. Box<dyn Trait> allocates on the heap; &dyn Trait is 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: Sized on 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 Sub to &dyn Super) directly in Rust 1.86+ . NEVER write an as_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 allow use<> 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.
Sized MUST NOT be a supertrait. The trait MUST NOT require Self: 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> where P is one of these.
- It does not use
Self outside of the receiver type (no -> Self, no fn foo(other: Self)).
- It is not an
async fn and 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, AsyncFnOnce are 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);
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]);
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;
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 {
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]] for impl Trait argument position and generic bounds.
[[rust-syntax-gats]] for generic associated types and lending iterators.
[[rust-syntax-lifetimes]] for variance of dyn Trait in 'a and T, 'static bound, 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 (the dyn keyword)
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)