RFA-384 · Case file with fixtures · Case 356 of 694 · Compiler evidence
Rc<Box<Self>> Cannot Be Used as a dyn-Dispatched Receiver
Rust dyn dispatch supports specific receiver shapes such as &Self, Box<Self>, Rc<Self>, Arc<Self>, and Pin around them. Arbitrary pointer nesting does not preserve a dispatchable metadata path.
- 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
- Dyn dispatch supports a defined set of receiver forms and transparent Pin wrapping, but arbitrary nesting such as Rc<Box<Self>> cannot locate the trait-object metadata correctly.
- First discriminating check
- Flatten the receiver to a supported form such as Rc<Self>, Box<Self>, a reference, or Pin around one supported pointer shape.
I knew that Rust supports receivers such as self: Rc<Self>, so I assumed self: Rc<Box<Self>> was only one harmless layer deeper. It is not a dyn-dispatchable receiver shape.
The failing fixture creates Box<dyn NestedReceiver>. Rust reports E0038 because the nested receiver cannot be dispatched on.
A receiver is part of the dispatch mechanism
The self parameter is not just an ordinary first argument. Method-call syntax and trait-object dispatch use it to find the implementation and transform the owning pointer into the form expected by the method.
The dyn-compatibility Reference lists supported receiver forms: references to Self, Box<Self>, Rc<Self>, Arc<Self>, and Pin<P> where P is a supported pointer form.
Rc<Box<Self>> is not on that list. The outer Rc points to a Box<Self> value, not directly to Self. Trait-object metadata and unsizing would need to travel through an additional arbitrary container relationship that the stable dispatch rules do not promise.
Smart pointers are not transparent by nesting
Box<T> and Rc<T> each have specific ownership and layout behavior. Combining them creates a real two-allocation representation: a reference-counted allocation containing a box pointer to another allocation.
The fact that both pointer types independently support some coercions does not imply every composition supports receiver dispatch. Abstraction capabilities do not multiply automatically through nesting.
This is similar to auto traits, pinning, and FFI layout: a wrapper can preserve a property only when its contract says so.
Flatten ownership when one pointer is enough
The repaired fixture changes the receiver to self: Rc<Self>. A value stored as Rc<dyn DirectReceiver> can call the consuming method, and the implementation receives ownership of that Rc clone.
If exclusive ownership is required, self: Box<Self> may be better. If borrowing is enough, &self avoids consuming a smart pointer. If shared thread-safe ownership is required, Arc<Self> communicates it.
I choose the receiver from lifecycle semantics first, then verify it belongs to the supported dynamic forms.
A nested representation can stay inside the method body
Sometimes the implementation genuinely needs a boxed value inside reference-counted state. The public receiver does not need to expose that storage topology. It can accept Rc<Self> and allocate or access internal boxes as part of the concrete implementation.
Keeping representation out of the trait signature makes implementations freer to change and keeps the dynamic contract small.
If callers must pass a nested owner for another reason, I make it an ordinary argument to a receiver-based method. Then trait-object dispatch uses &self or Rc<Self>, while the nested value is explicit data rather than the dispatch carrier.
Pin is a documented special composition
The Reference permits Pin<P> around supported pointer forms. This does not prove arbitrary wrappers are transparent. Pin participates in receiver rules intentionally because pinned receivers are needed for APIs whose values must not move.
I rely on the documented list rather than guessing from structural similarity. Language support can evolve, but source code should target the toolchain and receiver forms it actually guarantees.
Self: Sized is an alternative boundary
If the nested consuming helper is needed only by concrete callers, adding where Self: Sized can exclude it from dynamic dispatch while leaving other methods object compatible. Concrete code can still use Rc<Box<Concrete>>.
This is appropriate when the nested owner is a convenience rather than a core dynamic operation. If the entire purpose of the trait is that consuming call, flattening or redesigning the receiver is more honest.
My receiver audit includes storage cost
I draw the allocations and ownership counts for smart-pointer receivers. Rc<Box<T>> has different clone, allocation, and indirection costs from Rc<T>. Even if it compiled dynamically, the nesting would deserve justification.
I test the exact trait-object call, not only the concrete method. Concrete receivers can compile under rules that do not make the trait dyn compatible.
There is also a maintenance benefit in using conventional receiver shapes. Documentation tools, examples, and other engineers recognize &self, Box<Self>, Rc<Self>, and Arc<Self> immediately. A custom nesting makes every caller understand storage details before they can understand the operation. When that nesting is only accidental, flattening removes both a compiler restriction and an unnecessary architectural promise. If it is essential, I expose a smaller supported dispatch method and keep the unusual ownership transition in concrete code where its invariants can be tested directly.
The core principle is that a trait-object receiver must provide a supported route from the erased pointer to the implementation. Rc<Self> has such a route. Wrapping Self inside Box inside Rc changes that route and the ownership model. Rust asks me to flatten it or keep the operation concrete-only instead of assuming pointer wrappers are freely composable.