SuperAPI AI Directory

rust-syntax-trait-objects

Coding & development

View repository

Updated 10-15 22:18

About this skill

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)

  1. 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.
  2. 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.
  3. 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).
  4. 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).
  5. 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.
  6. 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.
  7. 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):

  1. All supertraits MUST be dyn-compatible.
  2. Sized MUST NOT be a supertrait. The trait MUST NOT require Self: Sized.
  3. The trait MUST NOT have any associated constants.
  4. The trait MUST NOT have any associated types with generics.
  5. 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.

  1. 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); // 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]] 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)