How Rust calls trait methods, what each form costs, and the complete set of rules that decide whether a trait
can be used as dyn Trait ("object safety", renamed "dyn compatibility" in the Reference in 2024; newer
compilers say the trait X is not dyn compatible, older ones the trait X cannot be made into an object,
both error[E0038]).
Contents
- Three ways to accept "something that implements Trait"
- What monomorphization costs
- What a trait object is
- Choosing: decision list
- Dyn compatibility rules
- Fixing E0038 pattern by pattern
- Clone for trait objects
- Comparing and hashing trait objects
- Trait upcasting and downcasting with Any
- Lifetimes and auto traits on trait objects
- Enum dispatch instead of dyn
- Returning impl Trait
- Review checklist
Three ways to accept "something that implements Trait"
use std::fmt::Display;
// 1. Named generic parameter: callers may write render::<u32>(..); bounds can be reused.
pub fn render<T: Display>(value: T) -> String {
format!("<{value}>")
}
// 2. impl Trait in argument position (Rust 1.26): an anonymous generic parameter.
// Same machine code as (1); the parameter cannot be turbofished.
pub fn render_impl(value: impl Display) -> String {
format!("<{value}>")
}
// 3. Trait object: one compiled function, dynamic dispatch through a vtable.
pub fn render_dyn(value: &dyn Display) -> String {
format!("<{value}>")
}
fn main() {
assert_eq!(render(5), "<5>");
assert_eq!(render_impl("x"), "<x>");
assert_eq!(render_dyn(&2.5), "<2.5>");
}| Property | T: Trait | impl Trait (arg) | &dyn Trait / Box<dyn Trait> |
|---|---|---|---|
| Dispatch | static | static | dynamic (vtable) |
| Inlining across the call | yes | yes | only if devirtualized |
| Copies of the function | one per type | one per type | one |
| Heterogeneous collections | no | no | yes |
| Turbofish | yes | no (1.63+: other explicit params may be given) | n/a |
| Trait must be dyn compatible | no | no | yes |
Unsized values (str, [T]) | with T: ?Sized and a reference | &(impl Trait + ?Sized) | yes |
| Name the type in a struct field | struct S<T: Trait> { t: T } | no | Box<dyn Trait> |
impl Trait in argument position is sugar. Use the named form when the parameter appears twice
(fn eq<T: PartialEq>(a: T, b: T)), in a where clause, or when callers need turbofish.
What monomorphization costs
Every distinct T gets its own copy of a generic function (and of every generic function it calls). That is
what makes iterators and closures zero-cost, but large generic functions instantiated for many types inflate
binary size and compile time. Two standard mitigations:
use std::io;
use std::path::Path;
// 1. Generic shell, non-generic inner function (std::fs::read_to_string does exactly this).
pub fn read_config<P: AsRef<Path>>(path: P) -> io::Result<String> {
fn inner(path: &Path) -> io::Result<String> {
let text = std::fs::read_to_string(path)?;
Ok(text.trim().to_owned())
}
inner(path.as_ref())
}
// 2. Accept &mut dyn for heavy internals that do not need speed.
pub fn write_report(out: &mut dyn io::Write, rows: &[(String, u64)]) -> io::Result<()> {
for (name, n) in rows {
writeln!(out, "{name:<20}{n:>10}")?;
}
Ok(())
}
fn main() -> io::Result<()> {
let mut buf = Vec::new();
write_report(&mut buf, &[("docs".into(), 3)])?;
assert!(String::from_utf8(buf).unwrap().starts_with("docs"));
Ok(())
}Measure with cargo bloat --crates / cargo llvm-lines before optimizing; most code never needs this.
What a trait object is
&dyn Trait and Box<dyn Trait> are fat pointers: a data pointer plus a pointer to a vtable. The vtable
holds the concrete type's size, alignment, drop_in_place, and one function pointer per dispatchable
method (including supertrait methods). The concrete type is erased, so:
- Every method callable through the vtable must have a single, non-generic signature (one function pointer).
- The method cannot mention the erased type
Selfexcept through the pointer it is called on. - The value itself is unsized (
dyn Trait: ?Sized), so it always lives behind a pointer:&,&mut,Box,Rc,Arc,Pin<Box<..>>.
Choosing: decision list
- Do callers need to store different implementors together, or choose one at runtime (config, plugins)?
Use
dyn. Otherwise prefer generics. - Is the set of implementors closed and known to you? Consider an
enum(exhaustive match, no vtable). - Is this a hot inner loop (per element, per byte)? Generics, so the call can inline.
- Is it called a few times per request (a storage backend, an HTTP client)?
dyncosts nothing measurable and keeps signatures and compile times small:Arc<dyn Storage + Send + Sync>is a fine default for application-level dependency injection. - Library API: offer generics at the edges (
impl AsRef<Path>,impl IntoIterator) and keep the trait dyn compatible so users can choose either form.
Dyn compatibility rules
A trait is dyn compatible when all of these hold:
| Rule | Example that breaks it |
|---|---|
No Sized supertrait (directly or through supertraits) | trait Shape: Clone (Clone: Sized), trait Cfg: Default, trait P: FromStr |
No supertrait that uses Self as a type parameter | trait Key: PartialEq (PartialEq<Rhs = Self>), Eq, PartialOrd, Ord |
| All supertraits dyn compatible | trait Plugin: Serialize where Serialize has a generic method |
| No associated constants | const ID: u32; |
| No generic associated types | type Item<'a> where Self: 'a; |
Every method is either dispatchable or marked where Self: Sized | see below |
A method is dispatchable when:
- it has a receiver:
&self,&mut self,self: Box<Self>,self: Rc<Self>,self: Arc<Self>,self: Pin<P>wherePis one of those; - it has no type or const parameters (lifetime parameters are fine), and no
impl Traitarguments; Selfappears only in the receiver: not in other argument types, not in the return type (Self::Assocprojections are fine);- it does not return an opaque type: no
async fn, no-> impl Trait(RPITIT); - it has no
where Self: Sizedbound (that is what makes it explicitly non-dispatchable).
Methods with where Self: Sized do not appear in the vtable and cannot be called on dyn Trait, but are
callable on every concrete type. That is the escape hatch for constructors, generic helpers and
Self-returning methods.
Associated types are allowed, but a trait object must name them: Box<dyn Iterator<Item = u32>>;
Box<dyn Iterator> is error[E0191].
Trait-level generic parameters are allowed: dyn Codec<Json> is a type. Each instantiation is a different
trait object type.
Fixing E0038 pattern by pattern
use std::fmt::Debug;
pub trait Shape: Debug {
// dispatchable
fn area(&self) -> f64;
fn name(&self) -> &str;
// constructor: no receiver -> needs Self: Sized
fn unit() -> Self
where
Self: Sized;
// returns Self -> needs Self: Sized ...
fn scaled(&self, k: f64) -> Self
where
Self: Sized;
// ... and offer a dyn-friendly twin
fn scaled_boxed(&self, k: f64) -> Box<dyn Shape>;
// generic method -> needs Self: Sized, or make it non-generic
fn describe_with<F: Fn(&str) -> String>(&self, f: F) -> String
where
Self: Sized,
{
f(self.name())
}
fn describe_dyn(&self, f: &dyn Fn(&str) -> String) -> String {
f(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Square(f64);
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
fn name(&self) -> &str {
"square"
}
fn unit() -> Self {
Square(1.0)
}
fn scaled(&self, k: f64) -> Self {
Square(self.0 * k)
}
fn scaled_boxed(&self, k: f64) -> Box<dyn Shape> {
Box::new(self.scaled(k))
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square::unit()), Square(2.0).scaled_boxed(2.0)];
let total: f64 = shapes.iter().map(|s| s.area()).sum();
assert_eq!(total, 17.0);
assert_eq!(shapes[1].describe_dyn(&|n| n.to_uppercase()), "SQUARE");
}| Offending item | Options, best first |
|---|---|
fn new(..) -> Self | where Self: Sized; or move construction to a separate factory trait |
fn merge(&self, other: &Self) | where Self: Sized; or other: &dyn Trait and downcast if the types must match |
fn process<T: Serialize>(&self, v: &T) | take &dyn erased_serde::Serialize-style erased input, bytes, or a concrete enum; or where Self: Sized |
fn iter(&self) -> impl Iterator<Item = u8> | -> Box<dyn Iterator<Item = u8> + '_> |
async fn get(&self) | #[async_trait], or -> Pin<Box<dyn Future<Output = T> + Send + '_>> (see async-and-advanced-generics.md) |
const VERSION: u32; | fn version(&self) -> u32 |
trait T: Clone | remove the supertrait, add box_clone (below) |
trait T: PartialEq | fn key(&self) -> Key and compare keys, or fn eq_dyn(&self, other: &dyn Any) -> bool |
GAT type Item<'a> | separate "lending" trait used only generically |
Test it once so it stays true: a function that takes &dyn Trait in the crate (or a doc test) turns a future
accidental break into a compile error in your CI, not in your users'.
#[allow(dead_code)]
fn _assert_dyn_compatible(_: &dyn std::fmt::Debug) {}Clone for trait objects
Clone requires Sized, so Box<dyn Trait> is not Clone by default. Add an object-safe clone method and
implement Clone for the box:
pub trait Filter: FilterClone {
fn keep(&self, x: i32) -> bool;
}
pub trait FilterClone {
fn box_clone(&self) -> Box<dyn Filter>;
}
// Blanket impl: every Clone + 'static filter gets box_clone for free.
impl<T: Filter + Clone + 'static> FilterClone for T {
fn box_clone(&self) -> Box<dyn Filter> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Filter> {
fn clone(&self) -> Self {
self.box_clone()
}
}
#[derive(Clone)]
struct Even;
impl Filter for Even {
fn keep(&self, x: i32) -> bool {
x % 2 == 0
}
}
fn main() {
let a: Box<dyn Filter> = Box::new(Even);
let b = a.clone();
assert!(b.keep(4));
}The dyn-clone crate packages exactly this (DynClone supertrait plus clone_trait_object!).
Comparing and hashing trait objects
PartialEq cannot be a supertrait of a dyn trait. Compare through Any:
use std::any::Any;
pub trait Node: Any {
fn eq_dyn(&self, other: &dyn Node) -> bool;
}
impl<T: Any + PartialEq> Node for T {
fn eq_dyn(&self, other: &dyn Node) -> bool {
// Rust 1.86+: &dyn Node upcasts to &dyn Any.
let other: &dyn Any = other;
other.downcast_ref::<T>().is_some_and(|o| self == o)
}
}
impl PartialEq for dyn Node {
fn eq(&self, other: &Self) -> bool {
self.eq_dyn(other)
}
}
fn main() {
let a: Box<dyn Node> = Box::new(3_u8);
let b: Box<dyn Node> = Box::new(3_u8);
let c: Box<dyn Node> = Box::new("3");
assert!(*a == *b);
assert!(*a != *c);
}On compilers older than 1.86 add fn as_any(&self) -> &dyn Any; to the trait and implement it as self.
Option::is_some_and needs Rust 1.70.
Trait upcasting and downcasting with Any
Since Rust 1.86 a trait object coerces to a trait object of any supertrait:
use std::any::Any;
use std::sync::Arc;
trait Named {
fn name(&self) -> String;
}
trait Plugin: Named + Any + Send + Sync {
fn run(&self) -> u32;
}
struct Echo;
impl Named for Echo {
fn name(&self) -> String {
"echo".into()
}
}
impl Plugin for Echo {
fn run(&self) -> u32 {
7
}
}
fn main() {
let p: Arc<dyn Plugin> = Arc::new(Echo);
let named: Arc<dyn Named> = p.clone(); // upcast (1.86+)
assert_eq!(named.name(), "echo");
let any: &dyn Any = &*p; // upcast to Any
assert!(any.downcast_ref::<Echo>().is_some());
assert_eq!(p.run(), 7);
}Notes:
Anyrequires'static: plugins holding borrowed data cannot use it.Box<dyn Any>::downcast::<T>()returnsResult<Box<T>, Box<dyn Any>>;Arc<dyn Any + Send + Sync>hasdowncasttoo.- Downcasting is a smell in most designs. Prefer adding the needed method to the trait.
Lifetimes and auto traits on trait objects
| Written | Means |
|---|---|
Box<dyn Trait> | Box<dyn Trait + 'static> |
&'a dyn Trait | &'a (dyn Trait + 'a) |
Box<dyn Trait + 'a> | may borrow data living for 'a |
Arc<dyn Trait> used across threads | needs dyn Trait + Send + Sync, or trait Trait: Send + Sync |
Only auto traits (Send, Sync, Unpin, UnwindSafe, RefUnwindSafe) may be added with +. Two
regular traits are error[E0225]; define a combined trait:
use std::io::{Read, Write};
pub trait Stream: Read + Write + Send {}
impl<T: Read + Write + Send> Stream for T {}
pub fn handshake(conn: &mut dyn Stream) -> std::io::Result<()> {
conn.write_all(b"HELLO\n")?;
let mut buf = [0_u8; 3];
conn.read_exact(&mut buf)?;
Ok(())
}Putting Send + Sync as supertraits (trait Storage: Send + Sync) forces every implementor to be
thread-safe and lets you write Arc<dyn Storage> without repeating bounds. Leave them off when
single-threaded implementations (Rc, RefCell) are legitimate, and write dyn Storage + Send + Sync at
the use sites that need it.
A method returning a borrowed trait object needs the right lifetime:
fn items(&self) -> Box<dyn Iterator<Item = &Item> + '_>; without + '_ the box defaults to 'static and the
borrow of self is rejected.
Enum dispatch instead of dyn
When you own every implementor:
pub enum Backend {
Memory(Vec<u8>),
File(std::path::PathBuf),
}
impl Backend {
pub fn describe(&self) -> String {
match self {
Backend::Memory(v) => format!("memory ({} bytes)", v.len()),
Backend::File(p) => format!("file {}", p.display()),
}
}
}Pros: no allocation, inlinable, exhaustive matching, values are Sized and can be Clone/PartialEq.
Cons: closed set; adding a variant to a public enum is breaking unless #[non_exhaustive]. The
enum_dispatch crate generates the forwarding impl Trait for Enum when you want both.
Returning impl Trait
-> impl Trait returns one concrete type chosen by the function body, hidden from the caller.
- One function can return only one concrete type: two closures in
if/elseare two types. UseBox<dyn Fn..>,either::Either, or an enum. - Auto traits leak: callers can rely on the hidden type being
Sendif it is. Changing the body so the returned future or iterator stops beingSendis a breaking change nobody sees in the signature. - Lifetime capture: in edition 2024
impl Traitin return position captures all in-scope generic and lifetime parameters; in 2021 it captured type parameters but only the lifetimes named in the bounds. Rust 1.82 stabilized precise capturing,-> impl Iterator<Item = u8> + use<'a, T>, to say exactly what is captured. The classic 2021 error is E0700 ("hidden type captures lifetime that does not appear in bounds").
pub fn evens(limit: u32) -> impl Iterator<Item = u32> + Clone {
(0..limit).filter(|n| n % 2 == 0)
}
pub fn words(text: &str) -> impl Iterator<Item = &str> + '_ {
text.split_whitespace()
}
fn main() {
assert_eq!(evens(7).collect::<Vec<_>>(), [0, 2, 4, 6]);
assert_eq!(words("a b").count(), 2);
}Review checklist
- Every trait used as
dynanywhere has a&dyn Traitusage in the crate (compile-time guard). - Constructors, generic helpers and
Self-returning methods carrywhere Self: Sized. - No
Clone/Default/PartialEqsupertraits on traits meant fordyn;box_clonewhere needed. - Trait objects stored in shared state are
dyn Trait + Send + Sync(or the trait has those supertraits). - Borrowed trait objects have explicit lifetimes (
+ '_). - Hot loops use generics; plumbing uses
dynwhere it simplifies signatures. -
dyn_compatreports no TR001.
rust-borrow-checker-error-explainer
Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.
rust-cargo-workspace-designer
Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.
rust-error-handling-thiserror-anyhow-designer
Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.
rust-lifetime-annotation-advisor
Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.
rust-tokio-async-service-builder
Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.
rust-unsafe-block-auditor
Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.