1

What if a library crate defines an enum that has a variant that is feature-gated?

#[non_exhaustive]
enum Foo {
    A,
    B,
    #[cfg(feature = "some-feature")]
    Gated,
}

This is a naïve attempt to allow the enum Foo to support the optional feature with the Gated variant, while also allowing clients who do not need the feature to opt out of the costs associated with it (by disabling the crate feature some-feature).

What are the potential dangers and/or costs associated with doing this? Are there any compelling reasons for avoiding this pattern?

RBF06
  • 2,013
  • 2
  • 21
  • 20
  • As a side note, I'm not sure how I feel about `#[non_exhaustive]` on enums. I think in some cases it can make sense where you rarely want to match all the possible variants (think keyboard scancodes in a GUI application) but in others, I think it actually causes more problems than it solves. It forces a `_` arm whenever enum values are matched, which in many cases will just panic. This means that introducing a new enum variant creates a runtime error instead of a compile-time error which is the _exact opposite_ of what I would usually want. – cdhowie Mar 01 '23 at 03:56
  • 2
    That said, I am going to vote to close this as opinion-based. _My_ opinion is that there is nothing wrong with cfg-gated enum variants, but I would remove `#[non_exhaustive]`. If another variant springs into existence due to features being changed, I would _want_ the compiler to tell me all of the places in my codebase that I need to look at to consider the new variant. – cdhowie Mar 01 '23 at 03:57
  • 1
    Posted the slightly modified question on URLO - https://users.rust-lang.org/t/a-possible-edge-case-for-semver/90117. In short, the answer on that (modified) question is "without `#[non_exhaustive]`, this is explicitly disallowed by Cargo conventions". – Cerberus Mar 01 '23 at 07:10
  • @cdhowie I believe that if a feature-gated enum variant exists, the enum should always be marked with `#[non_exhaustive]` because "Enabling a feature should never introduce a semver incompatible change" – RBF06 Mar 10 '23 at 00:03
  • @RBF06 That's super unfortunate. It removes one of the primary advantages of compile-time exhaustive match checking. – cdhowie Mar 10 '23 at 06:37

1 Answers1

3

If the enum is not marked with #[non_exhaustive], this may lead to an unexpected and effectively unsolvable breakage on the binary crate's side.

Imagine the following case:

  • Your library foo exports this enum:
pub enum Foo {
    A,
    B,
    #[cfg(feature = "some-feature")]
    Gated,
}
  • Library bar depends on foo without feature and matches on foo::Foo, handling Foo::A and Foo::B and ignoring Foo::Gated, since it doesn't exist in this confiuguration.
  • Binary baz depends both on bar and on foo, with feature.

Then, bar by itself works fine, but attempt to build baz will fail, since features will be unified across dependency tree, and when some-feature is active, bar breaks.

Cerberus
  • 8,879
  • 1
  • 25
  • 40
  • I see. Are there any dangers or reasons for avoiding this pattern even when the enum is marked with `#[non_exhaustive]`? – RBF06 Mar 02 '23 at 04:04
  • For me, is would just look very surprising (that is, I'd question why it was ever needed if found this in the wild). But probably not dangerous. – Cerberus Mar 02 '23 at 04:48