====== 2023-08-26 - handling enums in C++ ======
handling ''enum''s in [[wp>C++]] is tricky. but there are good practices that can help you out. here are two from the set.
===== handling all values =====
let's say we have a simple ''enum'' called ''Foo'', with some values. we want to add a ''str()'' helper, that would covert it to sth more human-readable than a number. very common implementation would be:
enum class Foo { A, B, C };
// ...
auto str(Foo f)
{
switch(f)
{
case Foo::A: return "A";
case Foo::B: return "B";
case Foo::C: return "C";
default: return "";
}
}
looks good, all tests pass. however there is a potential for a problem -- it's not future proof. let's now add ''Foo::D'':
enum class Foo { A, B, C, D }; // notice Foo::D is added!
// ...
auto str(Foo f)
{
switch(f)
{
case Foo::A: return "A";
case Foo::B: return "B";
case Foo::C: return "C";
default: return "";
}
}
looks good, all tests pass... however now there _is_ a problem! new value is not handled and it will return an incorrect value at runtime!
there's a neat way of fixing this problem and making code future-proof. compile with ''-Wall -Werror'' (as you anyway should!) and make sure no ''switch'' has any ''default''. eg.:
auto str(Foo f)
{
switch(f)
{
case Foo::A: return "A";
case Foo::B: return "B";
case Foo::C: return "C";
}
return "";
}
change looks insignificant, but now if we miss handling 1 value, we get a warning from a compiler and with ''-Werror'' it stops the build right away.
c.cpp: In function ‘auto str(Foo)’:
c.cpp:11:9: error: enumeration value ‘D’ not handled in switch [-Werror=switch]
11 | switch(f)
| ^
cc1plus: all warnings being treated as errors
bug is detected at compile time! nice... :)
===== handling some values =====
another common case where ''enum''s are used is adding special functions, that check if value is in a given logical state / group. eg. determining if it's an error:
enum class State
{
Initializing,
Running,
InputError
};
bool isOk(State s)
{
return s != State::InputError;
}
looks good, all tests pass. however... you probably already know where it's going. ;) let's consider extending ''State'' with a new error state:
enum class State
{
Initializing,
Running,
InputError,
OutputError
};
...and now ''isOk(State::OutputError) == true''. whoops...
a better way of writing these kind of function is to list all ''enum'' values explicitly, and assign ''return''s accordingly, eg.:
#include
// ...
bool isOk(State s)
{
switch(s)
{
case State::Initializing:
case State::Running:
return true;
case State::InputError:
return false;
}
assert(!"unknown value");
}
now, if states change, we get a proper error:
4.cpp: In function ‘bool isOk(State)’:
4.cpp:13:9: error: enumeration value ‘OutputError’ not handled in switch [-Werror=switch]
13 | switch(s)
| ^
cc1plus: all warnings being treated as errors
so we know we need to fix it:
#include
// ...
bool isOk(State s)
{
switch(s)
{
case State::Initializing:
case State::Running:
return true;
case State::InputError:
case State::OutputError:
return false;
}
assert(!"unknown value");
}
while writing this kind of functions using ''switch''-''case'' may look a bit odd at first, it has a very nice property of ensuring code being future-proof. as a side effect, it also makes core far easier to read. for comparison just imagine ''isOk()'' where there are 7 "ok" states and 13 "error" states...