RFA-380 · Case file with fixtures · Case 352 of 694 · Compiler evidence
A Sized Supertrait Excludes Every Rust Trait Object
dyn Trait is dynamically sized, so a trait-wide Self: Sized requirement excludes trait objects by definition. Move Sized to the few concrete-only methods and leave receiver methods on the dynamic surface.
- 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
- Every trait object is dynamically sized, while a Sized supertrait requires every implementer and every use of Self to have a compile-time known size.
- First discriminating check
- Remove the trait-wide Sized bound and apply `where Self: Sized` only to operations that genuinely require a concrete sized implementer.
I once saw a trait with a perfectly ordinary &self method fail at &dyn Service. The method was not the problem. The declaration said trait Service: Sized, so every implementation was required to have a size known at compile time.
The failing fixture shows rustc E0038 pointing to that supertrait bound.
A trait object is dynamically sized
Different implementers can have different layouts. A dyn Service value therefore has no one compile-time size. Code normally handles it behind a pointer such as &dyn Service, Box<dyn Service>, or Arc<dyn Service>, whose pointer representation provides the indirection and dispatch metadata.
The Sized trait represents types whose size is known at compile time. Requiring Self: Sized on the entire trait excludes the dynamically sized trait-object type itself.
This is not fixed by placing the object behind a Box. The box has a known size, but its pointee is still dyn Service, and that pointee must satisfy the base trait's rules.
A supertrait bound is global
Writing trait Service: Sized is equivalent to making Sized a requirement for every implementer and every use of the trait. The dyn-compatibility rules explicitly say Sized must not be a supertrait.
I sometimes find this bound left over from an early generic design. A method consumed self or returned Self, so the author added Sized to make the errors disappear. Later, a registry or plugin system needed dynamic dispatch and discovered that the complete trait had been closed to objects.
The useful question is not “does any method need Sized?” It is “does every capability in this trait require every implementer to be Sized?” Usually the answer is no.
Put Sized on concrete-only methods
The repaired fixture removes the supertrait and puts where Self: Sized on one consuming helper. The receiver-based name method remains callable through &dyn Service, while the concrete Local type can use the helper.
This method-level bound explicitly removes only that operation from dynamic dispatch. It does not make the method magically callable through dyn; it preserves the rest of the object-safe surface.
I use this for constructors, methods returning Self, generic convenience methods, and by-value operations that are meaningful only when the concrete type is known.
Generic code still gets the static surface
A generic function fn run<T: Service>(value: T) normally assumes T: Sized unless it writes T: ?Sized. It can call methods with Self: Sized because the concrete type is available.
A function accepting &dyn Service uses the dynamic surface and cannot call excluded helpers. This is a useful separation rather than an inconvenience: the function signature tells readers which type knowledge exists.
If a helper must work for both sized generics and trait objects, I redesign it around a receiver form and return type that the dynamic interface can represent.
Removing Sized may reveal another real constraint
After removing the global bound, rustc may report that one method is not dyn compatible because it returns Self, has generic type parameters, or lacks a receiver. I address each method at its own boundary.
Adding where Self: Sized selectively is often enough. For runtime construction, I may return Box<dyn Service>. For a generic method, I can move it to an extension trait. The goal is not to silence E0038 but to state which operations belong to static and dynamic use.
Public traits need an object assertion
If a public trait is intended for dynamic use, I keep a tiny compile test that constructs a trait object. A later contributor adding : Sized then receives feedback near the API change, not in a downstream application.
I also document whether dyn compatibility is promised. Removing an accidental bound can be compatible for many users, but adding it later is a large loss of capability.
My review checklist is:
- Does the trait itself require
Sized, or only one method? - Are downstream users expected to store heterogeneous implementers?
- Can concrete-only helpers move to an extension trait?
- Does the dynamic interface have representable receivers and results?
The core principle is that pointer size does not make its pointee Sized. A trait object exists precisely because the concrete pointee size is erased. Trait-wide Sized contradicts that purpose; method-local bounds preserve static conveniences without closing the entire dynamic interface.