RFA-517 · Case file with fixtures · Case 489 of 694 · Compiler evidence
A By-Value Trait Method Cannot Move an Unsized Dyn Value
A plain self receiver requests movement of the concrete value, but dyn erases its size. Borrow the receiver, consume a sized pointer receiver, or keep dispatch static according to ownership intent.
- 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
- Plain self requests movement of the concrete referent out of its sized pointer while dynamic dispatch deliberately erased that referent's layout.
- First discriminating check
- Choose a borrowed receiver, a supported owning pointer receiver such as self: Box<Self>, or sized static dispatch according to real consumption semantics.
I called a trait method taking plain self through Box<dyn Consume>. Rust emitted E0161 because the method asks to move the erased concrete value, whose inline size is not known through dyn Consume.
The failing fixture already stores the object behind a sized box handle. The problematic move is conceptually out of that box into the by-value receiver.
Dyn erases the concrete type and size
A trait object contains a pointer to data plus metadata for dynamic dispatch. Different implementors may have different layouts.
The trait-object Reference explains this erased form. Code operating through dyn Consume cannot reserve a stack slot for moving an unknown-sized concrete value as plain self.
The official E0161 page suggests borrowing when consumption is not essential.
The repaired fixture uses a shared receiver
The repaired fixture changes the method to &self. Dynamic dispatch passes a fixed-size reference, and String can report its length without being consumed.
This is the right repair only because the operation is observational. If the method must consume resources or prevent reuse, borrowing changes semantics.
I decide ownership intent before changing the receiver.
A sized pointer receiver can express dynamic consumption
Traits can use receivers such as self: Box<Self> in supported method forms. Then the method consumes the fixed-size owning box and dynamic dispatch can let each implementation handle its owned value.
This differs from plain self, which asks for the concrete value itself. Arc<Self> or other receiver forms have their own support and semantics.
I use an owning pointer receiver when consumption through the object boundary is a real requirement.
Static dispatch knows the concrete size
A generic function fn use_it<T: Consume>(value: T) is monomorphised for concrete T, so plain by-value self is workable when T is sized.
If runtime heterogeneity is unnecessary, static dispatch may be simpler and faster to reason about. Trait objects are valuable when values of different concrete types share storage or are selected at runtime.
The receiver design should agree with the dispatch model.
Sized restrictions can make the limitation explicit
A trait method can require Self: Sized. Then it remains available to concrete implementors but not callable through a trait object. Other object-safe methods can still use the trait dynamically.
This is useful when only one convenience method consumes by value. I document the split so users understand why it disappears on dyn Trait.
Changing the bound later is a public API decision.
Boxed values are sized, but their referents may not be
It is easy to say “Box is sized, so moving should work.” Moving the Box<dyn Trait> handle works. Dereferencing and moving its dyn Trait payload as a plain value does not.
I name both layers in debugging: ownership of the pointer and size of the referent. Many trait-object errors become clearer once these are separated.
Object-safe cleanup can use an explicit consuming verb
For resources behind trait objects, I may define fn close(self: Box<Self>) -> Result<...>. The owning receiver shows that the dynamic object cannot be used afterward, while the result reports failure that Drop cannot return. An implementation can then release its concrete state with full ownership.
If callers also need borrowed inspection, I keep separate status(&self) methods. Mixing observation and consumption under one vague verb makes receiver choices confusing. A clear API lets users see which calls retain the box and which surrender it.
Collections amplify the receiver decision
A Vec<Box<dyn Consume>> can iterate by reference for borrowed calls or consume the vector to move each box into a boxed receiver. Plain self on the erased referent still does not become sized. I test the actual collection flow because single-value examples can hide how ownership is transferred in production.
My E0161 checklist
- Does the method take plain
self, a borrow, or an owning pointer receiver? - Is the call dispatched through
dyn Trait? - Must the operation truly consume the underlying value?
- Would
&selfor&mut selfpreserve semantics? - Would
self: Box<Self>express dynamic ownership correctly? - Can
Self: Sizedreserve the method for static dispatch? - Is runtime heterogeneity actually required?
- Am I confusing movement of the box with movement of its unsized referent?
The core principle is that dynamic dispatch knows behaviour but erases concrete layout. Ownership-changing methods must operate through a sized handle or remain limited to concrete sized values.