Converting enum classes to strings and back in C++

, in Computing, Popular or Notable

UPDATE: This generated some interesting discussion on this reddit threadAll the code here as well as better documentation and some examples/test cases are available in this github repositoryAlmost every program I have ever worked on has eventually required me to convert from my beautifully type-safe enum classes into strings for serialization. Other languages provide reflection and annotations to make this easy but us C++ programmers fend for ourselves. Or look up blog posts on the internet. We get paid either way.

Say you have a some enum classes:

enum class ParkingPolicy {NoParking, OnlyPublicHolidays, OnlyWeekends, OnlyWeekdays, SevenDays};
enum class ColorOptions {Transparent, Red, Yellow, Green, Blue, White, Black};

And you want to serialize to json:

{
"name" : "Elm Street", 
"parking" : "OnlyWeekends", 
"signage" : "Green"
}

You could do this:

std::string parkingString = jsonObject["parking"];
ParkingPolicy parking;
if (parkingString == "NoParking")
  parking = ParkingPolicy::NoParking; break;
else if ((parkingString == "OnlyPublicHolidays")
  parking = ParkingPolicy::OnlyPublicHolidays; break;
else if ((parkingString == "OnlyWeekends")
  parking = ParkingPolicy::OnlyWeekends; break;
... etc, etc ...

but code like that makes my eye twitch. Sooner or later someone is going to add a value to the enum and forget to add it to the code.

Wouldn't it be nice to do something like this:

ParkingPolicy parking = EnumMapping::getValueFromName(ValidParkingPolicies, jsonObject["parking"]);

and this:

std::cout << "The parking policy is " << EnumMapping::getNameFromValue(ValidParkingPolicies, parking) << std::endl;

You can (with a little extra work).

const std::array<const NameValuePair<ParkingPolicy>, 5 > ValidParkingPolicies{{
  {ParkingPolicy::NoParking, "NoParking"},
  {ParkingPolicy::OnlyPublicHolidays, "OnlyPublicHolidays"},
  {ParkingPolicy::OnlyWeekends, "OnlyWeekends"},
  {ParkingPolicy::OnlyWeekdays, "OnlyWeekdays"},
  {ParkingPolicy::SevenDays, "SevenDays"}
}};

On the reddit thread a user named epicar posted some code that used an initialiser_list<> instead of a std::array to store the mapping. I had never considered this but it works well and the rest of the code doesn't careOnce you have defined the mapping between strings and enum class values like so, a simple templated function can perform the translations in a completely type-safe way.

namespace EnumMapping {

// I like exceptions, you might feel differently
class UnknownValueException : public std::runtime_error {
public:
    UnknownValueException(const std::string& name):std::runtime_error("Unknown value: " + name) {};
    UnknownValueException(int value):std::runtime_error("Unknown name for enum value: " + std::to_string(value)) {};
};

template<class T>
struct NameValuePair {
    using value_type = T;
    const T value;
    const char* const name;
};

// Templated helper functions.
// Mapping is some type of standard container that supports find_if()
// V is the type of the enum whose value we wish to look up
template<class Mapping, class V>
std::string getNameForValue(Mapping a, V value) {
    auto pos = std::find_if(std::begin(a), std::end(a), [&value](const typename Mapping::value_type& t){
        return (t.value == value);
    });
    if (pos != std::end(a)) {
        return pos->name;
    }

    throw UnknownValueException(static_cast<int>(value));
    // or return some default value here
    // return Mapping::value_type::value_type();
}

template<class Mapping>
typename Mapping::value_type::value_type getValueForName(Mapping a, const std::string& name)
{
    auto pos = std::find_if(std::begin(a), std::end(a), [&name](const typename Mapping::value_type& t){
        return (t.name == name);
    });
    if (pos != std::end(a)) {
        return pos->value;
    }

    throw UnknownValueException(name);
    // or return an empty string, whatever works for you
}

}  // end of namespace

You don't even have to include every value in the enum in your mapping array, or can even include values more than once with different names, in case "grey" and "gray" have to be synonyms.

A possible problem is that the values are looked up using a linear scan. This is fine if your enums can only take a few values but if you have dozens of values then a map would be a better choice. I am going for clarity and low setup cost rather than straight line efficiency here.

The EnumMapping github repository as better documentation and some honest-to-god unit tests.