Skip to content

enum or enum class: Either Way, Use Them

amirroth edited this page Sep 17, 2021 · 13 revisions

Enumerations, sometimes called enumerated sets, are an important part of any programming language. Even Fortran had enumerations starting in 2003, but having started with older versions of Fortran EnergyPlus did not use them. Enumerations were part of the original C language specifications using the keyword enum.

enum TimeStepType {
   System, // implied value of first element is 0, can be overridden with = <value>
   HVAC, // implied value of element is value of previous element + 1, can be overridden with = <value>
   Zone,
   Plant
};

enum TimeStepType ts = HVAC;

A C-style enum is essentially a subtype of int. Because of this, you can also assign enum values to int variables and compare enum variables to int values directly. This is considered "type unsafe" (not by me personally, by the internet) and is therefore frowned upon. Note, you cannot assign an int value to an enum because subtyping does not work in that direction, the right-hand side always has to be either the same type or a subtype of the left-hand side in an assignment. If this were the case, then that would truly be type unsafe, but it is not.

int tsint = HVAC; // this is allowed but is considered type unsafe
bool isTSZone = (ts == 3); // this is also allowed and is also considered type unsafe
enum TimeStepType ts = 3; // this is an error

Before C++-11 there was also the possibility of name clashing between different enums or enum's and variables because enum's were considered part of the enclosing name scope. Starting in C++-11, enum's are still considered part of the enclosing scope, but they can also be explicitly scope-resolved using the :: operator to avoid name clashes. This is good!

enum TimeStepType ts = TimeStepType::HVAC;

enum class

The C++-11 standard introduced the enum class construct which is both its own name scope (i.e., explicit scope resolution is required) and not an implicit subtype of int.

enum class TimeStepType {
   System, // implied value of first element is 0, can be overridden with = <value>
   HVAC, // implied value of element is value of previous element + 1, can be overridden with = <value>
   Zone,
   Plant
};

TimeStampType ts = HVAC; // this is an error, HVAC scope must be resolved
int tsint = TimeStepType::HVAC; // this is also an error, TimeStepType is not a subtype of int
bool isTSZone = (ts == 3); // this is also an error for the same reason
TimeStepType ts = 3; // this was an error before and is still an error

Note, putting : int after an enum class declaration:

enum class TimeStepType : int {
};

Does not make it into a subtype of int, it only specifies that the size of the variable has to be the size of int. Since : is used to indicated subtyping in the class construct, this is confusing. Anyway, the same internet considers enum class to be be preferable to enum, so there you have it.

enum's of any kind are worlds of fun! Let's see some examples.

enum's as array indices

enum classes are explicitly not subtypes of int, but they are actually implemented as int's and it is often useful to treat them that way. You can treat a enum class as an int using static_cast<int>(). static_cast<>() does not generate a function call or any other runtime code, it is a way of telling the compiler "I know what I am doing! I know that treating a enum class as an int is unsafe in the general case, but in this specific case it is safe because I know something about the range of integers that will be generated."

A common reason to use enum classes as ints is to use them as indices in an array. A common example: mapping enum classes to strings and back. (If interested, here are short tutorials on constexpr[constexpr](https://github.com/NREL/EnergyPlus/wiki/constexpr:-your-new-best-friend) and std::string_view)

enum class TimeStepType {
   Invalid = -1, // this is the only "good programming" use of a negative enum, i.e., error
   System, 
   HVAC, 
   Zone,
   Plant,
   NUM // good hygiene to name the last member of the enum NUM so that it can be used as the number of elements in the enum
};

constexpr std::array<std::string_view, TimeStepType::NUM> TimeStepTypeNamesUC = { // notice how NUM is used here
   "SYSTEM", 
   "HVAC", 
   "ZONE", 
   "PLANT"};

// Print out all TimeStepTypes, note use of NUM and static_cast<int>
for (int i = 0; i < static_cast<int>(TimeStepType::NUM); ++i)
   std::cout << TimeStepTypeNamesUC[i] << std::endl; 

// A function that converts TimeStepType name to the enumeration.
TimeStepType
getTimeStepType(std::string_view name)
{
   for (int i = 0; i < static_cast<int>(TimeStepType::NUM); ++i)
      if (TimeStepTypeNamesUC[i] == name) 
         return static_cast<TimeStepType>(i);
   return TimeStepType::Invalid;
}

// A general function that converts any name to the corresponding enumeration.
int 
getEnumerationValue(const gsl::span<std::string_view> list, std::string_view name)
{
   for (int i = 0; i < list.size(); ++i)
      if (list[i] == name)
         return i;
   return -1;
}

getEnumerationValue is an EnergyPlus function and the combination of this function and constexpr std::array<std::string_view, NUM> is the preferred way of converting enumeration names to values. Please do not use a std::map to do this. A std::map makes sense for some things , specially large dynamic data sets, but not for this. A std::map is a heap-allocated red-black binary tree and cannot be made constexpr. EnergyPlus may spend more time setting up the std::map than actually doing lookups in it. (See short tutorial on containers)

enum's and switch/case

Another good use of enum's is in switch/case statements. The switch/case construct is a good (and fast) replacement for if-else-if logic, especially when there is not a single dominant case, i.e., one case that occurs at least 80% of the time. Instead of:

if (shading == WinShadingType::IntShade) {
   ...
} else if (shading == WinShadingType::ExtShade || shading == WinShadingType::ExtScreen) {
   ... 
} else if (shading == WinShadingType::ExtScreen) {
   ...
} else if (

You can use:

switch (shading) {
case WinShadingType::IntShade: {
      ...
   }
   break; // use break between cases otherwise the code will "drop" to the next case. 
case WinShadingType::ExtShade:
case WinShadingType::ExtScreen: { // putting two case statements together like this is the same as putting an || in the conditional
      ... 
   }
   break;
...
default: // the compiler may complain if you don't put in a default statement
   assert(false); // use an assert if you do not plan on ever getting here
}

The switch statement uses an array of code addresses (called a jump table or a code pointer table) indexed by the enum itself to jump to any case in only three instructions, as opposed to having to test the cases sequentially. Having compact enum's that start at 0 is important for keeping the jump table to a reasonable size. Here are the pseudo-instructions:

ADD JUMP-TABLE, R1 -> R2 // assume shading variable is in register R1, the address of JUMP-TABLE is a compile time constant and can be hard-coded
LOAD R2 -> R2
JUMP-INDIRECT R2

The switch statement is fast and also makes the code look clean, but it has some limitations. Specifically, the tests can only be on a single int/enum/enum class variable and they can only be equality tests. This restriction is what enables the use of the int/enum/enum class as an index in the jump table. Incidentally, when you use enum class in a switch statement the compiler implicitly applies static_cast<int> to them. So much for using enum class as int being "type unsafe".

Clone this wiki locally