23
\$\begingroup\$

I have the following redundant-feeling design to convert between enums and strings regarding a class that stores enums. The approach doesn't scale if there are more enums and in any event, less-redundant code is also better.

Questions

  1. If there will be more enums, would it be possible to avoid defining two explicit conversion functions per enum type and device a system where the caller sees just one (i.e. convert) or two different function names (i.e. convertto/convertfrom for all the enums, not just per enum type)? Perhaps using some kind deduction magic with auto and decltype? It looks like ambiguity sets in since only the return value can be used to separate the different functions overloads (even if done with function templates).

  2. Is the following design of separating the conversion functions and putting them to an anonymous namespace good design (I've thought about putting the conversion functions to a file, say conversions.incl and including it)?

The idea would be make the multiple (i.e. more enums than the one presented here) conversions as implicit as possible

The conversions would be used like this:

random.cpp

string token_string = "none"; //In reality this will be externally, user, generated.
some_class_instance->set_type(enum_conversion(token_string));
token_string = enum_conversion(some_class_instance->get_type());

And to present one enum and related conversions (but there could be more):

some_class.h

class some_class
{
    public:
         enum class enum_type
         {
             none   = 0,
             type1  = 1,
             type2  = 2
         }

     void set_type(enum_type);
     enum_type get_type() const;

   private:
       enum_type type_;
};

namespace
{
    std::array<std::pair<std::string, some_class::enume_type>, 3> type_map;

    bool initialize_map()
    {
       type_map[0] = std::make_pair("none", some_class::enum_type::none);
       type_map[1] = std::make_pair("type1", some_class::enum_type::type1);
       type_map[2] = std::make_pair("type2", some_class::enum_type::type2);
    }

    bool initialization_result = initialize_map();

    some_class::enum_type enum_conversion(std::string const& enum_type)
    {
        for(auto val: type_map)
        {
            if(val.first == enum_type)
            {
                return val.second;
            }
        }

        return type_map[0].second;
    }

    std::string enum_conversion(some_class::enum_type enum_type)
    {
        for(auto val: type_map)
        {
            if(val.second == enum_type)
            {
                return val.first;
            }
        }

        return type_parameter_map[0].first;
    }
}
\$\endgroup\$
25
\$\begingroup\$

I would use some template logic to achieve the affect in a more scalable way:

#include <iostream>
#include <sstream>
#include <string>
#include <algorithm>

// This is the type that will hold all the strings.
// Each enumeration type will declare its own specialization.
// Any enum that does not have a specialization will generate a compiler error
// indicating that there is no definition of this variable (as there should be
// be no definition of a generic version).
template<typename T>
struct enumStrings
{
    static char const* data[];
};

// This is a utility type.
// Created automatically. Should not be used directly.
template<typename T>
struct enumRefHolder
{
    T& enumVal;
    enumRefHolder(T& enumVal): enumVal(enumVal) {}
};
template<typename T>
struct enumConstRefHolder
{
    T const& enumVal;
    enumConstRefHolder(T const& enumVal): enumVal(enumVal) {}
};

// The next two functions do the actual work of reading/writing an
// enum as a string.
template<typename T>
std::ostream& operator<<(std::ostream& str, enumConstRefHolder<T> const& data)
{
   return str << enumStrings<T>::data[data.enumVal];
}

template<typename T>
std::istream& operator>>(std::istream& str, enumRefHolder<T> const& data)
{
    std::string value;
    str >> value;

    // These two can be made easier to read in C++11
    // using std::begin() and std::end()
    //  
    static auto begin  = std::begin(enumStrings<T>::data);
    static auto end    = std::end(enumStrings<T>::data);

    auto find   = std::find(begin, end, value);
    if (find != end)
    {   
        data.enumVal = static_cast<T>(std::distance(begin, find));
    }   
    return str;
}


// This is the public interface:
// use the ability of function to deduce their template type without
// being explicitly told to create the correct type of enumRefHolder<T>
template<typename T>
enumConstRefHolder<T>  enumToString(T const& e) {return enumConstRefHolder<T>(e);}

template<typename T>
enumRefHolder<T>       enumFromString(T& e)     {return enumRefHolder<T>(e);}

Then you can use it like this:

// Define Enum Like this
enum X {Hi, Lo};
// Then you just need to define their string values.
template<> char const* enumStrings<X>::data[] = {"Hi", "Lo"};

int main()
{
    X   a=Hi;

    std::cout << enumToString(a) << "\n";

    std::stringstream line("Lo");
    line >> enumFromString(a);

    std::cout << "A: " << a << " : " << enumToString(a) << "\n";
}
\$\endgroup\$
  • \$\begingroup\$ I feel this design is far superior. I tried something like this, but run into problems with the specializations (hence my comment on overloading with specializations). Then I went into the second best alternative I could make to compile. The simple arrays feel like being enough (and perhaps the fastest) since they will be quite short, around ten items maximum. But anyway, thanks for the code and heads up, this was a good learning experience! \$\endgroup\$ – Veksi Aug 4 '12 at 7:48
  • \$\begingroup\$ A further question, if I still may, how would one define the conversions without using streams? E.g. template<typename T> T convertTo(std::string const& token) { std::stringstream line(token); T a; line >> enumFromString(a); return a; } but this doesn't feel the most straightforward solution. Also, for some reason, I can't seem to find a way to do this to the other direction, to produce a string from enum. } \$\endgroup\$ – Veksi Aug 4 '12 at 12:27
  • \$\begingroup\$ Hmm... Moreover, now when I'm trying this more, it looks like the ostream conversion doesn't compile. My VS 2012 RC fails with error messages "Error 1 error C2440: '<function-style-cast>' : cannot convert from 'const some_namespace::some_class::X' to 'some_namespace::`anonymous-namespace'::enumConstRefHolder<T>'" and error C2677: binary '[' : no global operator found which takes type 'const some_namespace::some_class::X' (or there is no acceptable conversion)" And I'm too rookie to fix the error message myself, I gather. Can I still lend your hand for a moment..? :) \$\endgroup\$ – Veksi Aug 4 '12 at 14:13
  • \$\begingroup\$ I think I've found the crux of the matter: my enums are strongly typed, so they can't be used to index the arrays as-is, but with a cast like so "return str << enumStrings<T>::data[static_cast<int>(data.enumValue_)];" then also the constructor of EnumConstRefHolder needs to take the parameter by constant reference. It looks like the strongly typed enums can be cast to integer and they work without their underlying type specified. Though, it probably is better to specify one. \$\endgroup\$ – Veksi Aug 4 '12 at 16:06
  • \$\begingroup\$ To still add comments (if someone cares to read them this far), the default underlying type is int, but it can be changed and hence it's safer to "interrogate" it during compilation. The cast can be done with std::underlying_type like this "return str << enumStrings<T>::data[static_cast<std::underlying_type<T>::type>(data.enumValue)". \$\endgroup\$ – Veksi Aug 4 '12 at 16:24
6
\$\begingroup\$

In most cases, the requirement of converting between c++ enumeration and string representation arises from interfacing to another program that does not understand your enumeration declarations. Often this other program will be a SQL database.

Hence I would go for a code generator to ensure consistency over the whole system.

The code generator could walk the whole database catalog and create an enumeration c-header and corresponding string arrays for everything in the database that could potentially carry enumeration semantics. Writing such a generator is less then a days work.

Note that this approach does not only free you from writing the string part. It also frees you from writing the enumeration definition altogether.

\$\endgroup\$
  • 1
    \$\begingroup\$ This is the approach I use for embedded firmware programming. I use the most capable and expressive language (say C#) and generate headers for all the less sophisticated languages (say C or C++), directly from the C# code, which is annotated with Attributes. \$\endgroup\$ – Mark Lakata Jun 20 '14 at 22:20
4
\$\begingroup\$

Here is a template class that enables writing and reading enum class members as strings. It is a simplification of Loki Astari's design. It avoids the need for helper functions, as suggested by Veski, by using the enable_if<> and is_enum<> templates.

The idea is to replace template <T> with

template <typename T,
    typename std::enable_if<std::is_enum<T>::value>::type* = nullptr>

The consequence is that the operator<<() and operator>>() templates are only instantiated for enums (because of the Substitution Failure Is Not An Error (SFINAE) principle). Then the helper classes enumConstRefHolder and enumRefHolder, and the functions enumToString() and enumFromString(), are no longer needed.

In addition to recognizing an enum member by a string (normalized to be all capital letters), the code recognizes its integer representation. (For the example below, both "FaST" and "1" will be read as Family::FAST.)

EnumIO.h:

#ifndef ENUMIO_H_
#define ENUMIO_H_

#include <algorithm>
#include <ios>
#include <iostream>
#include <sstream>
#include <vector>

// A template class that enables writing and reading enum class
// members as strings.
//
// Author:  Bradley Plohr (2017-05-12)
//
// Note:  The idea to keep the enum names as a static member in a
// template comes from Loki Astari:
//
// https://codereview.stackexchange.com/questions/14309
//         /conversion-between-enum-and-string-in-c-class-header
//
// Usage example:
//
// Enums.h:
// -------
// #ifndef ENUMS_H_
// #define ENUMS_H_
//
// enum class Family { SLOW, FAST };
//
// TODO:  other enum classes
//
// #endif /* ENUMS_H_ */
//
//
// Enums.cc:
// --------
// #include "Enums.h"
// #include "EnumIO.h"
// #include <string>
// #include <vector>
//
// template <>
// const std::vector<std::string>& EnumIO<Family>::enum_names()
// {
//      static std::vector<std::string> enum_names_({ "SLOW", "FAST" });
//      return enum_names_;
// }
//
// TODO:  enum names for other enum classes
//
//
// t_EnumIO.cc:
// -----------
// #include "EnumIO.h"
// #include "Enums.h"
// #include <iostream>
//
// int
// main()
// {
//     Family family;
//
//     family = Family::SLOW;
//     std::cout << family << std::endl;
//
//     std::cin >> family;
//     std::cout << family << std::endl;
//
//     return 0;
// }
//
// For the input
//
//     fAsT
//
// the output is
//
//     SLOW
//     FAST

template <typename T>
class EnumIO
{
public:
    static const std::vector<std::string>& enum_names();
};

template <typename T,
        typename std::enable_if<std::is_enum<T>::value>::type* = nullptr>
std::ostream&
operator<<(std::ostream& os, const T& t)
{
    os << EnumIO<T>::enum_names().at(static_cast<int>(t));

    return os;
}

static std::string
toUpper(const std::string& input)
{
    std::string copy(input);
    std::transform(copy.cbegin(), copy.cend(), copy.begin(),
            [](const unsigned char i) { return std::toupper(i); });

    return copy;
}

template <typename T,
        typename std::enable_if<std::is_enum<T>::value>::type* = nullptr>
std::istream&
operator>>(std::istream& is, T& t)
{
    std::string input;
    is >> input;
    if (is.fail())
        return is;
    input = toUpper(input);

    // check for a match with a name
    int i = 0;
    for (auto name : EnumIO<T>::enum_names()) {
        if (toUpper(name) == input) {
            // Here we assume that the integer representation of
            // the enum class is the default.  If the enum class
            // members are assigned other integers, this code
            // must be extended by consulting a vector containing
            // the assigned integers.
            t = static_cast<T>(i);

            return is;
        }
        ++i;
    }

    // check for a match with an integer
    int n = static_cast<int>(EnumIO<T>::enum_names().size());
    std::istringstream iss(input);
    int value;
    iss >> value;
    if (not iss.fail() && 0 <= value && value < n) {
        t = static_cast<T>(value); // See the comment above.
        return is;
    }

    is.setstate(std::ios::failbit);

    return is;
}

#endif /* ENUMIO_H_ */
\$\endgroup\$
  • \$\begingroup\$ Thanks, Loki, for the reply. However, I would ask for more information: (a) what needs to be done to "templatize [my] functions and the type_map by the enum"; and (b) I don't do a search when converting from enum to string (the code is just like yours). \$\endgroup\$ – Gidfiddle May 13 '17 at 3:30
  • \$\begingroup\$ Isn't this way too heavy to read an enum? \$\endgroup\$ – Incomputable May 13 '17 at 4:36
1
\$\begingroup\$

If you have large enumerations, you might find that linear search in an array is inefficient. There's also a real risk of accidentally omitting one or more of the mappings.

I solved this a different way by writing the enum→string conversion as a switch (with compiler warnings to indicate a missed case), and then generating a string→enum std::map when it's first required:

std::string to_string(some_class::enum_type e) {
    switch (e) {
    // you might want to use a macro to get matching labels and strings
    case some_class::enum_type::none: return "none";
    case some_class::enum_type::type1: return "type1";
    case some_class::enum_type::type2: return "type2";
    // N.B. no 'default', or GCC won't warn about missing case
    }
    // invalid value
    return {};
}

some_class::enum_type from_string(const std::string& s) {
    static auto const m = invert(some_class::enum_type::none,
                                 some_class::enum_type::type2,
                                 to_string);
    auto it = m.find(s);
    return it == m.end() ? some_class::enum_type::none : *it;
}

template<typename T, typename R>
std::map<R,T> invert(T first, T last, R(*forward_func)(T))
{
    if (first > last) std::swap(first, last);

    std::map<R,T> m;
    for (int i = first;  i <= last;  ++i)  {
        T t = T(i);
        R r = to_string(t);
        m[r] = t;
        // Or: if (!m.insert_or_assign[t].second)
        //         log_warning(m[r] and t both map to r);
    };
    return m;
}

To make from_string() into a template, you'll want some sort of enum_traits<T> to specify the 'first' and 'last' values and the default value to return if the string isn't found (your unit tests can use these limits when checking that every enum maps back to itself).

You might also need to help the compiler select the correct overload of to_string(); alternatively, you should be able to inline it into invert(). In my case, some of the enums I inherited had more than one mapping to/from string, depending on the context, so calling them all to_string wasn't an option for me.

\$\endgroup\$
0
\$\begingroup\$

A good start.
But you need to templatize your functions and the type_map by the enum to make the design extensible. That is a simple change so I will not focus on that.

About the only other thing is that you search when doing a conversion in either direction. By choosing the appropriate container to hold the information you can do a quick lookup in one direction (though not in both without some fancier than normal containers).

Otherwise I quite like it.

It's not the style I would have chosen - see my alternative answer.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.