RFA-370 · Case file with fixtures · Case 342 of 694 · Compiler evidence
An Associated Constant Makes a Rust Trait Not Dyn-Compatible
Trait objects dispatch operations through values, but an associated constant belongs to the implementing type. Move runtime-varying configuration behind a receiver method, or keep the constant on a separate static 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 dyn-compatible trait cannot contain associated constants because a trait-object value has no supported value-level dispatch operation for selecting that implementation constant.
- First discriminating check
- Reduce the trait to the constant and one ordinary receiver method, then decide whether the value belongs in a dispatchable method or a separate static trait.
I met this error while turning a small generic component into a runtime-selected one. The trait looked harmless: each implementation supplied const MAX, and one method checked a value against it. The concrete version worked, but &dyn Limits produced E0038. The associated constant changed the compatibility of the whole trait.
The failing program keeps the example small. Small implements the constant, yet rustc says that Limits is not dyn compatible because it contains associated constant MAX.
A trait object starts from a value
A trait object such as &dyn Limits contains enough information to find an erased value and dispatch supported methods for its concrete implementation. The caller has a value receiver at runtime. It no longer names Small as the static type.
An associated constant is different. The associated-constant reference defines it as a constant associated with a type or trait implementation. It is selected through type-level knowledge such as <Small as Limits>::MAX; there is no receiver value in that expression.
This is why “every implementation defines it” is not the missing proof. The problem is the operation by which a caller would select it after the concrete type has been erased.
Dyn compatibility applies to the complete trait
The dyn-compatibility rules state that a dyn-compatible trait must not have associated constants. A constant that is never mentioned by my dynamic code still affects whether the trait can be the base of a trait object.
I find this important during API evolution. Adding a constant can look source-compatible for generic callers and still break downstream users who construct Box<dyn Trait>. Public traits are not only method collections; their associated items define which forms of polymorphism remain possible.
The first check is therefore structural. I reduce the trait to the associated constant and one receiver method. If the trait object starts compiling when the constant is removed, I do not waste time changing lifetimes, adding Send, or boxing more values.
Use a method when the value varies at runtime
The repaired program changes const MAX into fn max(&self) -> usize. Now the vtable can select the implementation using the receiver, and both max and accepts work through &dyn Limits.
This design is correct when MAX is part of runtime behavior. A collection of several implementations can answer with different limits, and the caller does not need to know their concrete types.
The method can still be cheap. A method returning a literal is normally easy for static calls to inline, while dynamic calls have the dispatch cost already chosen by using a trait object. I measure performance rather than preserving a constant only because it feels faster.
Keep static capabilities separate when they are truly static
Sometimes the constant is used only by generic code, array declarations, compile-time assertions, or type-level configuration. Converting it to a method would remove const-context use and misrepresent the design.
In that case I split the interface. A static trait can carry the constant, while a dyn-compatible runtime trait carries receiver methods. A concrete type may implement both. Generic code uses T::MAX; erased code uses only the dynamic surface.
This separation also makes architecture clearer. Static capabilities require knowing the implementing type. Dynamic capabilities require having an implementing value. Combining both in one trait is convenient until a caller needs erasure.
A default value does not change the rule
Giving the constant a default in the trait does not make it dispatchable. An implementation may override that default, and the dyn-compatibility rule rejects associated constants as a category.
Adding where Self: Sized is useful for some methods that should remain concrete-only, but it is not an escape hatch for an associated constant. The Reference gives a Self: Sized exclusion for non-dispatchable functions, not for constants.
This is a useful contrast with a method returning Self: that method can be explicitly removed from the trait-object surface. A constant cannot be repaired in the same local way; I must redesign where it lives.
The compiler error describes an API boundary
E0038 can feel like a limitation around vtables. I read it as feedback that one abstraction is serving two forms of selection. Static selection knows a type and supports associated constants. Dynamic selection knows a value and supports a restricted receiver-based interface.
My review checklist is simple:
- Does this information need to differ among values selected at runtime?
- Does any caller need it in a const context?
- Do downstream users construct trait objects from this trait?
- Would two smaller traits state these capabilities more honestly?
For plugin limits, codecs, storage backends, and policy engines, these questions prevent a small constant from silently closing the dynamic extension point.
The core principle is that implementation availability is not the same as dispatchability. Every concrete type may have MAX, but a trait object needs an operation defined for the erased value. I use a receiver method for runtime behavior and keep associated constants on interfaces that remain statically selected.