For a complete comparison, we should add a 3rd (and 4th?) type of enum
: Sum Types, as in Haskell or Rust.
Scope
C and C++ plain enum
are unscoped. That is, their enumerators are part of the enclosing scope of the declaration1. This is largely seen as undesirable.
A contrario, Java, Haskell, and Rust's "enumerators" are scoped.
1 In the early days of C, struct data members were also unscoped. This is why struct tm
data members are all prefixed with tm_
. struct data members were changed to be scoped, but enums were not.
Full vs Partial
C and C++ enum
(plain or class
) are partial, they are not guaranteed to hold a value matching an enumerator, and may instead hold any value achievable by bitwise ORing enumerators or, in the case of C++, any value which may fit in the specified underlying type, if any.
Java, Haskell, and Rust fully define the valid enumerators, and no other value is supported.
Advantage C/C++
enum
can be used as a set of flags.
Advantage Java/Haskell/Rust
- Most
enum
are not meant to be used as set of flags, so not having to worry about an exotic unnamed state is generally beneficial.
Advantage Rust
- Niche optimizations mean that
Option<E>
where E
is an enum, has the same size as E
.
Overlap
C and C++ enum
may have multiple "equal" enumerators, that are indistinguishable from one another.
Java, Haskell, and Rust cannot have indistinguishable enumerators.
Constant
C and C++ enum
are integers in disguise, their integer value can be specified and they can always be converted down to their integer representation. Similarly, in-range integers can be converted to enum
.
Rust enum
with no payloads can have a specified underlying integer representation (#[repr(u8)]
is common) and specified integer values, in which case they can be converted to this representation (e as u8
). There is no automatic conversion back from integer, as not all values may be valid enum values, but a user-written conversion function can be optimized down to the equivalent cast.
Java enum
can be converted to an int
using the ordinal
method, the integer value is the index of the enumerator in the declaration.
Advantage C/C++/Rust
enum
can be used to enumerate a fixed set of related integral constants, at no run-time cost.
Payload
Java enum
can have a state, though all enumerators must have the same set of fields.
Haskell and Rust enums are full sum types which can have a different set of fields per enumerator.
Advantage Java
- Per-enumerator state is easier to visual than a set of method each mapping an integer value to a specific state.
Advantage Haskell/Rust
- Full blown sum types are a delight to program with, allowing expressing clearly which set of fields are relevant to a particular situation. The
Maybe
(Haskell) and Option
(Rust) types completely eliminate the Billion Dollars mistake that is null, for example.
Methods
C and C++ enum
do not support methods.
Java, Haskell, and Rust enums are first-class user-defined types of the language, and support everything user-defined types do: associated constants, inherent methods, inheritance (Java) or typeclass/trait (Haskell/Rust) implementations, etc...
Summary
First, a classification by versatility:
- Rust has the most versatile enums. They are first-class citizens of the language, while being able to boil down to an efficient integral representation.
- Haskell has the second most versatile enums. They are first-class citizens of the language, and sum types are just awesome programming tools.
- Java has the third most versatile enums. They are first-class citizens of the language, but are uniform.
- C and C++ have the least versatile enums. They are second-class citizens of the language, and may hold unnamed values. They have very efficient integral representation, though.
Getting down to the details, the one task that C and C++ enums can fulfill better than others is the role of set of flags if the enumerators are carefully defined. Java, Haskell, and Rust require user-written code to efficiently encode enum sets of integers, with Rust (at least) being able to optimize that code down to same assembly as C or C++.
Otherwise, first-class citizens enum
are always preferable, and sum-types even more preferable.