Default derivation incomprehension

Hello fellow developers.

I've got a compilation error that I fail to understand.
The E struct default derivation fails because the A and B don't implement Default. But the default implementation of the Test struct is Test(None). It has nothing to do with the generics associated with it.

So why does the compiler checks for an A and B default implementation in this case ?

Thanks for your help !

#[derive(Default)]
struct Test<T>(Option<T>);

struct A(i32);
enum B {
    C,
    D,
}

#[derive(Default)]
struct E {
    f1: Test<A>,
    f2: Test<B>,
}

fn main() {}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `A: Default` is not satisfied
  --> src/main.rs:12:5
   |
10 | #[derive(Default)]
   |          ------- in this derive macro expansion
11 | struct E {
12 |     f1: Test<A>,
   |     ^^^^^^^^^^^ the trait `Default` is not implemented for `A`
   |
   = help: the trait `Default` is implemented for `Test<T>`
note: required for `Test<A>` to implement `Default`
  --> src/main.rs:1:10
   |
1  | #[derive(Default)]
   |          ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro
   = note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `A` with `#[derive(Default)]`
   |
4  + #[derive(Default)]
5  | struct A(i32);
   |

error[E0277]: the trait bound `B: Default` is not satisfied
  --> src/main.rs:13:5
   |
10 | #[derive(Default)]
   |          ------- in this derive macro expansion
...
13 |     f2: Test<B>,
   |     ^^^^^^^^^^^ the trait `Default` is not implemented for `B`
   |
   = help: the trait `Default` is implemented for `Test<T>`
note: required for `Test<B>` to implement `Default`
  --> src/main.rs:1:10
   |
1  | #[derive(Default)]
   |          ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro
   = note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` (bin "playground") due to 2 previous errors

Because T in Test<T> must also impl Default!

The derive for Test<T> is more or less something like this:

impl<T> Default for Test<T>
where
    T: Default,
{
    fn default() -> Self {
        Self(None)
    }
}

This allows you to use types T which do not implement Default, as is the case with your A struct and B enum. But it means that Test<A> and Test<B> do not implement Default.

Thus, if you do derive Default for A and B, everything works. Or alternatively, if you manually implement Default for Test<T> without the where clause, it also works.

Thanks for your help. I didn't know the derive impl of Default also constrained T. It makes sense.

For my use case it doesn't make sense to implement Default for A and B so I implemented Default manually for Test without constraining T as you suggested.

Sounds good,

FWIW, the reason for the where clause is that derive macros do not have any information from the type system. The actual implementation returns Self(Option::<T>::default()). But not knowing anything about Option, it can't just return Self(None), even though that's what Option::default() returns, regardless of the type T.

2 Likes

#[derive(Default)] applies the Default procedural derivation macro to the following item (the macro is named the same as the corresponding trait, but is different from it). The only thing it knows is the syntactic structure of the item's definition. Macros don't interact with the type system, and have no way to query it for any information.

This means that the macro knows about the existence of free generic parameters, which are listed after the impl keyword, but doesn't know anything about the types. It knows nothing about A or B. It doesn't know whether Option is the usual type from the standard library, or a custom user-defined type from the current crate or its dependencies. Thus it has no way to know that Option<T> has an unconditional default variant.

The only general solution available to the macro is to conservatively apply Default bounds to all generic parameters, and hope for the best. This solution ensures that a composition of #[derive(Default)] types can also derive Default.

Any more complex trait bounds (including no bounds) would require extra annotations from the user. Thus far such annotations were not implemented for stdlib derive macros, but you can encounter them in some procedural macro crates.

The solution is to manually implement Default for your type.

1 Like

No, that's wrong.

The built-in derive macro for Default is just not smart enough. It blindly applies the bound on all type parameters.

It wouldn't have to do this. It's perfectly feasible to generate constraints that only bound the types of the fields. That would look like:

impl<T> Default for Test<T>
where
    Option<T>: Default
{
    ...
}

This in turn would not constrain T itself to be Default.

No, again, this is incorrect. The implementation of Default is known (defined) to just forward to Default implemented on each field. Therefore the derive could simply add constraints to the types of the fields, since fields types are available to the macro, as they are syntactically part of the definition of the UDT.

The current, overly conservative behavior is a historical accident.

1 Like

Some additional information on this:

This is called a "perfect derive", and it's discussed in the past. But it's not really a historical accident. The main reason why prefect derive didn't happen is that it could leak internal implementation detail and easily introduce semver hazard.

Relevant Niko blog post

5 Likes

Well, the status quo is way more counter-intuitive. I have written several such "perfect" derive macros, and the alleged "leaking of implementation details" was never an issue in practice.

I could imagine a more conservative implementation where only public fields would be considered (that's easy to do in a proc-macro, since visibility is also part of the type definition), and private fields could either be opted in, or simply be assumed to impl Default and have their constraints omitted from the publicly-exposed list of bounds.

Hmm yeah, I agree the current behavior is much more confusing. But the semver hazard part have hit me in the past though. Some upstream added a field that is !Send and suddenly my code is broken :frowning: . It's not about perfect derive but it's similar enough so you get the idea.

And I'm not opposed to adding perfect derive to Rust std, instead I think it's a good idea. I was just saying it has some downside to it.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.