Private fields and non-exhaustive structs and base structs

I have a non-exhaustive struct like this:


#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
    pub event_interval: u32,
    pub ring_entries: u32,
    #[cfg(target_os = "linux")]
    pub mode: Mode,
}

While documenting the struct I noticed the following doc example didn't compile:

let config = Config {
    #[cfg(target_os = "linux")]
    mode: Mode::Polling { idle_timeout: 100 },
    .. Config::default()
};

let runtime = config.build()?;
runtime.block_on(async {
    /* ... */
});

That is because base structs cannot be used with non exhaustive structs, since base structs are considered struct expressions.


error[E0639]: cannot create non-exhaustive struct using struct expression
  --> src/runtime/config.rs:14:14
   |
7  |   let config = Config {
   |  ______________^
8  | |     #[cfg(target_os = "linux")]
9  | |     mode: Mode::Polling { idle_timeout: 100 },
10 | |     .. Config::default()
11 | | };
   | |_^

error: aborting due to previous error

using a dummy priv_: () field didn't work either. It would be nice if this were allowed, since this case is not actually broken when fields are added to the type. And I feel that base struct initialization often looks cleaner than the builder types pattern.

#[non_exhaustive] explicitly allows you to add new non-pub fields to the type. There's been various discussions around a #[non_exhaustive(pub)] option which promises future fields to be pub, but it's not gone anywhere yet.

The root disconnect here is that Struct { ..default } is sugar for splatting the rest of the fields from default into the struct expression, e.g. Struct { $(field: default.field,)* }, so you need access to all of the splatted fields in order to move them into the new struct.

The other direction seems to make more sense to a lot of people, where Struct { $(field: value,)* ..default } is closer to sugar for setting $(field)* on default, thus allowing private fields to be copied over, e.g. { $(default.field = value;)* default }.

So the typical suggestion is to use an s! macro to get those semantics:

macro_rules! s {
    {
        $($field:ident: $value:expr),* $(,)?
    } => {
        $crate::s! {
            $($field: $value,)*
            ..::core::default::Default::default()
        }
    };
    
    {
        $($field:ident: $value:expr,)*
        ..$splat:expr
    } => {
        // match instead of let; something about temporary lifetimes
        match $splat {
            mut s => if false {
                // this branch is a trick to help out type inference
                s
            } else {
                $(s.$field = $value;)*
                s
            }
        }
    }
}

(This clashes with other uses of s!, e.g. windows uses it for what I'd call cstr8!. I don't know a better name; perhaps r! (for record)? Ultimately, the solution is probably first-class field defaults, which handles the Default::default() case which is by-and-large the majority use case.)

(The absolute impossibility of searching for s! means I have to reinvent it every time...)

There are meaningful differences between the two. Notably, because the former only moves the fields that need to be moved, it can be used to copy fields from behind a reference if the field's types are Copy even if the struct itself isn't (example), as well as enabling (generic) type changing FRU.

2 Likes

Rather than a dummy priv_: () field, if you do plan to only add pub fields you can use something like

#[derive(Clone, Debug)]
pub struct Config {
    pub event_interval: u32,
    pub ring_entries: u32,
    #[cfg(target_os = "linux")]
    pub mode: Mode,
    #[doc(hidden)]
    /// Do not access other than with .., here to emulate `#[non_exhaustive(pub)]`
    pub _do_not_use_ඞ: (),
}

Which assuming users obey the doc-comments acts exactly the same as non_exhaustive(pub) would.

3 Likes

Its a bit sad to have to use this but I don't think there is a better way.

You might be interested in the discussions that happened related to

(Be sure to see the Zulip conversation too.)

4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.