RFA-420 · Case file with fixtures · Case 392 of 694 · Compiler evidence
Why a Rust Trait impl Cannot Add a Stricter Generic Bound
Every trait implementation must honour the callable contract advertised by the trait. Move a bound to the trait when it is universal, remove it from the implementation, or redesign the capability instead of narrowing one implementation.
- 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 implementation must honour the callable contract visible through the trait; adding a bound would make one implementor reject calls that generic code is entitled to make.
- First discriminating check
- Compare the trait and impl bounds, then decide whether the capability is universal, removable from the algorithm, or belongs on a different method or trait parameter.
I had a trait whose method promised to encode any value. One implementation only knew how to handle Copy values, so adding T: Copy to that implementation looked honest. Rust rejected it with E0276.
The failing fixture keeps the difference small. The trait declares fn encode<T>(value: T), while the implementation declares fn encode<T: Copy>(value: T). The compiler reports that the impl has a stricter requirement than the trait.
A trait is a promise to callers
The E0276 explanation says that a trait implementation cannot impose requirements absent from the trait definition. I find it easier to remember this from the caller's side.
Generic code is allowed to know only the trait contract:
fn send<E: Encode, T>(value: T) {
E::encode(value);
}
The trait says this call works for every T. If E = Wire silently required T: Copy, the generic function would become invalid only for one implementation. Method lookup would no longer preserve the promise used to type-check the caller.
An implementation supplies behaviour for the existing contract. It does not get to publish a narrower contract under the same method name.
Put a universal bound in the trait
In the repaired fixture, encoding genuinely requires Copy for every implementation, so the bound moves to the trait:
trait Encode {
fn encode<T: Copy>(value: T) -> usize;
}
Now callers see the condition and must prove it before calling. Each implementation receives exactly the capability that the public contract promised.
This is the right repair only when Copy is part of the abstraction. Moving a convenient implementation detail into a public trait can overconstrain every future implementation.
Remove the bound when the algorithm does not need it
Sometimes the impl added a bound because its first algorithm duplicated value. I first ask whether the value can be borrowed, moved once, transformed through another trait, or stored temporarily. Removing the duplication may remove the Copy requirement without changing the public design.
Copy is especially easy to add casually because numeric fixtures satisfy it. The bound excludes String, Vec, file handles, and many domain records. I test a non-Copy value when I want evidence that a supposedly generic interface is really generic.
A helper can carry implementation-specific constraints
An implementation may call a private helper with stronger bounds only after it has obtained those capabilities through the trait's contract or concrete type information. It cannot demand new evidence from the caller halfway through dispatch.
If only one concrete format supports a fast path for Copy, I can expose a separate inherent method such as Wire::encode_copy. The trait method keeps its universal behaviour and can use a general path. The extra capability is then visible in the method selected by the caller.
Another design is to put the input type on the trait:
trait Encode<T> {
fn encode(value: T) -> usize;
}
Implementations can then exist for selected T types. This changes the abstraction: the implementation is now parameterised by an input family rather than promising one method generic over every input. It can be correct, but I do not use it merely to silence the diagnostic.
Where clauses and inline bounds are the same issue
Writing fn encode<T>(...) where T: Copy instead of T: Copy does not change the contract. E0276 is about the requirement, not its spelling.
The same reasoning applies to added lifetime relationships and trait bounds involving associated types. If the implementation requires more than callers can infer from the trait, it is too narrow.
This is substitutability, not compiler stubbornness
The Reference section on trait implementations describes an implementation as implementing the associated items of a trait for a type. Generic code must be able to replace one valid implementor with another without discovering a new compile-time precondition.
This is close to an API compatibility rule. Tightening a parameter requirement breaks callers even when the implementation body becomes easier to write.
My review checklist
When E0276 appears, I check:
- Did the trait deliberately promise all
T? - Is the new bound essential to the abstraction or only one algorithm?
- Can ownership or borrowing remove the bound?
- Should the capability become a separate inherent method?
- Should the input type parameter belong to the trait itself?
- Do tests include a useful non-Copy input?
The core principle is simple: an implementation may be more capable internally, but it cannot be less callable than its trait. I repair the contract at the level where the requirement is true, so generic callers and every implementation share the same honest boundary.