7

This question may very well be the n-th iteration of "How to map strings to enums".

My requirements go a little bit further and I want to throw a certain exception when a key is not found in the range of valid inputs. So I have this implementation of this EnumMap (needs boost for const std::map definition):

#include <map>
#include <string>
#include <sstream>
#include <stdexcept>
#include <boost/assign.hpp>

typedef enum colors {
  RED,
  GREEN,
} colors;
// boost::assign::map_list_of
const std::map<std::string,int> colorsMap  = boost::assign::map_list_of
                                            ("red",   RED)
                                            ("green", GREEN);
//-----------------------------------------------------------------------------
// wrapper for a map std::string --> enum
class EnumMap {
 private:
  std::map<std::string,int> m_map;
  // print the map to a string
  std::string toString() const {
    std::string ostr;
    for(auto x : m_map) {
      ostr += x.first + ", ";
    }
    return ostr;
  }
 public:
  // constructor
  EnumMap(const std::map<std::string,int> &m) : m_map(m) { }
  // access
  int at(const std::string &str_type) {
    try{
      return m_map.at(str_type);
    }
    catch(std::out_of_range) {
      throw(str_type + " is not a valid input, try : " + toString());
    }
    catch(...) {
      throw("Unknown exception");
    }
  }
};
//-----------------------------------------------------------------------------
int main()
{
  EnumMap aColorMap(colorsMap);
  try {
    aColorMap.at("red");    // ok
    aColorMap.at("yellow"); // exception : "yellow is not a valid input ..."
  }
  catch(std::string &ex) {
    std::cout << ex << std::endl;
  }
  return 0;
}

This works well and does what I need. Now, I want to make it possible to know at compile time that all the elements in a certain enum are passed to the EnumMap constructor, and also that all the elements in the enum are matched with the corresponding string.

I tried with std::initializer_list and static_assert, but it seems that VC2010 does not still support std::initializer_list (see here).

Does anyone have an idea on how it would be possible to implement this? Perhaps with templates, or implementing my own Enum class?

Community
  • 1
  • 1
FKaria
  • 1,012
  • 13
  • 14
  • 1
    You do test your code before shipping, right? So a runtime check would be sufficient to ensure it does work, don't you think? – Mats Petersson Apr 16 '13 at 19:51
  • The only language construct that I know of that will warn you if you missed an enum value is `switch` if you add a `case` for each value and you don't have a `default`-branch. I guess this means you will need a macro but I'm far from suggesting anything specific :) – Daniel Frey Apr 16 '13 at 19:53
  • I agree that run-time tests should suffice, and I also know about the warning. I think I'm just trying to figure out how far this can be pushed out – FKaria Apr 16 '13 at 19:58
  • @DanielFrey: macro+switch is pointless since one could define the enum and the map right from the macro. – ipc Apr 16 '13 at 20:03
  • @ipc Not exactly as the enum itself might be in a header and the mapping and all its infrastructure might not be allowed in the header and has to go into an implementation file. – Daniel Frey Apr 16 '13 at 20:05
  • 1
    The above code has undefined behavior, because you have an identifier in global scope that starts with a `_` and the next character is a capital letter. (aka, `_RED`). – Yakk - Adam Nevraumont Apr 16 '13 at 20:07
  • @Yakk: I didn't know about that, edited it right now for correctness. Do you have a reference where I can have look at this? – FKaria Apr 16 '13 at 20:11
  • Start with `typedef enum colors { begin_colors = 0, red=begin_colors, green, end_colors };`, which lets you interact with the range of values in your `enum` via the half-open interval of `begin_colors` through `end_colors`. Next, upgrade your compiler to the Nov2012 CTP compiler if you are going to ask a question with C++11 tagging. Finally, roll your own map initializer that takes types tagged with `colors` and run-time `const char(*)[N]`, aggregates them, checks that each `enum` from `begin_colors` through `end_colors` is among them, then either builds a `map` and returns it via `move`. – Yakk - Adam Nevraumont Apr 16 '13 at 20:12
  • I don't think what you want is possible since enum is not a first-class concept in C++ (it's just a loose front-end on integers). You might be able to do something like what you want at compile-time if you use a more robust type like a [boost::mpl::map](http://www.boost.org/doc/libs/1_53_0/libs/mpl/doc/refmanual/map.html) or [vector_c](http://www.boost.org/doc/libs/1_53_0/libs/mpl/doc/refmanual/vector-c.html). – metal Apr 16 '13 at 20:13
  • 1
    [C++ reserved word list](http://en.cppreference.com/w/cpp/keyword) over at cpp reference. The paragraph near the bottom includes " each name that contains a double underscore __ or begins with an underscore followed by an uppercase letter is always reserved to the implementation and should not be used as an identifier. Each name that begins with an underscore is reserved to the implementation for use as a name in the global namespace". – Yakk - Adam Nevraumont Apr 16 '13 at 20:13
  • As for Yakk's comment, see also [this question](http://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier) for chapter and verse in the standard. – metal Apr 16 '13 at 20:15

3 Answers3

4
typedef enum colors {
  MIN_COLOR,
  RED = MIN_COLOR,
  GREEN,
  MAX_COLOR
} colors;

template< colors C >
struct MapEntry {
  std::string s;
  MapEntry(std::string s_):s(s_) {}
};
void do_in_order() {}
template<typename F0, typename... Fs>
void do_in_order(F0&& f0, Fs&&... fs) {
  std::forward<F0>(f0)();
  do_in_order( std::forward<Fs>(fs)... );
}
struct MapInit {
  std::map< std::string, color > retval;
  operator std::map< std::string, color >() {
    return std::move(retval);
  }
  template<colors C>
  void AddToMap( MapEntry<C>&& ent ) {
    retval.insert( std::make_pair( std::move(end.s), C ) );
  }
  template< typename... Entries >
  MapInit( Entries&& entries ) {
    do_in_order([&](){ AddToMap(entries); }...);
  } 
};
template<typename... Entries>
MapInit mapInit( Entries&&... entries ) {
  return MapInit( std::forward<Entries>(entries)... );
}
const std::map<std::string, colors> = mapInit( MapEntry<RED>("red"), MapEntry<GREEN>("green") );

which gives you a C++11 way to construct a std::map from compile time color and run-time string data.

Throw in a "list of MapEntry<colors> to list of colors" metafunction next.

template<colors... Cs>
struct color_list {};
template<typename... Ts>
struct type_list {};
template<typename MapEnt>
struct extract_color;
template<colors C>
struct extract_color<MapEntry<C>> {
  enum {value=C};
};
template<typename Entries>
struct extract_colors;
template<typename... MapEntries>
struct extract_colors<type_list<MapEntries...>> {
  typedef color_list< ( (colors)extract_colors<MapEntries>::value)... > type;
};

Sort that list. Detect duplicates -- if there are, you screwed up.

Compile-time sorting is harder than the rest of this, and a 100+ lines of code. I'll leave it out if you don't mind too much! Here is a compile time merge sort I wrote in the past to answer a stack overflow question that would work with relatively simple adaptation (it sorts types with values, in this case we are sorting a list of compile-time values directly).

// takes a sorted list of type L<T...>, returns true if there are adjacent equal
// elements:
template<typename clist, typename=void>
struct any_duplicates:std::false_type {};
template<typename T, template<T...>class L, T t0, T t1, T... ts>
struct any_duplicates< L<t0, t1, ts...>, typename std::enable_if<t0==t1>::type>:
  std::true_type {};
template<typename T, template<T...>class L, T t0, T t1, T... ts>
struct any_duplicates< L<t0, t1, ts...>, typename std::enable_if<t0!=t1>::type>:
  any_duplicates< L<t1, ts...> > {};

Detect elements outside of the valid range of colors (ie, <MIN_COLOR or >=MAX_COLOR). If so, you screwed up.

 template<typename List>
 struct min_max;
 template<typename T, template<T...>class L, T t0>
 struct min_max {
   enum {
     min = t0,
     max = t1,
   };
 };
 template<typename T, template<T...>class L, T t0, T t1, T... ts>
 struct min_max {
   typedef min_max<L<t1, ts...>> rest_of_list;
   enum {
     rest_min = rest_of_list::min,
     rest_max = rest_of_list::max,
     min = (rest_min < t0):rest_min:t0,
     max = (rest_max > t0):rest_max:t0,
   };
 };
 template< typename T, T min, T max, typename List >
 struct bounded: std::integral_constant< bool,
   (min_max<List>::min >= min) && (min_max<List>::max < max)
 > {};

Count how many elements there are -- there should be MAX_COLOR elements. If not, you screwed up.

 template<typename List>
 struct element_count;
 template<typename T, template<T...>L, T... ts>
 struct element_count<L<ts...>>:std::integral_constant< std::size_t, sizeof...(ts) > {};

If none of these occurred, by pigeonhole you must have initialized each of them.

The only thing missing is that you could have gone off and used the same string for two values. As compile time strings are a pain, just check this at run time (that the number of entries in the map equals the number of colors after you initialize it).

Doing this in C++03 will be harder. You lack variardic templates, so you end up having to fake them. Which is a pain. mpl might be able to help you there.

Variardic templates are available in the Nov 2012 MSVC CTP compiler update.

Here is a toy example without duplicate checking and without bounds checking (it just checks that the number of map entries matches);

#include <cstddef>
#include <utility>
#include <string>
#include <map>

enum TestEnum {
   BeginVal = 0,
   One = BeginVal,
   Two,
   Three,
   EndVal
};

template<TestEnum e>
struct MapEntry {
  enum { val = e };
  std::string s;
  MapEntry( std::string s_ ):s(s_) {}
};

void do_in_order() {}
template<typename F0, typename... Fs>
void do_in_order(F0&& f0, Fs&&... fs) {
  std::forward<F0>(f0)();
  do_in_order( std::forward<Fs>(fs)... );
}

template<typename... MapEntries>
struct count_entries:std::integral_constant< std::size_t, sizeof...(MapEntries) > {};

// should also detect duplicates and check the range of the values:
template<typename... MapEntries>
struct caught_them_all:
  std::integral_constant<
    bool,
    count_entries<MapEntries...>::value == (TestEnum::EndVal-TestEnum::BeginVal)
  >
{};

struct BuildMap {
  typedef std::map<std::string, TestEnum> result_map;
  mutable result_map val;
  operator result_map() const {
    return std::move(val);
  }
  template<typename... MapEntries>
  BuildMap( MapEntries&&... entries ) {
    static_assert( caught_them_all<MapEntries...>::value, "Missing enum value" );
    bool _[] = { ( (val[ entries.s ] = TestEnum(MapEntries::val)), false )... };
  }
};

std::map< std::string, TestEnum > bob = BuildMap(
  MapEntry<One>("One")
  ,MapEntry<Two>("Two")
#if 0
  ,MapEntry<Three>("Three")
#endif
);

int main() {}

Replace the #if 0 with #if 1 to watch it compile. Live link if you want to play.

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • `static_assert( clause, "things are no good" )` is the C++11 way to get a compilation error. Or did you mean compile-time sorting&uniqueness testing, bounds checking, and the like traits classes? – Yakk - Adam Nevraumont Apr 16 '13 at 21:23
  • Great answer. It seems that this would only work for a specific `enum` type, namely `colors`. It seems that this can be as close as possible to achieve what I am trying to do, and no simpler solution seems possible since you are using a lot heavy wapons. I am thinking that the unique other alternative would be to implement a custom `Enum` class and the custom `EnumMap` to contain its elements. – FKaria Apr 16 '13 at 21:56
  • @FkKria In theory you might be able to make it work for any `enum` that "looks like" `enum bob { begin_val=0, ..., end_val }` by talking about `E::begin_val` and `E::end_val` with a type `E` passed in. Going a bit further, a traits class `template struct begin_val` and `end_val` would let you do this non-intrusively for any `enum` with adjacent values, and specialize as needed. Going even further, traits classes that defined a collection of valid ranges over which your `enum` is defined could be used to check if you "caught them all". – Yakk - Adam Nevraumont Apr 16 '13 at 22:05
  • @FKaria Finally, you could create pseudo-enums which are collections of indexed types, possibly introduced in the definition of the pseudo-enum (`typedef PseudoEnum< struct Red, struct Green, struct Blue > Colors`), or even `typedef PseudoEnum< std::pair, std::pair, std::pair > Colors`, with access like `Colors::Val()`, where the first component defines a tag and the second a valid, and then you'd have full compile-time reflection of the structure of the `PseudoEnum`. I'd be surprised if `boost::mpl` didn't have such a pseudo-enum already. – Yakk - Adam Nevraumont Apr 16 '13 at 22:08
  • You still haven't show the example failing the compilation because the map is missing an element with certain string. – BЈовић Apr 17 '13 at 06:05
  • @BЈовић And you didn't answer the clarification I asked for! The answer above simply describes how to do it. Doing it fully would take at least another 100+ lines of tedious code, mainly in the compile time sorting engine: and the OP understood what I was describing. Writing 100+ lines of compile time sorting code doesn't seem all that useful after you have done it once: would a link to how to do compile time sorting in C++, plus a solution that presumes the `enum` values are sorted, satisfy? – Yakk - Adam Nevraumont Apr 17 '13 at 13:00
  • I just wanted to see how you break the compilation once the string to enum conversion fails. You added lots of code, without a real example showing that the compilation really fails. You already got 50 lines of code - adding 100 more is not going to change anything. Or, do you have a link to a real example? – BЈовић Apr 17 '13 at 13:37
  • @BЈовић toy example given that doesn't check for duplicates and doesn't check the bounds of the values, but demonstrates the failure to compile feature, including link to online compiling example. I had to use some slightly different techniques because my favorite Clang 3.2 online compiler is down right now. – Yakk - Adam Nevraumont Apr 17 '13 at 13:49
  • 1
    I think I misunderstood the question. I thought this should fail the compilation : `std::cout< – BЈовић Apr 17 '13 at 13:58
  • Still bad habit of all uppercase constants – Slava Jan 08 '16 at 19:06
  • @Slava So, your take away from my above answer was "Yakk didn't rename the enum values from what the OP used"? ;) – Yakk - Adam Nevraumont Jan 08 '16 at 20:51
  • @Yakk yea missed that, my bad – Slava Jan 09 '16 at 16:25
2

Does anyone have an idea on how it would be possible to implement this? Perhaps with templates, or implementing my own Enum class?

It is not possible to do. Not with std::map, and not with template meta-programming.

BЈовић
  • 62,405
  • 41
  • 173
  • 273
0

I want to post the solution I arrived finally. I don't want to mark it as a definitive answer because I would like to figure out if we can define the vectors of strings at compile time.

// 
//  Cumstom Enum Header File
//

#include <vector>
#include <string>

#include <boost/algorithm/string/classification.hpp> 
#include <boost/algorithm/string/split.hpp>
#include <boost/range/algorithm/find.hpp>

std::vector<std::string> split_to_vector(std::string s)
{
    // splits a comma separated string to a vector of strings
    std::vector<std::string> v;
    boost::split(v, s, boost::is_any_of(", "), boost::token_compress_on);
    return v;
}

#define ENUM_TO_STRING(X,...) \
    struct X {  \
      enum Enum {__VA_ARGS__};  \
        static  \
        std::string to_string(X::Enum k) {  \
            std::vector<std::string> keys = split_to_vector(#__VA_ARGS__);  \
            return keys[static_cast<int>(k)];   \
        }   \
        static  \
        X::Enum which_one(const std::string s) {    \
            std::vector<std::string> keys = split_to_vector(#__VA_ARGS__);  \
            auto it = boost::find(keys, s); \
            if(it == keys.end()) {  \
                throw("not a valid key");   \
            }   \
            return static_cast<X::Enum>(it - keys.begin()); \
        }   \
    }


//
// Usage 
//

#include <iostream>

ENUM_TO_STRING(Color, Red, Green, Blue);

int main()
{
    std::string red_s = Color::to_string(Color::Red);
    std::cout << red_s << std::endl;

    Color::Enum red_e = Color::which_one("Red");
    std::cout << red_e << std::endl;

    // won't compile
    // std::string yellow_s = Colors::to_string(Colors::Yellow);

    // run-time error
    // Color::Enum yellow_e = Colors::which_one("Yellow");
}

Run it on Coliru: http://coliru.stacked-crooked.com/a/e81e1af0145df99a

FKaria
  • 1,012
  • 13
  • 14
  • You can make it compile-time. See [here](http://stackoverflow.com/questions/28828957/enum-to-string-in-modern-c-and-future-c17/31362042#31362042). There is a variant of that solution with compile-time `to_string`, as well. – antron Mar 04 '16 at 04:43
  • Thanks @antron. I also found a way to define the vector at compile time using Boost preprocessor library but I just didn't update this answer. I just now posted my solution in [that thread](http://stackoverflow.com/a/35788543/1405424). I believe is similar to your solution, except that it used Boost preprocessor for the macros. I also omitted the string -> enum conversion because the question only asks for enum -> string. – FKaria Mar 04 '16 at 05:24