14
$\begingroup$

In Java, enum values behave like objects (since they are basically syntactic sugar for objects) and they can have their own class methods. In C++, however, enums are essentially an alias for an integer constant.

What are some possible situations where a Java type enum would be better/make more sense to be used and when would a C++ enum make more sense in a language?

$\endgroup$
3
  • 17
    $\begingroup$ There's a third kind of enum: sum types, as in Swift and Rust. $\endgroup$
    – Bbrk24
    Jul 26, 2023 at 15:42
  • $\begingroup$ Also, Typescript's enums can be string constants, not just integers. $\endgroup$
    – kaya3
    Jul 26, 2023 at 15:51
  • 3
    $\begingroup$ ... and then there's C#, which provides a C++-style enums, but allows to use extension methods on them. TBH you can even use extension methods on "primitive" types in C#, and that's one of the reasons I changed my mind about C# after 3.0 $\endgroup$
    – user213769
    Jul 27, 2023 at 4:25

4 Answers 4

21
$\begingroup$

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:

  1. 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.
  2. Haskell has the second most versatile enums. They are first-class citizens of the language, and sum types are just awesome programming tools.
  3. Java has the third most versatile enums. They are first-class citizens of the language, but are uniform.
  4. 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.

$\endgroup$
13
  • $\begingroup$ Swift has type-safe bitwise flags using OptionSet: langdev.stackexchange.com/a/1671/15 So that missing functionality of an enum isn't really felt. $\endgroup$
    – Bbrk24
    Jul 27, 2023 at 13:32
  • $\begingroup$ @Bbrk24: I'm... not enthused, to be honest. I much prefer a bitset wrapper taking an arbitrary integral-like enum, since this way I can use the enum E by itself, when needed, and have my EnumSet<E> and EnumMap<E, V>. $\endgroup$ Jul 27, 2023 at 14:34
  • $\begingroup$ Really, this is beneficial because of Swift's base type inference rules. Instead of edges(EDGE_LEFT | EDGE_TOP) or edges(edge::left | edge::top) as in C++, Swift allows you to say edges([.left, .top]). There's no namespace pollution nor loss of type safety, yet it's briefer. Plus, you don't have to worry about whether it takes a Set<Edge> or an OptionSet type: the API is the same. $\endgroup$
    – Bbrk24
    Jul 27, 2023 at 14:38
  • $\begingroup$ std::variant in C++ is an interesting "enum". I sometimes do std::variant<std::integral_constant<...>...> even. $\endgroup$ Jul 28, 2023 at 3:52
  • 1
    $\begingroup$ @Yakk-AdamNevraumont: It's not part of the language... and its ergonomics suffer from that. The difference between std::visit and match (Rust) is just night and day :( $\endgroup$ Jul 28, 2023 at 6:49
7
$\begingroup$

In C++ you can have a plain enum or an enum class, and the enum class is similar in some way to Java enums. So plain enum are more likely the one you are describing here when you say C++ enum.

In that case, enums are indeed aliases for integer constants: they convert automatically to the underlying type, even you don't want to, from a static typing point of view there is little difference. There can be values that belong the an enum type even if there is no enum with that value: if you define three values A, B and C (resp. bound to 0, 1 and 2), then A|B|C is a bitwise-or value equal to 3, which takes 2 bits and fits the underlying type of the enum, however this is not a proper enum value by itself.

A Java-type enum object can list all its values at runtime. The set of values is restricted to only the values that are defined by the enum, you cannot use an enum and an int interchangeably, you have to call the associated methods to convert from and to integers, from and to strings, etc.

I think there is little added-value for plain enums compared to, e.g. constants (or even preprocessor variables). A group of constants declarations is almost equivalent: the notation is shorter with plain enums, and there is an additional relationship between constants that is expressed by the construct. Only enum classes introduce a scope, for example (nb. compared to Java enums, enum classes in C++ don't have reflection).

Java-style enums are the standard now I think for any high-level language, when it is acceptable to have a little bit of runtime features that help the programmer. Writing low-level, resource contrained code is maybe the only case where I see a benefit in using plain enums.

$\endgroup$
14
  • 2
    $\begingroup$ Plain enums have the benefit that they can be contained within a scope, making them more useful than preprocessor directives because they aren't forced to pollute the global namespace. This allows them to be treated similarly to more proper objects by placing them inside a struct, though at that point you're essentially just reinventing Java-style enums. (That said, it's also worth pointing out that as truly constant values, enum-in-struct was often used to provide constexpr-like functionality before C++11, for things like std::string::npos or template metaprogramming.) $\endgroup$ Jul 27, 2023 at 6:13
  • $\begingroup$ but plain enums are unscoped? $\endgroup$
    – coredump
    Jul 27, 2023 at 7:22
  • 2
    $\begingroup$ @coredump: Not quite, the enumerators of plain enums are in the enclosing scope, so it's still better than macros which are in the global scope -- well, in C++ there are enclosing scopes (namespaces, class/struct); in C there's only the global scope where enums can be declared, so there they lose value indeed. $\endgroup$ Jul 27, 2023 at 12:18
  • $\begingroup$ @MatthieuM. I edited the answer to focus on constants, I agree that preprocessor macros are just not equivalent (their only advantage is they can be stringified). $\endgroup$
    – coredump
    Jul 27, 2023 at 12:35
  • 2
    $\begingroup$ @coredump: There actually is an advantage to using an enum for a group of constants => lints. C and C++ compilers have warning if you forget to handle an enumerator in a switch, and no equivalent for constants. There are also hacks to enumerate the enumerators of an enum in C++ (magic meta-template wizardry), for pseudo-reflection. $\endgroup$ Jul 27, 2023 at 12:44
6
$\begingroup$

C++ enums are essentially just int constants. This makes them efficient to implement, and usable for low-level or performance-critical programs.

OTOH, Java-style enums have full class functionality. They provides better type safety (since you can't just implicitly convert them to/from int). It also lets you define fields and methods for additional functionality (e.g., custom string formatting and parsing).

Of course, you're not constrained to what's provided by your language's enum keyword. It's possible to implement a C++ class that behaves like a Java enum, or a Java class that just defines a bunch of public static final int constants like a C(++) enum.

To recap,

  • Java-style (class-based) = for richer functionality, type safety, and object-oriented design.
  • C(++) style (integer-based) = for low-level or performance-critical code, or interop with legacy C code.
$\endgroup$
6
  • $\begingroup$ Since Java enums are final, a large part of their functionality could be desugared into free functions and global variables instead. They could be boxed upon being converted to Object, if your language requires it $\endgroup$
    – user253751
    Jul 26, 2023 at 17:09
  • 1
    $\begingroup$ It's worth noting that "since you can't just implicitly convert them to/from int" doesn't really depend on whether the enums are classes or just ints under the hood. The compiler can still refuse to let you cast implicitly, or even do the reverse and allow implicitly casting an int to an enum class $\endgroup$ Jul 27, 2023 at 4:58
  • 13
    $\begingroup$ Mind you, if we're talking language design rather than the specific features of C++ and Java, there's no reason that "it's just an int under the hood" style enums couldn't have type safety; C++ chose to make them implicitly convertable with integers. Likewise Java could have allowed class methods for enums syntactically while actually representing them as primitives and static functions. These are not inherent consequences of making enums represented as primitive ints or not, they are specific choices the language designers made (even if only to make things easier for implementers). $\endgroup$
    – Ben
    Jul 27, 2023 at 5:04
  • $\begingroup$ I am not certain if you could get the same effect in Java as in C++. Not without completely doing away type safety and use simple ints as you suggested. Even plain enums has some type checking. You could definitely create Java style enums in C++. $\endgroup$ Jul 27, 2023 at 13:16
  • $\begingroup$ If enumerations use compiler-assigned integer constants, a system could easily construct a singleton object for each value, and have an array of object references which can convert from an integer to an object every time boxing would be required, with no need to create a new object on every conversion. $\endgroup$
    – supercat
    Jul 27, 2023 at 20:09
1
$\begingroup$

When programming in C++ I sometimes miss the additional methods that come automatically with Java enums, especially an easy way to converted into string representation when logging, but also for parsing, obtaining next or previous value in the order and the like. In C++, you need to write your own free-standing functions to replace them. Free-standing functions are then quite difficult to find by another programmer who is not aware of them.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .