For a complete comparison, we should add a 3rd (and 4th?) type of
enum: Sum Types, as in Haskell or Rust.
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.
enum can be used as a set of flags.
enum are not meant to be used as set of flags, so not having to worry about an exotic unnamed state is generally beneficial.
- Niche optimizations mean that
E is an enum, has the same size as
C and C++
enum may have multiple "equal" enumerators, that are indistinguishable from one another.
Java, Haskell, and Rust cannot have indistinguishable enumerators.
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 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.
enum can be converted to an
int using the
ordinal method, the integer value is the index of the enumerator in the declaration.
enum can be used to enumerate a fixed set of related integral constants, at no run-time cost.
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.
- Per-enumerator state is easier to visual than a set of method each mapping an integer value to a specific state.
- 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.
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...
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.