Is it possible to disable the effect of #[non_exhaustive] given to an enum?

Using an enum with the #[non_exhaustive] attribute forces me to do a runtime check like this;

use std::num::IntErrorKind;

fn f(kind: IntErrorKind) {
    match kind {
        IntErrorKind::Empty => todo!(),
        IntErrorKind::InvalidDigit => todo!(),
        IntErrorKind::PosOverflow => todo!(),
        IntErrorKind::NegOverflow => todo!(),
        IntErrorKind::Zero => todo!(),
        // This arm is required due to #[non_exhaustive].
        _ => todo!(),
    }
}

If I omit the last match arm, I get this error;

error[E0004]: non-exhaustive patterns: `_` not covered
  --> src/main.rs:4:11
   |
4  |     match kind {
   |           ^^^^ pattern `_` not covered

When a third party library adds a new variant to an enum, I would prefer a compile error to a runtime error.

Is there any way to disable the #[non_exhaustive] given to a third party enum when I agree to do so?

Allowing opt-out would undermine the non-exhaustive feature. The only thing you can do is add a catch-all match arm or convince the crate maintainers to remove the #[non_exhaustive] attribute.


There is a hack that will fail to link the binary when a non-exhaustive type is changed:

// Define an external function that will not exist
extern "C" {
    fn _link_error();
}

fn f(kind: IntErrorKind) {
    match kind {
        IntErrorKind::Empty => todo!(),
        IntErrorKind::InvalidDigit => todo!(),
        IntErrorKind::PosOverflow => todo!(),
        IntErrorKind::NegOverflow => todo!(),
        IntErrorKind::Zero => todo!(),
        // Fail to link when IntErrorKind adds new variants that are not covered
        _ => unsafe { _link_error() },
    }
}

It will compile only if all variants are covered. If any variants are not covered, you will get an obnoxious linker error. The error message will tell you which source lines are responsible, but it's always going to be hard to read. It can't be used in ![forbid(unsafe_code)] crates. It's not ideal for several reasons.

13 Likes

There is also an unstable non_exhaustive_omitted_patterns lint, but it requires a nightly toolchain:

#![feature(non_exhaustive_omitted_patterns_lint)]
#![deny(non_exhaustive_omitted_patterns)]

use std::num::IntErrorKind;

fn f(kind: IntErrorKind) {
    match kind {
        IntErrorKind::Empty => todo!(),
        IntErrorKind::InvalidDigit => todo!(),
        IntErrorKind::PosOverflow => todo!(),
        IntErrorKind::NegOverflow => todo!(),
        IntErrorKind::Zero => todo!(),
        // This arm is required due to #[non_exhaustive].
        _ => todo!(),
    }
}

(playground)

That will error if you comment out one of the explicit match arms. It still requires the final catch-all branch though.

8 Likes

Thanks all.

On the other hand, one could say that #[non_exhaustive] also undermines Rust's powerful exhaustive matching feature.

Having said that, non_exhaustive_omitted_patterns will be a good solution that strikes a balance between library developers who want to use #[non-exhaustive] and consumers who want exhaustive matching. I'd like to use that once it is stabilized.

5 Likes

I agree there is a natural tension between a consumer wanting to avoid runtime errors and a provider wanting to avoid breaking changes. And I would personally err on the side of not using the attribute. It hasn't ever been used it in any of my crates, and it's unlikely to ever appear in them.

My personal philosophy here is that breaking changes should be communicated effectively with SemVer and that breaking changes are broadly healthy for the ecosystem. So, #[non_exhasutive] not only undermines exhaustive matching, but also the responsibility of crate maintainers to control breaking changes in their public types.

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.