Partial initialization and Default::default() in type Default implementation

Hello,

This is my first post in this forum. I just hit this problem in a personal project and I was wondering if the situation could be improved. Consider the type:

#[derive(Debug, Default)]
struct A {
    name: Option<String>,
    age: Option<u32>,
}

It's common to do a partial initialization of an instance of the type by using the .. shorthand (snippet 1):

fn main() {
    let a = A {
        name: Some("name".to_string()),
        ..Default::default()
    };
    println!("{:?}", a);
}

However, if we consider the following struct instead:

#[derive(Debug)]
struct A {
    name: Option<String>,
    age: Option<u32>,
}

And try to perform the same operation, but when implementing the Default trait (snippet 2):

impl Default for A {
    fn default() -> A {
        A {
            name: Some("name".to_string()),
            ..Default::default()
        }
    }
}

rustc 1.81 yields --it's printing a warning, but I think a better solution might be feasible--:

❯ cargo run --quiet
warning: function cannot return without recursing
  --> src/main.rs:8:5
   |
8  |     fn default() -> A {
   |     ^^^^^^^^^^^^^^^^^ cannot return without recursing
...
11 |             ..Default::default()
   |               ------------------ recursive call site
   |
   = help: a `loop` may express intention better if this is on purpose
   = note: `#[warn(unconditional_recursion)]` on by default

warning: fields `name` and `age` are never read
 --> src/main.rs:3:5
  |
2 | struct A {
  |        - fields in this struct
3 |     name: Option<String>,
  |     ^^^^
4 |     age: Option<u32>,
  |     ^^^
  |
  = note: `A` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
  = note: `#[warn(dead_code)]` on by default


thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Aborted (core dumped)

Leaving aside the warning and the stack overflow error reported by rustc, my question is about the semantics of this operation. Couldn't (snippet 2) desugar into:

impl Default for A {
    fn default() -> A {
        A {
            name: Some("name".to_string()),
            age: Default::default(),
        }
    }
}

What do you think about this? Is there anything blocking this option? I'd be happy to look into this more in detail.

This has been reported in other places such as Use Default::default syntax sugar maybe cause stack overflow · Issue #94930 · rust-lang/rust · GitHub or Print warning when Default::default calls itself · Issue #90513 · rust-lang/rust · GitHub, but I think assuming (snippet 1) is the expected behavior, I think (snippet 2) would also be desired. The idea behind the '..' syntactic sugar is that it calls to Default::default() on the rest of fields not specifically set by the user.

Thank you!

This is asked fairly often, but desugaring to an element-wise Default isn’t really an option because the struct update syntax can take any instance of the struct, not just the default.

let foo = A { name: …, age: … };
let bar = A { age: 42, ..foo };

Thank you for your answer @jdahlstrom!

Yes, I'm aware of this, and of course the only problematic case is when implementing the Default trait and using this feature, I was just wondering if there could be some solution, but that would imply making rustc have a special case for Default what would not be desirable of course. I'll read more about the internals of rustc on how it desugars '..' just for my own knowledge.

You would need a different syntax for such a feature. It could be written like this:

A {
    name: Some("name".to_string()),
    .. // Initialize other fields to their `Default` values.
}

This is similar to the default field values proposal.

3 Likes

For the use case of hand-implementing Default, there could also be a "default default" derive that only exists to provide an instance that the hand-written default method could base itself on. This could be implemented as a third-party crate. Unfortunately, this would still not solve the use case where one or more fields don't have a Default impl at all, and you'd like to default-initialize all the fields that can be, and manually init the rest.

Thanks a lot for the pointer.

A #[derive(Default)] desugars to a Default::default() call on all fields of the struct, automatically generated with a macro. As @jdahlstrom alluded to, a crate like smart-default can do something similar, but optionally override selected fields with a custom value. This also supports implementing a "default" on structs with fields that do not impl Default.

3 Likes

Ah, yes, smart-default is pretty much exactly what you want.

1 Like

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.