We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Trait Objects and Dispatch step 2 of 10
Dyn compatibility: the actual rules
E0038 is the wall. Every intermediate Rust learner hits it, usually while
trying to build the Vec<Box<dyn MyTrait>> from the previous item, and the
first reaction is always “why is this trait special?”
It is not special. There is a short, mechanical list of rules, and E0038 is one of the rare diagnostics that enumerates its own causes — read it and it will tell you exactly which method broke the rule and why. This item turns it from a mystery into a checklist.
Terminology, because the internet is out of date
This property used to be called object safety. It was renamed dyn compatibility in 2024. Every Stack Overflow answer, book and blog post written before then says “object safe”. They mean the same thing. The compiler now says:
the trait `Render` is not dyn compatible
Why the rule exists at all
A Box<dyn Render> is a data pointer plus a vtable pointer. The vtable is
a fixed array of function pointers, built once per concrete type at compile
time. So every method reachable through dyn must be expressible as one
function pointer, taking a pointer-shaped receiver. Two things break that:
-
A generic method.
fn tagged<T: Display>(&self, t: T)is not one function. It is one function perTanybody ever calls it with — a set the compiler cannot enumerate when it builds the vtable, because a caller in another crate can add to it later. There is no slot to put. -
A method mentioning
Selfoutside the receiver.fn duplicated(&self) -> Selfmust return a value whose type is the erased concrete type. The caller holds adyn Renderand has no name for that type, and no idea how big it is.
The full rule list
A trait is dyn compatible when:
- all of its supertraits are dyn compatible;
-
Sizedis not a supertrait (trait Foo: Sizedis never dyn compatible); - it has no associated constants;
- it has no associated types with their own generic parameters;
- every method is either dispatchable or explicitly opted out.
A method is dispatchable when it has no type parameters, does not mention
Self except as the receiver type, takes its receiver as &Self,
&mut Self, Box<Self>, Rc<Self>, Arc<Self> or Pin of one of those,
and does not return impl Trait or use async fn.
The escape hatch, and its sting
fn tagged<T: Display>(&self, t: T) -> String
where
Self: Sized,
{ ... }
where Self: Sized says “this method only exists when Self has a known
size”. dyn Render is never Sized, so the method is simply not part of
the trait object’s interface — no vtable slot needed, and the rest of the
trait becomes dyn compatible.
Here is the part that surprises people: it does not merely make the method
legal, it makes it invisible on trait objects. boxed.tagged(1) where
boxed: Box<dyn Render> will not compile, and the error is a confusing “the
method exists but its trait bounds were not satisfied”. That is the price.
The method is still perfectly callable on Dot and Word directly.
What to write
The trait below is given to you broken. Make it usable as Box<dyn Render>
by opting the two offending methods out — without deleting them and without
making them uncallable on the concrete types. Everything else already
compiles and must keep working.
pub trait Render {
fn label(&self) -> String; // dispatchable
fn tagged<T: Display>(&self, t: T) -> String; // generic — not dispatchable
fn duplicated(&self) -> Self; // returns Self — not dispatchable
}
Dot(n).label() is n dots. Word(s).label() is s uppercased.
duplicated doubles: Dot(n) -> Dot(n * 2), and Word(s) -> Word(s + s).
The default tagged body is format!("{}#{}", self.label(), t).
dyn_labels builds a Vec<Box<dyn Render>> and returns each label().
concrete_report works with concrete values and returns the results of
tagged and duplicated().label() — proof that opting out of the vtable did
not cost you the methods.
A different error you will eventually meet: E0191
Box<dyn Iterator> does not fail with E0038. It fails with E0191:
the value of the associated type `Item` must be specified
Associated types are fine on trait objects, but you have to pin them down —
Box<dyn Iterator<Item = u32>> works. Two different errors, two different
fixes; do not confuse them.
The refactor worth knowing
When a generic method blocks you, the usual real-world fix is not
where Self: Sized but taking a trait object instead of a type parameter:
turn fn tagged<T: Display>(&self, t: T) into
fn tagged(&self, t: &dyn Display). Now there is exactly one function, it is
dispatchable, and it stays callable through dyn. Reach for that first; reach
for where Self: Sized when the method genuinely cannot be made
vtable-shaped, as duplicated cannot.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.