Table of Contents

2023-08-26 - handling enums in C++

handling enums in 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 "<unknown>";
  }
}

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 "<unknown>";
  }
}

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 "<unknown>";
}

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 enums 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 returns accordingly, eg.:

#include <cassert>
// ...
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 <cassert>
// ...
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…