-
Notifications
You must be signed in to change notification settings - Fork 396
enum or enum class: Either Way, Use Them
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 enum
s 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;
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 class
es 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 class
es as int
s is to use them as indices in an array. A common example: mapping enum class
es 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)
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".