Skip to content

enum or enum class: Either Way, Use Them

amirroth edited this page Jan 12, 2023 · 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 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, static_cast<int>(TimeStepType::NUM)> // NUM is used to size the array, must be cast to int
   TimeStepTypeNamesUC = { 
      "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 this page on different C++ standard library containers and when to use which)

enum's as Array Indices: Part 2 (Electric Boogaloo)

An important aspect of using enum's is learning to see them where they don't already exist. Check out this error/diagnostic-tracking/printing example from the EnergyRecovery module. This is actually a subset of the machinery, but you will get the idea.

// regen inlet relative humidity for temperature equation                                                                 
bool PrintRegenInRelHumTempMess;      // - flag to print regen in RH error message for temp eq                            
int RegenInRelHumTempErrIndex;        // - index to recurring error struc for regen outlet hum rat                        
int RegenInRelHumTempErrorCount;      // - counter if regen outlet temp limits are exceeded                               
std::string RegenInRelHumTempBuffer1; // - buffer for RegenOutHumRat warn mess on following timstep                       
std::string RegenInRelHumTempBuffer2; // - buffer for RegenOutHumRat warn mess on following timstep                       
std::string RegenInRelHumTempBuffer3; // - buffer for RegenOutHumRat warn mess on following timstep                       
Real64 RegenInRelHumTempLast;         // - last value of regen outlet humidity ratio                                      
// process inlet relative humidity for temperature equation                                                               
bool PrintProcInRelHumTempMess;      // - flag to print regen in RH error message for temp eq                             
int ProcInRelHumTempErrIndex;        // - index to recurring error struc for regen outlet hum rat                         
int ProcInRelHumTempErrorCount;      // - counter if regen outlet temp limits are exceeded                                
std::string ProcInRelHumTempBuffer1; // - buffer for RegenOutHumRat warn mess on following timstep                        
std::string ProcInRelHumTempBuffer2; // - buffer for RegenOutHumRat warn mess on following timstep                        
std::string ProcInRelHumTempBuffer3; // - buffer for RegenOutHumRat warn mess on following timstep                        
Real64 ProcInRelHumTempLast;         // - last value of regen outlet humidity ratio                                       
// used when regen outlet humrat is below regen inlet humrat, verify coefficients warning issued                          
bool PrintRegenOutHumRatFailedMess;      // - flag for regen outlet hum rat error message                                 
int RegenOutHumRatFailedErrIndex;        // - index to recurring error struc for regen outlet hum rat                     
int RegenOutHumRatFailedErrorCount;      // - counter if regen outlet temp limits are exceeded                            
std::string RegenOutHumRatFailedBuffer1; // - buffer for RegenOutHumRat warn mess on following timstep                    
std::string RegenOutHumRatFailedBuffer2; // - buffer for RegenOutHumRat warn mess on following timstep                    
std::string RegenOutHumRatFailedBuffer3; // - buffer for RegenOutHumRat warn mess on following timstep                    
Real64 RegenOutHumRatFailedLast;         // - last value of regen outlet humidity ratio                                   
// used when regen and process mass flow rates are not equal to within 2%                                                 
bool PrintImbalancedMassFlowMess;      // - flag for imbalanced regen and process mass flow rate                          
int ImbalancedFlowErrIndex;            // - index to recurring error struc for imbalanced flow                            
int ImbalancedMassFlowErrorCount;      // - counter for imbalanced regen and process mass flow rate                       
std::string ImbalancedMassFlowBuffer1; // - buffer for imbalanced regen and process mass flow rate                        
std::string ImbalancedMassFlowBuffer2; // - buffer for imbalanced regen and process mass flow rate                        
std::string ImbalancedMassFlowBuffer3; // - buffer for imbalanced regen and process mass flow rate                        
Real64 ImbalancedMassFlowLast;              // - last value of heat exchanger mass flow rate imbalance                         ```

Here is the code file:

// print error when regeneration inlet relative humidity is outside model boundaries                      
if (state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex).PrintRegenInRelHumTempMess) {
    ++state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex).RegenInRelHumTempErrorCount;
    if (state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex).RegenInRelHumTempErrorCount < 2)              {
        ShowWarningError(state,
                         state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex).RegenInRelHumTempBuffer1);
        ShowContinueError(state,
                          state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex).                                          .RegenInRelHumTempBuffer2);
        ShowContinueError(state,
                          state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex).RegenInRelHumTempBuffer3);
        ShowContinueError(state,
                          "...Using regeneration inlet air relative humidities that are outside the regeneration outlet temperature "
                          "equation model boundaries may adversely affect desiccant model performance. Verify correct model"
                          "coefficients.");
    } 
}

Here's a slightly more readable version of the code file that uses a local reference variable to shorten the chain of indexed array lookups:

// print error when regeneration inlet relative humidity is outside model boundaries                      
auto &balDesDehumPerfData(state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex));
 
if (balDesDehumPerfData.PrintRegenInRelHumTempMess) {
    ++balDesDehumPerfData.RegenInRelHumTempErrorCount;
    if (balDesDehumPerfData.RegenInRelHumTempErrorCount < 2)              {
        ShowWarningError(state, balDesDehumPerfData.RegenInRelHumTempBuffer1);
        ShowContinueError(state, balDesDehumPerfData.RegenInRelHumTempBuffer2);
        ShowContinueError(state, balDesDehumPerfData.RegenInRelHumTempBuffer3);
        ShowContinueError(state,
                          "...Using regeneration inlet air relative humidities that are outside the regeneration outlet temperature "
                          "equation model boundaries may adversely affect desiccant model performance. Verify correct model"
                          "coefficients.");
    } 
}

Now, that we have a more readable code, consider this enum-based implementation instead:

enum class HeatRecoveryError_t : int { 
    Invalid = -1,
    RegenInRelHumTemp, 
    ProcInRelHumTemp, 
    RegenOutHumRatFailed,
    ImbalancedMassFlow,
    NUM
};

struct HeatRecoveryError_c {
    bool PrintMessage;
    int ErrorIndex; 
    int ErrorCount;
    std::string buffer1, buffer2, buffer3;
    Real64 lastValue;
};

std::array<HeatRecoveryError_c, static_cast<int>(HeatRecoveryError_c::NUM)> errors;

And here is the code file:

// print error when regeneration inlet relative humidity is outside model boundaries                      
auto &balDesDehumPerfData(state.dataHeatRecovery->BalDesDehumPerfData(state.dataHeatRecovery->ExchCond(ExchNum).PerfDataIndex));
auto &balDesDehumPerfDataError(balDesDehumPerfData.errors[static_cast<int>(HeatRecoveryError_t::RegenInRelHumTemp)]);
 
if (balDesDehumPerfDataError.PrintMess) {
    ++balDesDehumPerfDataError.ErrorCount;
    if (balDesDehumPerfDataError.ErrorCount < 2)              {
        ShowWarningError(state, balDesDehumPerfDataError.buffer1);
        ShowContinueError(state, balDesDehumPerfDataError.buffer2);
        ShowContinueError(state, balDesDehumPerfDataError.buffer3);
        ShowContinueError(state,
                          "...Using regeneration inlet air relative humidities that are outside the regeneration outlet temperature "
                          "equation model boundaries may adversely affect desiccant model performance. Verify correct model"
                          "coefficients.");
    } 
}

This is now a very generic piece of code that can be encapsulated in a function or loop rather than copy-pasted fifteen times.

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