RFA-128 · Case file with fixtures · Case 100 of 694 · Compiler evidence
When derive(Default) Adds an Unneeded Generic Bound
Built-in derive can generate conservative bounds from generic field syntax. If the generated T: Default constraint is stronger than construction really needs, write the small manual Default implementation and test the weakest intended API.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- check, dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The built-in derive generates an implementation with a conservative T: Default bound from the generic field syntax rather than proving the weaker bound accepted by PhantomData.
- First discriminating check
- Expand or inspect the derived implementation, identify which generic bounds it introduced, and compare them with the actual requirements of constructing every field.
I usually reach for #[derive(Default)] because it is short and reliable. The failing program shows an edge where the generated public contract is stronger than the fields require.
Marker<T> contains only PhantomData<T>. Deriving Default makes Marker<NoDefault>::default() fail with E0277 because NoDefault does not implement Default. But creating PhantomData<NoDefault> needs no NoDefault value at all.
Derive writes an implementation for us
The derive attribute reference describes derive as a way to generate implementations for supported traits. I treat the result as code generation, not as a theorem prover that always finds the weakest possible bounds.
For this generic structure, the generated implementation behaves like it carries a T: Default requirement. That requirement is safe and conservative, but it excludes a valid use of the field type.
The important debugging move is to separate two questions:
Can every field be constructed with Default?
What generic bounds did the generated impl expose?
Those answers can differ.
PhantomData does not need a T value
The PhantomData documentation explains that the marker acts as though it relates to T for type-system purposes without storing a value of T. Its Default value is simply PhantomData.
This works regardless of whether T has a default value. There is no inner object to construct, initialize, or inspect.
That is why adding #[derive(Default)] to NoDefault would repair the diagnostic but weaken the design. The domain type may have no meaningful default. I do not want to invent one because a wrapper's generated implementation requested it.
The manual implementation expresses the weaker truth
The repaired program writes the implementation directly:
impl<T> Default for Marker<T> {
fn default() -> Self {
Self { marker: PhantomData }
}
}
No operation in this body needs T: Default, so no such bound appears. Marker<NoDefault> now works while NoDefault remains honest about having no default value.
This manual code is not a workaround against the compiler. It is a more precise trait implementation for the API I want.
Where the difference matters
An unnecessary bound is easy to miss inside one crate because the type parameters used in tests may already implement common traits. It becomes visible to downstream users with marker types, handles, trait objects, functions, or domain entities that deliberately omit Default.
For a public library, the extra bound reduces which types compose with the abstraction. Removing it later is usually compatible, but users may already have added meaningless implementations or wrappers to satisfy it.
I therefore include a compile-time use with a deliberately non-defaultable parameter when weak bounds are part of the API:
struct NoDefault;
let _: Marker<NoDefault> = Marker::default();
This small test protects a negative property: T must not need Default.
Not every derive bound is a problem
If a structure stores T directly and constructs it with T::default(), the bound is real. A manual implementation cannot remove the need without changing what the type stores or how the default is defined.
Similarly, a field such as Option<T> has a default value without T: Default, while a direct T field does not. I inspect the actual field operations rather than using one rule for all generic wrappers.
Manual implementations also have a maintenance cost. If fields are added later, the custom default must be updated. I prefer derive when its contract matches the intended contract, and a manual implementation when precision is valuable.
My debugging sequence
When a derived trait asks for a surprising generic bound, I do this:
- Read the full diagnostic chain and identify the generated trait implementation.
- Expand its effective
whereclause mentally or with a macro-expansion tool. - Check the trait implementation of each field type, not only its syntax.
- Decide the weakest bound the public wrapper should promise.
- Write a manual implementation when the generated bound is stronger.
- Test with a marker type that intentionally lacks the disputed trait.
The broader principle is that convenience generation still designs an API. I keep derive when it says exactly what I mean. When it does not, a few explicit lines can preserve a much more general and truthful abstraction.