Empty structs to derive Default by default

Empty struct can have only one value (thus the default one), so requiring to explicitly derive Default seems unnecessary.

Example: Playground

//#[derive(Default)]
struct X;

fn f<T: Default>(_: T) {
    ()
}

fn main() {
    f(X);
}

With #[derive(Default)] commented out, compiler refuses to cooperate:

Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `X: std::default::Default` is not satisfied
 --> src/main.rs:9:7
  |
4 | fn f<T: Default>(_: T) {
  |    -    ------- required by this bound in `f`
...
9 |     f(X);
  |       ^ the trait `std::default::Default` is not implemented for `X`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

I got redirected here to internals forum, and I'd like to validate if such suggestion makes sense. And if it does - to implement it! :slight_smile:

The standard downside to any proposal for auto-deriving traits is that the traits your type implements are part of its public API, with all the semver that implies, so you'd never be allowed to make the struct non-empty in the future without breaking people who were using this trait.

In this case, maybe that's not such a big deal. But the potential benefit also seems pretty small, so I'd say the "direct" pros and cons kinda cancel out, and the (IMO quite strong) tie-breaker argument is that it's more consistent with the rest of the language if we don't have this auto-derive.

11 Likes

Why? What advantage would this have?

Making a trait magical like this requires a ton of mechanisms and it's just generally bad for understandability in the absence of very significant benefits. Why don't you just write #[derive(Default)] for your type if you want so? Not having to type a dozen characters is not nearly enough of an advantage to consider a change to the core language.

In addition, all the usual caveats applying to auto traits also apply here. Now when you don't want your type to be Default, you have to somehow work around it. This has been proposed for several traits in the past, e.g. Copy – that's bad too, because, again, you might want to enforce invalidation of a moved value for higher-level semantic reasons, even though it might technically be possible to make it Copy.

7 Likes

Thanks for comments. Please note I'm suggesting auto-deriving Default only for empty struct. In my understanding, the empty struct type contains exactly one value. This one value is the default value - by definition. Thus requiring to put extra annotation on a type with only one possible value kind of makes no sense - not taking into account overhead of implementing it in the language.

My point is that empty struct already logically derives Default, so adding it into type interface implicitly makes sense to me. How easy/hard it is to implement in core language is a separate topic IMHO.

We should probably state explicitly that it's entirely possible to have an empty struct in your public API that you don't want to implement Default.

The example that pops to mind for me is using ZSTs as a sort of token for enforcing that foo() is always called before bar(), by making foo() return it and bar() consume it. Allowing the token to be constructed out of thin air with default() would break that.

So it's really not a question of "every type already is conceptually Default, so why not?". It's a question of "do we derive Default implicitly and have an opt-out, or do we not do that and have an opt-in?". Rust tends to prefer the latter.

10 Likes

To make this even more clear: given a unit struct struct Unit;, an empty record struct struct Record {}, or an empty tuple struct struct Tuple();, the consumer can already construct the structure out of thin air any time they want.

For these types, it is impossible to separate construction visibility with type visibility, so I don't think giving a new way to construct the type would be problematic....

Except it is, for unnamable types.

You shouldn't implement unconstructable tokens like this, but with an unnamable unit struct works just as well as a namable struct with private constructors, until you can use Default::default() where the token is required.

On top of that, making derive(Default) automatic for this types would be a breaking change, because it would conflict with existing explicit impls (or derives) of the trait. (You could make the auto impl go away with an explicit impl, but that exists nowhere else in the language and would be unique just to address a tiny papercut.)

7 Likes

@Ixrec, thanks for clarification, the explicit opt-in as a preference totally makes sense.

Yet

Please correct me if I'm wrong, but anyone can still create instance out of thin air, without Default derived. If such empty struct type is exposed, the default value creation does not require Default derived:

pub struct X;

let x = X; // anyone can do that, Default not derived.
1 Like

Here's an example of the private-in-public shenanigans I allude to in my previous post.

// don't do this, use a namable type with private constructor instead, like a normal person
pub mod lib {
    mod priv_in_pub {
        pub struct LinearToken;
    }
    pub fn acquire_token() -> priv_in_pub::LinearToken { priv_in_pub::LinearToken }
    pub fn consume_token(_: priv_in_pub::LinearToken) {}
}

pub mod downstream {
    use crate::lib;
    fn main() {
        // ok
        let token = lib::acquire_token();
        consume_token(token);

        // bad, made possible by proposal
        consume_token(Default::default());

        // bad, still impossible with proposal
        let token = lib::LinearToken; // no...
        let token = lib::priv_in_pub::LinearToken; // no...
        consume_token(token);
    }
}
7 Likes

Thanks for clarification and for the example, I certainly didn't think that private types can be exposed via derived Default.

As I understand, there is no way for empty structs/tuples/etc to comply with <_: Default> bound and not expose private types like in provided example.

Well, then I must admin such proposal will do more harm than good. Closing the topic. Thanks again for clarifications!

5 Likes