3
\$\begingroup\$

I've been working on some kind of mapper that would allow me to map between types.

My objectives:

  • it should allow mapping between two enums
  • it should allow mapping enum to string_view
  • it should allow reverse mapping using a single configuration (enum1 <-> enum2, enum <-> string_view)
  • the fewer resources it uses the better
  • the more efficient it is the better (not essential)
  • easy use, clear interface
  • conformed to C++20 standard
  • compilable with gcc 10.2 up

I've already created some code. At the first glance it does what I want, also has quite easy to use interface, but I'm looking for some potential problems, as well as some places where I could do some refactoring. Also, maybe it just seemingly looks ok but as the usage of this mapper will spread around my code base, some problems may emerge.

Code is below, also I'm attaching the Compiler Explorer link: click

I'm looking forward to hearing your opinions.

// #include <iostream> #include <tuple> #include <optional> #include <string_view> #include <cassert> template<size_t Size> struct FixedString { char data_[Size + 1] {}; constexpr FixedString(const char* src) { std::copy(src, src + Size, std::begin(data_)); } constexpr FixedString(const std::string_view src) { std::copy(src, src.size(), std::begin(data_)); } constexpr operator const char*() const { return data_; } constexpr operator std::string_view() const { return std::string_view { data_}; } }; template<size_t Size> FixedString(const char (&)[Size]) -> FixedString<Size - 1>; template<auto T1, auto T2> struct Tie { constexpr static decltype(T1) t1 = T1; constexpr static decltype(T2) t2 = T2; }; template<auto T1, FixedString T2> struct TieStr { constexpr static decltype(T1) t1 = T1; constexpr static decltype(T2) t2 = T2; }; template<class = void> class Mapper; template<class T> struct IsFixedString : std::false_type {}; template<size_t N> struct IsFixedString<FixedString<N>> : std::true_type {}; template<class Config> class Mapper { public: template<class...T> using MakeConfig = std::tuple<T...>; template<class...T> using MakeMapper = Mapper<MakeConfig<T...>>; template<class Destination, class Source> static constexpr auto map(const Source& source) { return do_map<Destination, Source>(source); } private: template<class Destination, class Source> static constexpr auto do_map(const Source& source) { static_assert(!std::is_same_v<Destination, Source>, "Destination and Source types cannot be the same."); return mape_types<Destination>(source, std::make_integer_sequence<uint32_t, std::tuple_size_v<Config>>{}); } template<bool Flag = false> static constexpr void type_mismatch() { static_assert(Flag, "Mapper cannot perform mapping."); } template <class Destination, class Source, uint32_t... Is> static auto mape_types(const Source& source, std::integer_sequence<uint32_t, Is...>) { auto destination = std::optional<Destination>{}; ( tryToMap<Is, Destination, Source>(destination, source) || ... ); return destination; } template<auto Idx, class Destination, class Source> static bool tryToMap(std::optional<Destination>& ret_val, const Source& src) { using SelectedTie = std::tuple_element_t<Idx, Config>; using RawDst_t = std::decay_t<Destination>; using RawSrc_t = std::decay_t<Source>; using RawT1_t = std::decay_t<decltype(SelectedTie::t1)>; using RawT2_t = std::decay_t<decltype(SelectedTie::t2)>; static_assert( not std::is_convertible_v<RawT1_t, RawDst_t> || not std::is_convertible_v<RawT2_t, RawDst_t> || std::is_same_v<RawDst_t, RawT1_t> || std::is_same_v<RawDst_t, RawT2_t>, "Destination type does not belong to the configuration." ); if constexpr (std::is_same_v<RawSrc_t, RawT1_t> || std::is_convertible_v<RawSrc_t, RawT1_t>) { if (SelectedTie::t1 == src) { ret_val.emplace(SelectedTie::t2); return true; } } else if constexpr (std::is_same_v<RawSrc_t, RawT2_t> || std::is_convertible_v<RawSrc_t, RawT2_t>) { if (SelectedTie::t2 == src) { ret_val.emplace(SelectedTie::t1); return true; } } else { type_mismatch(); } return false; } }; enum class Enum1_src { A, B, C }; enum class Enum1_dst { A, B, C }; //Configure mapper using Mp1 = Mapper<>::MakeMapper< Tie<Enum1_src::A, Enum1_dst::A>, Tie<Enum1_src::B, Enum1_dst::B>, Tie<Enum1_src::C, Enum1_dst::C> >; using Mp2StrMapper = Mapper<>::MakeMapper< TieStr<Enum1_src::A, "Enum1_src::A_string">, TieStr<Enum1_src::B, "Enum1_src::B_string">, TieStr<Enum1_src::C, "Enum1_src::C_string"> >; int main() { using namespace std::literals; //enum to string_view assert(Mp2StrMapper::map<std::string_view>(Enum1_src::A).value() == "Enum1_src::A_string"); assert(Mp2StrMapper::map<std::string_view>(Enum1_src::B).value() == "Enum1_src::B_string"); assert(Mp2StrMapper::map<std::string_view>(Enum1_src::C).value() == "Enum1_src::C_string"); //string_view to enum assert(Mp2StrMapper::map<Enum1_src>("Enum1_src::A_string"sv) == Enum1_src::A); assert(Mp2StrMapper::map<Enum1_src>("Enum1_src::B_string"sv) == Enum1_src::B); assert(Mp2StrMapper::map<Enum1_src>("Enum1_src::C_string"sv) == Enum1_src::C); //enum to enum assert(Mp1::map<Enum1_dst>(Enum1_src::A) == Enum1_dst::A); assert(Mp1::map<Enum1_dst>(Enum1_src::B) == Enum1_dst::B); assert(Mp1::map<Enum1_dst>(Enum1_src::C) == Enum1_dst::C); } 
\$\endgroup\$

    1 Answer 1

    2
    \$\begingroup\$

    General

    There's a clear need for this functionality. I frequently find myself writing conversions between the enumerations used by differently libraries, or between enums and strings for saving and restoring settings in files.

    For enumerations, I usually use a switch:

    auto gd_to_fr(auto gd) { switch (gd) { case dubh: return sable; case uaine: return vert; case gorm: return bleu; } } 

    This has the advantage that we can have the compiler check that all cases are covered. However, we don't get to check the reverse mapping is consistent until the unit-tests run.

    Fixed String

    I see the FixedString class serves these functions:

    1. Ensures that string literals can be compared using == operator like C++ strings.
    2. Prevents use of modifiable string values in maps.
    3. Enables compile-time use of string values.

    Since C++20, std::string has a constexpr constructor, so I believe all three concerns can be addressed by using the standard library class (in conjunction with consteval functions) rather than creating this one of our own.

    One limitation of the current implementation here is that it only deals with char strings, and fails to handle wide-char or UTF strings.

    The IsFixedString predicate is never used - I'm guessing a remnant of an abandoned experiment?

    Tie

    The separate TieStr class shouldn't be necessary, if we can arrange for character arrays to convert to std::string automatically. We can't do that right now, because we're using template metaprogramming to ensure everything our maps are compile-time constructed, and so we need structural types to use as the non-type template parameters.

    I recommend we move away from value-templated types to real objects, but use consteval to enforce compile-time construction.

    Mapper

    The class requires lots of template arguments to create and use, which affects readability of the code that uses it.

    We can't use the same type for source and destination. This could be useful for state machine transitions, or for the classic rock-paper-scissors game (where we could use a map for the "beats" relationship).


    Suggested replacement

    We'd like a simple-to-use interface, where we can construct a mapper as simply as

    constexpr mapper mp1{ Enum1_src::A, Enum1_dst::A, Enum1_src::B, Enum1_dst::B, Enum1_src::C, Enum1_dst::C }; 

    and use it with a simple function call:

    static_assert(mp1.map(Enum1_src::A) == Enum1_dst::A); static_assert(mp1.map(Enum1_src::B) == Enum1_dst::B); static_assert(mp1.map(Enum1_src::C) == Enum1_dst::C); 

    That suggests a recursive template:

    template<typename Key, typename Value, typename... Rest> class mapper { const Key key; const Value value; const mapper<Rest...> next; public: consteval mapper(Key k, Value v, Rest... rest) : key{k}, value{v}, next{rest...} { } constexpr auto map(const Key& k) const { return key == k ? value : next.map(k); } }; template<typename Key, typename Value> struct mapper<Key, Value> { const Key key; const Value value; constexpr auto map(const Key& k) const { if (k == key) { return value; } throw std::invalid_argument("key not found"); } }; 

    This gives us a linear search, which should be fine for normal-sized enumerations (and decent compilers will optimise the if-else if chain to a 'switch' where that's reasonable).

    To have the reverse mapping, we just need an additional member function in each class:

     constexpr auto rmap(const Value& v) const { return value == v ? key : next.rmap(v); } 
     constexpr auto rmap(const Value& v) const { if (v == value) { return key; } throw std::invalid_argument("key not found"); } 

    The remaining problem is to ensure that character arrays are converted to (equality-comparable) string view objects.

    We can do this by laundering our template arguments through a function that converts character pointers, but passes other types unchanged:

    template<typename Value> consteval auto map_value(Value v) { return v; } template<typename CharT> consteval std::basic_string_view<CharT> map_value(const CharT *v) { return {v}; } template<typename Value> using map_value_type = decltype(map_value(std::declval<Value>())); 

    We then need to change the member types in the mapper classes:

     using key_type = map_value_type<Key>; using value_type = map_value_type<Value>; const key_type key; const value_type value; public: consteval mapper(Key k, Value v, Rest... rest) : key{map_value(k)}, value{map_value(v)}, next{rest...} { } 

    Modified code

    Combining the above, and applying some small refactorings, we get

    #include <stdexcept> #include <string_view> #include <utility> namespace impl { template<typename Value> consteval auto map_value(Value v) { return v; } template<typename CharT> consteval std::basic_string_view<CharT> map_value(const CharT *v) { return {v}; } template<typename Value> using map_value_type = decltype(map_value(std::declval<Value>())); template<typename Key, typename Value> struct base_mapper { using key_type = map_value_type<Key>; using value_type = map_value_type<Value>; const key_type key; const value_type value; consteval base_mapper(Key k, Value v) : key{map_value(k)}, value{map_value(v)} { } }; } template<typename Key, typename Value, typename... Rest> class mapper : impl::base_mapper<Key, Value> { using base = impl::base_mapper<Key, Value>; const mapper<Rest...> next; public: consteval mapper(Key k, Value v, Rest... rest) : base{k, v}, next{rest...} { } constexpr auto map(const base::key_type& k) const { return base::key == k ? base::value : next.map(k); } constexpr auto rmap(const base::value_type& v) const { return base::value == v ? base::key : next.rmap(v); } }; template<typename Key, typename Value> class mapper<Key, Value> : impl::base_mapper<Key, Value> { using base = impl::base_mapper<Key, Value>; public: consteval mapper(Key k, Value v) : base{k, v} { } constexpr auto map(const base::key_type& k) const { if (k == base::key) { return base::value; } throw std::invalid_argument("key not found"); } constexpr auto rmap(const base::value_type& v) const { if (v == base::value) { return base::key; } throw std::invalid_argument("key not found"); } }; 

    And the test:

    #include <cassert> using std::literals::string_view_literals::operator""sv; enum class Enum1_src { A, B, C }; enum class Enum1_dst { A, B, C }; constexpr mapper mp1{ Enum1_src::A, Enum1_dst::A, Enum1_src::B, Enum1_dst::B, Enum1_src::C, Enum1_dst::C }; static_assert(mp1.map(Enum1_src::A) == Enum1_dst::A); static_assert(mp1.map(Enum1_src::B) == Enum1_dst::B); static_assert(mp1.map(Enum1_src::C) == Enum1_dst::C); static_assert(mp1.rmap(Enum1_dst::A) == Enum1_src::A); static_assert(mp1.rmap(Enum1_dst::B) == Enum1_src::B); static_assert(mp1.rmap(Enum1_dst::C) == Enum1_src::C); // Mixing string views and literals is fine constexpr mapper mp2{ Enum1_src::A, "A"sv, Enum1_src::B, "B", Enum1_src::C, "C"sv }; // return type is a string view static_assert(mp2.map(Enum1_src::A) == "A"sv); static_assert(mp2.map(Enum1_src::A) == "A"); static_assert(mp2.map(Enum1_src::B) == "B"sv); static_assert(mp2.map(Enum1_src::C) == "C"sv); // argument type is a string view static_assert(mp2.rmap("A") == Enum1_src::A); static_assert(mp2.rmap("A"sv) == Enum1_src::A); static_assert(mp2.rmap("B"sv) == Enum1_src::B); static_assert(mp2.rmap("C"sv) == Enum1_src::C); int main() { char a[] = "A"; //mapper m{Enum1_src::A, a}; // compile-time error: a is not constexpr assert(mp2.rmap(a) == Enum1_src::A); a[0] = 'B'; assert(mp2.rmap(a) == Enum1_src::B); } 

    Alternative implementation

    We could use the (newly-constexpr) algorithms to binary-search an array of pairs for the lookup. The user interface is slightly different (unless someone can show how do deduce std::pair within the initializer list):

    constexpr mapper mp1 = { std::pair{ Enum1_src::A, Enum1_dst::A }, std::pair{ Enum1_src::B, Enum1_dst::B }, std::pair{ Enum1_src::C, Enum1_dst::C } }; // Mixing string views and literals is (still) fine constexpr mapper mp2{ std::pair{ Enum1_src::A, "A"sv }, std::pair{ Enum1_src::B, "B" }, std::pair{ Enum1_src::C, "C"sv } }; 

    The mapper class then has two arrays (one for the forward map, and one for the reverse), and a lookup function:

     template<typename Key, typename Value, std::size_t N> class mapper { std::array<std::pair<Key,Value>, N> forward; std::array<std::pair<Value,Key>, N> reverse; public: template<typename... Args> consteval mapper(Args... values) : forward{values...}, reverse{} { std::ranges::transform(forward, reverse.begin(), [](auto p){return std::pair{p.second, p.first};}); std::ranges::sort(forward); std::ranges::sort(reverse); } constexpr auto map(const Key& k) const { return lookup(k, forward); } constexpr auto rmap(const Value& v) const { return lookup(v, reverse); } private: template<typename A, typename B> static constexpr B lookup(A key, std::array<std::pair<A,B>, N> const& values) { auto const keypair = std::pair{key, B{}}; auto const result_range = std::ranges::equal_range(values, keypair, [](auto a, auto b){return a.first < b.first;}); switch (result_range.size()) { case 0: throw std::invalid_argument("key not found"); case 1: return result_range.front().second; default: throw std::invalid_argument("duplicate key detected"); } } }; 

    It needs a deduction guide:

    template<typename Key, typename Value, typename... Args> mapper(std::pair<Key, Value>, Args...) -> mapper<impl::map_value_type<Key>, impl::map_value_type<Value>, 1 + sizeof... (Args)>; 
    \$\endgroup\$
    11
    • 1
      \$\begingroup\$Nice, but why not create a KeyValuePair type (or whatever you want to call it), to avoid some typing in the code and also to be able to group keys and values like constexpr mapper mp1{ {Enum1_src::A, Enum1_dst::A}, {...}, ...}. This will make it easier to visually spot errors, like missing one enum, that would otherwise result in a hard to decode error message from the compiler.\$\endgroup\$CommentedMay 16, 2022 at 20:53
    • \$\begingroup\$@G.Sliepen, certainly that's a way to do it, and probably better, because we could then use a std::initializer_list<std::pair<Key,Value>> constructor, possibly with the newly-constexpr sort and binary-search algorithms in the implementation. Can I claim that my code is just intended as an illustration of how the consteval object approach gives simpler code than the metaprogrammed template class, rather than the best implementation of the object?\$\endgroup\$CommentedMay 17, 2022 at 6:57
    • \$\begingroup\$Hey Toby I like your version. It's quite easy to understand. I just added one small improvement, template<class T> constexpr auto automap(const T& key) const { if constexpr (std::is_same_v<impl::MapValue_t<T>, typename Base_t::Key_t>) { return map(key); } else { return rmap(key); } } that allows to automatically select whetner we map or reverse map\$\endgroup\$
      – bielu000
      CommentedMay 17, 2022 at 6:57
    • \$\begingroup\$G.Sliepen any improvement will be appreciated, so if you see something that you could improve feel free to share it with us :)\$\endgroup\$
      – bielu000
      CommentedMay 17, 2022 at 7:00
    • 1
      \$\begingroup\$I'll probably extend this answer using what I wrote in my comment here.\$\endgroup\$CommentedMay 17, 2022 at 8:06

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.