0

I'm loading/saving a set of parameters from/to a file using boost::property_tree. Many of those parameters are enumerations (different types). So I need a way to get enums from a boost::property_tree (i.e., converting a string to enum), and viceversa. For example

const Enum_1 position = params.get<Enum_1>("Test.position");

I've checked out this answer, which involves creating a translator for each enumeration. As I have several dozens of enumerations, it looks like a bit overwhelming.

Is there any more generic way to do it when many enums are involved?

PS: I'm posting my current solution in an answer since I haven't been able to find something easier/simpler. I'll be glad to hear better options.

cbuchart
  • 10,847
  • 9
  • 53
  • 93
  • I think your current solution posted is clean and simple. Why clutter it up? – natersoz Sep 06 '18 at 15:00
  • 1
    @natersoz, I'm not very proficient with Boost and would like to know if there is a better / more standard way to do it. In any case I wanted to share this work since I couldn't find it when I looked it for, so someone else can benefit from it. – cbuchart Sep 06 '18 at 15:03

2 Answers2

1

My current solution consists on a templated translator that relies on a boost::bimap to ease the std::string/enum conversion.

// Generic translator for enums
template<typename T>
struct EnumTranslator {
  typedef std::string internal_type;
  typedef T external_type;
  typedef boost::bimap<internal_type, external_type> map_type;

  boost::optional<external_type> get_value(const internal_type& str) {
    // If needed, 'str' can be transformed here so look-up is case insensitive
    const auto it = s_map.left.find(str);
    if (it == s_map.left.end()) return boost::optional<external_type>(boost::none);
    return boost::optional<external_type>(it->get_right());
  }

  boost::optional<internal_type> put_value(const external_type& value) {
    const auto it = s_map.right.find(value);
    if (it == s_map.right.end()) return boost::optional<internal_type>(boost::none);
    return boost::optional<internal_type>(it->get_left());
  }

private:
  static const map_type s_map;
};

Such dictionaries are then defined for each enum:

// Dictionaries for string<-->enum conversion
typedef EnumTranslator<Enum_1> Enum_1_Translator;
const Enum_1_Translator::map_type Enum_1_Translator::s_map =
  boost::assign::list_of<Enum_1_Translator::map_type::relation>
  ("first", Enum_1::first)
  ("second", Enum_1::second)
  ("third", Enum_1::third);

typedef EnumTranslator<Enum_2> Enum_2_Translator;
const Enum_2_Translator::map_type Enum_2_Translator::s_map =
  boost::assign::list_of<Enum_2_Translator::map_type::relation>
  ("foo", Enum_2::foo)
  ("bar", Enum_2::bar)
  ("foobar", Enum_2::foobar);

Finally, the translators must be registered so they can be used by boost::property_tree.

// Register translators
namespace boost {
  namespace property_tree {
    template<typename Ch, typename Traits, typename Alloc>
    struct translator_between<std::basic_string<Ch, Traits, Alloc>, Enum_1> {
      typedef Enum_1_Translator type;
    };

    template<typename Ch, typename Traits, typename Alloc>
    struct translator_between<std::basic_string<Ch, Traits, Alloc>, Enum_2> {
      typedef Enum_2_Translator type;
    };
  }
}

Final example of use (params is a boost::property_tree::ptree):

const Enum_1 position = params.get<Enum_1>("Test.position");
const Enum_2 foo_or_bar = params.get<Enum_2>("Test.foo_or_bar");

Maybe someone would prefer to add some macros to reduce the code cluttering, for example:

#define DECLARE_ENUM_TRANSLATOR(E) \
  typedef EnumTranslator<E> E##EnumTranslator; \
  const E##EnumTranslator::map_type E##EnumTranslator::s_map = \
    boost::assign::list_of<E##EnumTranslator::map_type::relation>

#define REGISTER_ENUM_TRANSLATOR(E) \
  namespace boost { namespace property_tree { \
  template<typename Ch, typename Traits, typename Alloc> \
  struct translator_between<std::basic_string<Ch, Traits, Alloc>, E> { \
    typedef E##EnumTranslator type; \
  }; } }

In this way, new enums can be registered by:

DECLARE_ENUM_TRANSLATOR(Enum_1)
  ("first", Enum_1::first)
  ("second", Enum_1::second)
  ("third", Enum_1::third);
REGISTER_ENUM_TRANSLATOR(Enum_1);

DECLARE_ENUM_TRANSLATOR(Enum_2)
  ("foo", Enum_2::foo)
  ("bar", Enum_2::bar)
  ("foobar", Enum_2::foobar);
REGISTER_ENUM_TRANSLATOR(Enum_2);

Note: these macros are not compatible with enums within a namespace or class, due to the double colons (a_namespace::the_enum). As a workaround, a typedef can be done to rename the enumeration, or just do not use the macros in these cases ;).

cbuchart
  • 10,847
  • 9
  • 53
  • 93
  • 2
    A suggested improvement: use ADL to find the enum translator in question. So `template struct tag_t{using type=T;};`, then `auto get_property_translator( tag_t )` in the namespace of `SomeEnum` can be used to find the translator type for `SomeEnum` in any context by just doing `decltype( get_property_translator( tag_t{} ) )`. Also the macro could split the namespace token(s) from the enum type tokens. – Yakk - Adam Nevraumont Sep 06 '18 at 15:15
  • @Yakk-AdamNevraumont thanks for your comment and contribution... let me take a look at it, I think I have to read about a couple of new things to understand it before ;) – cbuchart Sep 06 '18 at 15:51
1

Looking at the header, a good point of customization is:

namespace boost { namespace property_tree
{

  template <typename Ch, typename Traits, typename E, typename Enabler = void>
  struct customize_stream
  {
    static void insert(std::basic_ostream<Ch, Traits>& s, const E& e) {
        s << e;
    }
    static void extract(std::basic_istream<Ch, Traits>& s, E& e) {
        s >> e;
        if(!s.eof()) {
            s >> std::ws;
        }
    }
  };

it has an Enabler field.

namespace boost { namespace property_tree {
  template <typename Ch, typename Traits, typename E>
  struct customize_stream<Ch, Traits, E,
    std::enable_if_t< /* some test */ >
  >
  {
    static void insert(std::basic_ostream<Ch, Traits>& s, const E& e) {
      // your code
    }
    static void extract(std::basic_istream<Ch, Traits>& s, E& e) {
      // your code
    }
  };

where you can put whatever code in // your code and whatever test in /* some test */.

The remaining part is to (a) associate bob::a with "a", and (b) connect this to the above.

I like doing these associations in the namespace of bob, then finding them via ADL.

Create a template<class T> struct tag_t {}. If you pass a tag_t<foo> to a function, ADL will find functions in both the namespace of tag_t and in the namespace of foo.

Create a function that gets the mapping from enum value to string (and back). Suppose your mapping is:

std::vector< std::pair< E, std::string > >

and you just do a linear search. Then:

namespace string_mapping {
  template<class Enum>
  using mapping = std::vector<std::pair< Enum, std::string > >;
}
namespace some_ns {
  enum bob { a, b, c };
  string_mapping::mapping<bob> const& get_string_mapping( tag_t<bob> ) {
    static string_mapping::mapping<bob> retval = {
      {bob::a, "a"},
      {bob::b, "b"},
      {bob::c, "c"},
    };
    return retval;
  }
}

we can find this mapping wherever we have a type T=bob by doing get_string_mapping( tag_t<T>{} ).

Use something like can_apply to detect if get_string_mapping( tag_t<T>{} ) can be found, use that to enable your custom customize_stream to use get_string_mapping to load/save the data to/from streams.

Now all we have to do is reduce the pain of writing get_string_mapping.

#define MAP_ENUM_TO_STRING( ENUM ) \
  string_mapping::mapping<ENUM> const& get_string_mapping( tag_t<ENUM> ) { \
    static string_mapping::mapping<ENUM> retval =

#define END_ENUM_TO_STRING ; return retval; }

use:

 MAP_ENUM_TO_STRING( bob )
    {
      {bob::a, "a"},
      {bob::b, "b"},
      {bob::c, "c"},
    }
 END_ENUM_TO_STRING

within bob's namespace.

If you want something fancier (ooo, sorted lists, or unordered maps), that can easily be done within get_string_mapping or even by a get_efficient_string_mapping that calls get_string_mapping and does a one-off reprocessing of the flat data.

The big advantage of this is that we don't have to do this in the global namespace; we can put it naturally right under an enum or in the enum's namespace.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524