RFA-381 · Case file with fixtures · Case 353 of 694 · Compiler evidence
A Receiver-Free Trait Function Needs Self: Sized for dyn
A trait object selects implementations through a receiver value. A receiver-free function has no such selector, so make it concrete-only with where Self: Sized, turn it into a method, or move universal behavior outside the trait.
- 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
- A receiver-free function gives a trait object no value through which to choose an implementation, unless the function is explicitly excluded from dynamic dispatch.
- First discriminating check
- Decide whether the function needs a receiver; otherwise add `where Self: Sized` and call it through a concrete implementation type.
I added a default fn version() -> u8 to a trait and later found that &dyn Codec no longer compiled. The dynamic code only called name(&self), but the receiver-free function changed the compatibility of the whole trait.
The failing fixture records E0038: associated function version has no self parameter.
Dynamic dispatch needs a value that selects an implementation
A call such as codec.name() starts with a trait-object value. Its metadata identifies the concrete implementation and the method entry to invoke.
Codec::version() has no receiver. The expression names the trait but no implementing value or concrete implementing type. If Json and Binary provide different versions, nothing in the call chooses between them.
Even if only Json exists today, a trait permits other implementations later and sometimes in other crates. Rust checks the interface rule, not a temporary whole-program count of implementations.
Default bodies do not select Self
A default function body can make every implementation share code, but an implementation may override it unless the design prevents that by another mechanism. More importantly, calling the trait function still requires choosing an implementation context for Self.
The associated-function Reference distinguishes methods, which have a self parameter, from associated functions without one. This syntactic difference becomes a dispatch difference.
I do not assume “static method on a trait” means “one global function belonging to the trait namespace.” It belongs to each implementation of that trait.
Exclude it from the object surface
The repaired fixture adds where Self: Sized to version. This marks the function explicitly non-dispatchable. The trait becomes dyn compatible, and concrete code calls Json::version() while trait-object code calls only name.
The dyn-compatibility rules describe this exception: an associated function that cannot be dispatched may carry a Self: Sized bound.
This is appropriate when the function constructs Self, returns type-level metadata, or supports generic configuration known at compile time.
Add a receiver when the value is runtime behavior
If different runtime objects should report different versions, fn version(&self) -> u8 is the direct interface. The receiver supplies the implementation selector, and the method can be called through dyn Codec.
Changing a function into a method affects call sites and may suggest that the result depends on instance state even when it depends only on type. I accept that only when runtime dispatch is genuinely part of the requirement.
For a registry of codecs, a receiver method often makes sense because the registry already holds values. For compile-time buffer sizes, a static associated item may be the honest model.
Move universal behavior to a free function
If the implementation never varies, placing the function on the trait adds a false selection dimension. A module-level function or constant is clearer and callable without inventing a concrete implementer.
Extension traits can also keep generic conveniences separate from the minimal dynamic contract. This reduces the chance that a new helper accidentally breaks trait-object users.
I often keep three layers: a small dyn-compatible behavior trait, an extension trait for sized generic helpers, and free functions for operations that do not depend on an implementation.
This is related to E0790 but happens earlier
Calling a receiver-free trait function as Trait::function() can produce E0790 because no implementation was selected. In this case, E0038 prevents the trait object itself because the function was not declared concrete-only.
The common cause is missing selection information. One diagnostic concerns the shape of a dynamic interface; the other concerns one call expression. Fully qualified syntax such as <Json as Codec>::version() selects an implementation, but it does not by itself make the original trait dyn compatible. The method needs the Self: Sized boundary too.
I test both call modes
For a mixed static/dynamic trait I keep one compile test calling the associated function through a concrete type and another constructing &dyn Trait and calling its receiver methods. This proves the intended split.
In review I ask whether each receiver-free function varies by implementation, whether dynamic callers need it, and whether Self: Sized is visible enough in documentation.
The core principle is that a trait name describes a family, not one implementation. Dynamic dispatch chooses a family member through a value receiver. A receiver-free function needs an explicit concrete type, or it must be excluded from the trait-object surface so the remaining methods can dispatch coherently.