Conversion between enum and string in C++ class header

  • 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;
    }
    }

  • Martin York

    Martin York Correct answer

    9 years ago

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

    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!

    A further question, if I still may, how would one define the conversions without using streams? E.g. template 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. }

    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: '' : cannot convert from 'const some_namespace::some_class::X' to 'some_namespace::`anonymous-namespace'::enumConstRefHolder'" 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..? :)

    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::data[static_cast(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.

    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::data[static_cast::type>(data.enumValue)".

    And further R. Martinho Fernandes has a bit cleaner helper function over at Stackoverflow.

    It also looks like as if enumConstRefHolder and enumRefHolder can be removed. If someone is interested, I can post the code after changes.

    that's what I get for trying to tidy the code without compiling. I have fixed the errors.

    note: I had to include for this code to compile

    What is the advantage of this approach compared to using a type safe `enum` and a `std::map`. E.g. `enum class Foo {Lo, Hi}; const std::map FooStrings;` ?

    @JamieBullock: At the time it was written the array was the only object that could be initialized inline (now with C++11 you can do it with a map).

    Is there any way to add a + or += operator to this? It would be nice to be able to append this to other strings without having to convert to streams first.

    @rost0031: `enumToString(a)` returns a `char const*`. If you already have a string then the `operator+` already works as expected. If you want to add something to this you need to convert it to `std::string` with `std::string(enumToString(a))`

    Hmm, I'm getting the following error: `no match for 'operator+' (operand types are 'const char [597]' and 'enumConstRefHolder') "Application only" + enumToString(_CB_EUIROM);` This works fine if I do `ss << enumToString(_CB_EUIROM);` and then append ss.str().

    I'm using this design, but I had to change `char const*` to `std::string`. I'm not sure how else it would work. `std::find` matches based on an equality operator. And as mentioned here https://stackoverflow.com/questions/15050766/comparing-the-values-of-char-arrays-in-c `char const*` doesn't have an equality operator. It might work if `std::find` used `strcmp`

    @HesNotTheStig You can compare a `std::string` to `char const*`. This is because `operator==(std::string const&, std::string const&)` is a free standing function and C++ lax rules of type conversion. Because there is not exact match the compiler will attempt to convert the parameters to make the operator work. Since `std::string` has a non `explicit` default constructor it can construct a string in-place using this default constructor.

    @HesNotTheStig The code above compiles and runs for me (in C++ 11 and above). So if it is not working then there is something else wrong that we need to find.

    It compiles fine. It's just that when it was running, it wasn't matching correctly. Upon debugging, I determined that it was comparing the pointers. Changing it to `std::string` made it behave correctly for me.

    @HesNotTheStig That does not make sense. It never compares `char*` to `char*` so there is not pointer comparison. Can you make a gist that shows the error.

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM