Expand description
Rust enums are closed, meaning that the integer value distinguishing an enum, its discriminant, must be one of the variants listed. If the integer value isn’t one of those discriminants, it is considered immediate undefined behavior. This is true for enums with and without fields.
This has some disadvantages:
- in constrained environments, closed enums can require premature runtime checks when using
TryFrom
to convert from an integer. This is doubly true if the value will be checked again at a later point, such as with a C library. - an outdated binary using an enum won’t preserve the value of an unknown field when reserializing
data without an extra
Unrecognized
value making the type more expensive than an integer. - it can introduce Undefined Behavior at unexpected times if the author is unfamiliar with
the rules of writing
unsafe
Rust.
In constrast, C++ scoped enumerations are open, meaning that the enum is a strongly-typed integer that could hold any value, though with a scoped set of well-known values.
The open enum pattern lets you have this in Rust. With a [newtype][newtype] and associated constants, the open_enum macro turns this enum declaration:
#[open_enum]
enum Color {
Red,
Green,
Blue,
Orange,
Black,
}
into a tuple struct with associated constants:
#[derive(PartialEq, Eq)] // In order to work in `match`.
struct Color(pub i8); // Automatic integer type, can be specified with #[repr]
impl Color {
pub const Red: Self = Color(0);
pub const Green: Self = Color(1);
pub const Blue: Self = Color(2);
pub const Orange: Self = Color(3);
pub const Black: Self = Color(4);
}
There are clear readability benefits to using field-less enum
s to represent enumerated integer data.
It provides more type safety than a raw integer, the enum
syntax is consise, and it provides a
set of constants grouped under a type that can have methods.
§Usage
Usage is similar to regular enum
s, but with some key differences.
// Construct an open enum with the same `EnumName::VariantName` syntax.
let mut blood_of_angry_men = Color::Red;
// Access the integer value with `.0`.
// This does not work: `Color::Red as u8`.
assert_eq!(blood_of_angry_men.0, 0);
// Construct an open enum with an arbitrary integer value like any tuple struct.
let dark_of_ages_past = Color(4);
// open enums always implement `PartialEq` and `Eq`, unlike regular enums.
assert_eq!(dark_of_ages_past, Color::Black);
// This is outside of the known colors - but that's OK!
let this_is_fine = Color(10);
// A match is always non-exhaustive - requiring a wildcard branch.
match this_is_fine {
Color::Red => panic!("a world about to dawn"),
Color::Green => panic!("grass"),
Color::Blue => panic!("蒼: not to be confused with 緑"),
Color::Orange => panic!("fun fact: the fruit name came first"),
Color::Black => panic!("the night that ends at last"),
// Wildcard branch, if we don't recognize the value. `x =>` also works.
Color(value) => assert_eq!(value, 10),
}
// Unlike a regular enum, you can pass the discriminant as a reference.
fn increment(x: &mut i8) {
*x += 1;
}
increment(&mut blood_of_angry_men.0);
// These aren't men, they're skinks!
assert_eq!(blood_of_angry_men, Color::Green);
§Integer type
open_enum
will automatically determine an appropriately sized integer1 to
represent the enum, if possible2. To choose a specific representation, it’s the same
as a regular enum
: add #[repr(type)]
.
You can also specify #[repr(C)]
to choose a C int
.34
If you specify an explicit repr
, the output struct will be #[repr(transparent)]
.
#[open_enum]
#[repr(i16)]
#[derive(Debug)]
enum Fruit {
Apple,
Banana,
Kumquat,
Orange,
}
assert_eq!(Fruit::Banana.0, 1i16);
assert_eq!(Fruit::Kumquat, Fruit(2));
Warning:
open_enum
may change the automatic integer representation for a given enum in a future version with a minor version bump - it is not considered a breaking change. Do not depend on this type remaining stable - use an explicit#[repr]
for stability.
§Aliasing variants
Regular enum
s cannot have multiple variants with the same discriminant.
However, since open_enum
produces associated constants, multiple
names can represent the same integer value. By default, open_enum
rejects aliasing variants, but it can be allowed with the allow_alias
option:
#[open_enum(allow_alias)]
#[derive(Debug)]
enum Character {
Viola = 0,
Cesario = 0,
Sebastian,
Orsino,
Olivia,
Malvolio,
}
assert_eq!(Character::Viola, Character::Cesario);
§Custom debug implementation
open_enum
will generate a debug implementation that mirrors the standard #[derive(Debug)]
for normal Rust enums
by printing the name of the variant rather than the value contained, if the value is a named variant.
However, if an enum has #[open_enum(allow_alias)]
specified, the debug representation will be the numeric value only.
For example, this given enum,
#[open_enum]
#[derive(Debug)]
enum Fruit {
Apple,
Pear,
Banana,
Blueberry = 5,
Raspberry,
}
will have the following debug implementation emitted:
fn fmt(&self, fmt: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
#![allow(unreachable_patterns)]
let s = match *self {
Self::Apple => stringify!(Apple),
Self::Pear => stringify!(Pear),
Self::Banana => stringify!(Banana),
Self::Blueberry => stringify!(Blueberry),
Self::Raspberry => stringify!(Raspberry),
_ => {
return fmt.debug_tuple(stringify!(Fruit)).field(&self.0).finish();
}
};
fmt.pad(s)
}
§Compared with #[non_exhuastive]
The non_exhaustive
attribute indicates that a type or variant
may have more fields or variants added in the future. When applied to an enum
(not its variants),
it requires that foreign crates provide a wildcard arm when match
ing.
Since open enums are inherently non-exhaustive5, this attribute is incompatible
with open_enum
. Unlike non_exhaustive
, open enums also require a wildcard branch on match
es in
the defining crate.
§Disadvantages of open enums
- The kind listed in the source code, an
enum
, is not the same as the actual output, astruct
, which could be confusing or hard to debug, since its usage is similar, but not exactly the same. - No niche optimization:
Option<Color>
is 1 byte as a regular enum, but 2 bytes as an open enum. - No pattern-matching assistance in rust-analyzer.
- You must have a wildcard case when pattern matching.
match
es that exist elsewhere won’t break when you add a new variant, similar to#[non_exhaustive]
. However, it also means you may accidentally forget to fill out a branch arm.
Like regular
enum
s, the declared discriminant for enums without an explicitrepr
is interpreted as anisize
regardless of the automatic storage type chosen. ↩This optimization fails if the
enum
declares a non-literal constant expression as one of its discriminant values, and falls back toisize
. To avoid this, specify an explicitrepr
. ↩This requires either the
std
orlibc_
feature (note the underscore) ↩Note that this might not actually be the correct default
enum
size for C on all platforms, since the compiler could choose something smaller thanint
. ↩Unless the enum defines a variant for every value of its underlying integer. ↩
Attribute Macros§
- open_
enum - Constructs an open enum from a Rust enum definition, allowing it to represent more than just its listed variants.