HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Enum to string with template metaprogramming

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
templatewithenummetaprogrammingstring

Problem

This allows enum values to be outputted as strings. But enum_strings is a type, not a map or a vector, so if the enum value is known during compile time, there will be no look-up time at all. If the enum value is a run-time value, then the look-up time is \$O(\log N)\$ due to a binary search being made through the pack.

Note: This compiles on GCC 5.3. I did not run it on other compilers.

```
#include
#include
#include
#include
#include

template
struct string_literal {
static constexpr char value[sizeof...(Cs)] = {Cs...};
};
template constexpr char string_literal::value[sizeof...(Cs)];

template
constexpr string_literal operator ""_e() { return {}; }

enum Colour {Red, Blue, Green};
enum Animal {Dog, Cat, Bird};
enum Fruit {Apple = -1, Orange = 3, Grape = 5, Banana = 100};

template
struct E : std::integral_constant {
using type = T;
};

using enum_strings = std::tuple, E, E>,
std::tuple, E, E>,
std::tuple, E, E, E>
>;

template struct get_enum_pack;

template class P, typename First, typename... Rest>
struct get_enum_pack> : get_enum_pack> {};

template class P, typename... Es, typename... Packs>
struct get_enum_pack, Packs...>> {
using type = P;
};

template struct enum_binary_search;

template class P, int... Is, typename... Ts>
struct enum_binary_search...>> {
using Tuple = std::tuple...>;
static std::istream& execute (std::istream& is, T& t) {
std::string buf;
is >> buf;
execute_impl(buf, t);
return is;
}
private:
template
static void execute_impl (const std::string& buf, T& t) {
constexpr std::size_t MidIndex = (MinIndex + MaxIndex) / 2;
using S = std::tuple_element_t;
const std::string str = S::type::value;
if (buf == str)
t = static_cast(S::value);
else if (buf (buf, t); // Replacing MidIndex with MidIndex - 1 leads to compiling error (array subscript out of bounds) for reasons I don't understand.
else if

Solution

Putting multiple enum values into one enum_strings data structure means this one piece of code will depend on every single enum definition in the code-base where conversion to a string is required. It's also easy to forget to add a value.

Having a separate enum_strings-type structure for each enum would mean that the strings can be defined near the enum, and one is less likely to forget to add things to one central point. Headers containing unrelated enums from other places also won't be included accidentally.

enum_binary_search::execute_impl converts to std::string to do comparisons. This is likely to be very slow (it allocates memory and copys over all the data in the array). It also does the comparison multiple times.

std::strcmp can be used to do the comparison once, with no copying.

It's complicated and unreadable! This isn't your fault, it's just how template metaprogramming is in C++. Generally, constexpr-based solutions are simpler, and promise to get much easier with future versions of the language.

For example, here's a version that does a linear search, based on this stackoverflow answer.

#include 

class CExprStr // use std::string_view in C++17
{
public:

    template
    constexpr CExprStr(char const (&s)[N]): m_size(N), m_data(s) { }

    constexpr char operator[](std::size_t index) const
    {
        return
            (index >= m_size) ? throw std::out_of_range("Invalid index.") :
            m_data[index];
    }

    constexpr char const* data() const
    {
        return m_data;
    }

private:

    std::size_t m_size;
    char const* m_data;
};

namespace 
{

    constexpr bool EqualsImpl(CExprStr const& a, char const* b, std::size_t i)
    {
        return
            (a[i] != b[i]) ? false :
            (a[i] == 0 || b[i] == 0) ? true :
            EqualsImpl(a, b, i + 1);
    }

} // unnamed

constexpr bool operator==(CExprStr const& a, char const* b)
{
    return EqualsImpl(a, b, 0);
}

#include 
#include 

template
using EnumStringT = std::tuple;

template
using EnumMapT = std::array, N>;

template
constexpr char const* EnumGetValue(EnumMapT const& map, EnumT key, std::size_t i = 0)
{
    return
        (i == map.size()) ? throw std::invalid_argument("Enum key not present in map.") : // (will also fail to compile, as it's not a constant expression).
        (std::get(map[i]) == key) ? std::get(map[i]).data() : 
        EnumGetValue(map, key, i + 1);
}

template
constexpr EnumT EnumGetKey(EnumMapT const& map, char const* value, std::size_t i = 0)
{
    return
        (i == map.size()) ? throw std::invalid_argument("Enum value not present in map.") : // (will also fail to compile, as it's not a constant expression).
        (std::get(map[i]) == value) ? std::get(map[i]) :
        EnumGetKey(map, value, i + 1);
}

#include 
#include 
#include 

enum class Color { Red, Green, Blue };

namespace
{
    using E = EnumStringT;

    constexpr EnumMapT ColorStringMap =
    {{
        E{ Color::Red, "red" },
        E{ Color::Green, "green" },
        E{ Color::Blue, "blue" },
    }};

} // unnamed 

constexpr char const* ColorToString(Color value)
{
    return EnumGetValue(ColorStringMap, value);
}

std::ostream& operator>(std::istream& stream, Color& value)
{
    auto buffer = std::string();
    stream >> buffer;

    value = StringToColor(buffer.c_str());

    return stream;
}

#include 
#include 

int main()
{
    {
        constexpr auto red = ColorToString(Color::Red);
        //constexpr auto invalid = ColorToString((Color)-1); // won't compile
        //auto invalid = ColorToString((Color)-1); // throws std::invalid_argument
        std::cout > c;

        std::cout << c << std::endl;
    }
}


CExprStr exists purely to do string comparison between const char*s, since std::strcmp isn't a constexpr function. In C++17, we can delete the whole class and use std::string_view instead.

Similarly, in C++20, a whole bunch of std algorithms become constexpr, including std::find_if, std::binary_search, std::lexicographical_compare, which makes a constexpr based solution more appealing.

It should be fairly simple to add binary lookup to EnumGetValue and EnumGetKey above. Sorting the array in a constexpr function will still be tricky in C++14, but slightly easier in C++17.

Code Snippets

#include <stdexcept>

class CExprStr // use std::string_view in C++17
{
public:

    template<std::size_t N>
    constexpr CExprStr(char const (&s)[N]): m_size(N), m_data(s) { }

    constexpr char operator[](std::size_t index) const
    {
        return
            (index >= m_size) ? throw std::out_of_range("Invalid index.") :
            m_data[index];
    }

    constexpr char const* data() const
    {
        return m_data;
    }

private:

    std::size_t m_size;
    char const* m_data;
};

namespace 
{

    constexpr bool EqualsImpl(CExprStr const& a, char const* b, std::size_t i)
    {
        return
            (a[i] != b[i]) ? false :
            (a[i] == 0 || b[i] == 0) ? true :
            EqualsImpl(a, b, i + 1);
    }

} // unnamed

constexpr bool operator==(CExprStr const& a, char const* b)
{
    return EqualsImpl(a, b, 0);
}


#include <tuple>
#include <stdexcept>

template<class EnumT>
using EnumStringT = std::tuple<EnumT, CExprStr>;

template<class EnumT, std::size_t N>
using EnumMapT = std::array<EnumStringT<EnumT>, N>;

template<class EnumT, std::size_t N>
constexpr char const* EnumGetValue(EnumMapT<EnumT, N> const& map, EnumT key, std::size_t i = 0)
{
    return
        (i == map.size()) ? throw std::invalid_argument("Enum key not present in map.") : // (will also fail to compile, as it's not a constant expression).
        (std::get<0>(map[i]) == key) ? std::get<1>(map[i]).data() : 
        EnumGetValue(map, key, i + 1);
}

template<class EnumT, std::size_t N>
constexpr EnumT EnumGetKey(EnumMapT<EnumT, N> const& map, char const* value, std::size_t i = 0)
{
    return
        (i == map.size()) ? throw std::invalid_argument("Enum value not present in map.") : // (will also fail to compile, as it's not a constant expression).
        (std::get<1>(map[i]) == value) ? std::get<0>(map[i]) :
        EnumGetKey(map, value, i + 1);
}


#include <array>
#include <iostream>
#include <string>

enum class Color { Red, Green, Blue };

namespace
{
    using E = EnumStringT<Color>;

    constexpr EnumMapT<Color, 3> ColorStringMap =
    {{
        E{ Color::Red, "red" },
        E{ Color::Green, "green" },
        E{ Color::Blue, "blue" },
    }};

} // unnamed 

constexpr char const* ColorToString(Color value)
{
    return EnumGetValue(ColorStringMap, value);
}

std::ostream& operator<<(std::ostream& stream, Color value)
{
    stream << ColorToString(value);
    return stream;
}

constexpr Color StringToColor(char const* value)
{
    return EnumGetKey(ColorStringMap, value);
}

std::istream& operator>>(std::istream& stream, Color& value)
{
    auto buffer = std::string();
    stream >> buffer;

    value = StringToColor(buffer.c_str());

    return stream;
}


#include <iostream>
#include <sstream>

int main()
{
    {
        constexpr auto red = ColorToString(Color::Red);
        //constexpr auto invalid = ColorToString((Color)-1); // won't compile
        //auto invalid = ColorToString((Color)-1); // throws std::invalid_argument
        s

Context

StackExchange Code Review Q#125953, answer score: 2

Revisions (0)

No revisions yet.