Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-636 · Case file with fixtures · Case 608 of 694 · Compiler evidence

Trait Objects Need the Explicit dyn Marker

dyn marks runtime type erasure and dynamic dispatch. Add it for an erased interface, or use impl Trait or a named generic when the caller and compiler should preserve a concrete type.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Older bare-trait syntax hid whether the interface wanted runtime erasure or a caller-selected concrete generic type.
First discriminating check
Choose dynamic dispatch deliberately and write &dyn Trait, or preserve concrete identity with impl Trait or a named generic.

A trait describes behaviour; it is not by itself one concrete sized type. When I want a runtime-erased value implementing that behaviour, Rust asks me to say dyn Trait. The failing fixture uses &Store in edition 2024 and receives E0782.

dyn makes the dispatch model visible

&dyn Store is a trait object behind a shared borrow. The handle contains a data pointer and metadata used to dispatch methods for the concrete implementation at runtime. Different calls may receive different implementing types through the same interface.

The official E0782 explanation notes that the explicit marker removes ambiguity with a hypothetical concrete type named Foo. Bare trait objects were accepted in older editions but became an error beginning with edition 2021.

The Reference defines trait object types, including lifetime and auto-trait bounds.

Three similar signatures choose different owners

fn inspect(value: &dyn Store) lets the caller pass any dyn-compatible implementation and uses dynamic dispatch. fn inspect(value: &impl Store) is shorthand for a generic parameter in argument position; each call preserves a concrete type and normally uses static dispatch. fn inspect<S: Store>(value: &S) names that generic type so the signature can relate it elsewhere.

I choose based on architecture. Trait objects help plugin boundaries and heterogeneous collections. Generics help reusable algorithms where concrete type identity and optimisation matter. Neither is automatically more senior or more performant; the constraints make the decision.

The repaired fixture intentionally accepts &dyn Store and demonstrates one concrete memory store.

The pointer form expresses ownership

&dyn Trait borrows an existing implementation. Box<dyn Trait> transfers unique ownership of a heap allocation. Arc<dyn Trait + Send + Sync> can share a thread-safe implementation. These are not interchangeable wrappers around syntax.

I use the least ownership required. A borrowed service parameter avoids allocation and hides no shutdown owner. A boxed component can be replaced and dropped by its container. An Arc is appropriate only when shared ownership is real; it can otherwise obscure lifetime and cleanup.

Object lifetime bounds also matter. Writing 'static means the erased object contains no non-static borrows, not that the handle lives forever. I avoid forcing clones to satisfy an alias with an unnecessarily strong default.

The trait must be dyn-compatible

Some trait methods cannot be called through erasure because they require concrete Self knowledge or introduce method-level generics. Adding dyn can uncover these rules after E0782.

I separate object-safe behaviour from construction or generic extension methods where needed. Associated types are specified in the object type when required, such as dyn Iterator<Item = Record>.

The Book’s trait object chapter shows heterogeneous behaviour behind one interface. I keep these interfaces cohesive because every added requirement narrows implementors and increases mocking surface.

Edition migrations should expose intent

Automated fixes can insert dyn throughout an older crate. I still review whether every site truly wants dynamic dispatch. Some bare-trait syntax may have survived from a time before the code needed performance or ownership clarity.

Public signatures gain readability from explicit erasure. Callers can see allocation and dispatch decisions when combined with Box, Arc, or a borrow. Benchmarks focus on actual hot paths because dynamic calls can be insignificant beside I/O, while generics can increase binary size through monomorphisation.

Tests use at least two implementations when heterogeneity is the reason for erasure. Otherwise a trait object may be complexity without a demonstrated variation point.

My E0782 checklist

  • Is a trait name being used where a type is expected without dyn?
  • Does this boundary truly need runtime erasure and heterogeneous implementations?
  • Would impl Trait or a named generic preserve useful concrete identity?
  • Does the pointer express borrow, unique ownership, or shared ownership honestly?
  • Which lifetime, Send, Sync, and associated-type bounds are required?
  • Is the trait dyn-compatible, and are all object methods actually needed?
  • Did an edition migration insert syntax without reviewing dispatch intent?
  • Do tests demonstrate the variation point and measure relevant hot paths?

The core principle is that a trait object is a deliberate runtime representation, not an implicit spelling of a trait. E0782 makes erasure visible. I add dyn only after choosing dispatch, ownership, lifetime, and extensibility together.