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 can make working with enums troublesome in high performance code that can’t afford premature
runtime checks.
It can also 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.
open_enum
lets you have this in Rust. It 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 u8); // Automatic integer type, can be specified.
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 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 value 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);
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
- 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
Constructs an open enum from a Rust enum definition, allowing it to represent more than just its listed variants.