2
\$\begingroup\$

So I've decided to flex showcase my "enum that accepts strings etc..." here.

I don't know if it's great but I'm pretty sure it's decent. It is implemented in less than 200 lines of code (not counting boost helper macros), while something like magic_enum is thousands of lines of code.

An enum-like structure is created from a macro call.

#pragma once

#include <boost/preprocessor/variadic/to_seq.hpp>
#include <boost/preprocessor/seq/for_each.hpp>
#include <boost/preprocessor/stringize.hpp>
#include <vector>
#include <string>
#include <cstring>
#include <ostream>

/*  senum_t.h

    "string enum type".

    custom enum-like structure that accepts string construction/comparison/printing.

    usage (macro call):

        senum_t(NAME_OF_ENUM, IGNORE_CASE, ...ENUM_ENTRIES);
*/

namespace detail {

    std::string tolower(const char* str) {
        size_t slen = std::strlen(str);
        std::string lower(slen, '\0');
        for (size_t c = 0; c < slen; c++) {
            lower[c] = std::tolower(str[c]);
        }
        return lower;
    }
}

#define senum_t_STRINGIZE_CALLBACK(r, data, elem) BOOST_PP_STRINGIZE(elem),
#define senum_t_VA_ARGS_TO_STRINGS(...) BOOST_PP_SEQ_FOR_EACH(senum_t_STRINGIZE_CALLBACK,, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__))

#define senum_t(name, icase, ...) struct name {\
public:\
    typedef int value_type;\
    \
    enum enum_type : value_type {\
        __VA_ARGS__, BAD_STRING,\
    };\
private:\
    void _from_c_str(const char* sval) {/*nicase*/\
        for (size_t i = 0; i < s_ssval.size(); i++) {\
            if (std::strcmp(sval, s_ssval[i].c_str()) == 0) {\
                m_eval = static_cast<enum_type>(i);\
                return;\
            }\
        }\
        m_eval = enum_type::BAD_STRING;\
    }\
    void _from_c_str_i(const char* sval) {/*icase*/\
        std::string svall = detail::tolower(sval);\
        for (size_t i = 0; i < s_ssval.size(); i++) {\
            if (svall == detail::tolower(s_ssval[i].c_str())) {\
                m_eval = static_cast<enum_type>(i);\
                return;\
            }\
        }\
        m_eval = enum_type::BAD_STRING;\
    }\
private:\
    bool _is_equal_from_c_str(const char* sval) const {/*nicase*/\
        return std::strcmp(m_sval.c_str(), sval) == 0;\
    }\
    bool _is_equal_from_c_str_i(const char* sval) const {/*icase*/\
        return detail::tolower(m_sval.c_str()) == detail::tolower(sval);\
    }\
public:\
    name(const char* sval) : m_sval{ sval } {\
        s_icase ?\
            _from_c_str_i(sval) :\
            _from_c_str(sval);\
    }\
    name(const std::string& sval) : name(sval.c_str()) {\
    }\
    name(const enum_type eval) : m_eval{ eval } {\
        m_sval = s_ssval[static_cast<size_t>(eval)];\
    }\
    name() : name("") {\
    }\
public:\
    bool operator==(const name& other) const {\
        return m_eval == other.m_eval;\
    }\
    bool operator==(const enum_type eval) const {\
        return m_eval == eval;\
    }\
    bool operator==(const char* sval) const {\
        return s_icase ?\
            _is_equal_from_c_str_i(sval) :\
            _is_equal_from_c_str(sval);\
    }\
    bool operator==(const std::string& sval) const {\
        return s_icase ?\
            _is_equal_from_c_str_i(sval.c_str()) :\
            _is_equal_from_c_str(sval.c_str());\
    }\
public:\
    operator const enum_type& () const {\
        return m_eval;\
    }\
    operator const std::string& () const {\
        return m_sval;\
    }\
public:\
    static std::vector<std::string>::const_iterator sscbegin() {\
        return s_ssval.cbegin();\
    }\
    static std::vector<std::string>::const_iterator sscend() {\
        return s_ssval.cend();\
    }\
private:\
    enum_type\
        m_eval;\
    std::string\
        m_sval;\
    static const bool\
        s_icase;\
    static const std::vector<std::string>\
        s_ssval;\
    \
    friend bool operator==(const name::enum_type, const name&);\
    friend bool operator==(const char*, const name&);\
    friend bool operator==(const std::string&, const name&);\
    friend std::ostream& operator<<(std::ostream&, const name&);\
    friend std::ostream& operator<<(std::ostream&, const name::enum_type&);\
};\
/*static s_icase*/\
const bool name::s_icase = icase;\
/*static s_ssval*/\
const std::vector<std::string> name::s_ssval = {\
    senum_t_VA_ARGS_TO_STRINGS(__VA_ARGS__)\
    "BAD_STRING",\
};\
/*operator==(reverse order)*/\
bool operator==(const name::enum_type eval, const name& senum) {\
    return eval == senum.m_eval;\
}\
/*operator==(reverse order)*/\
bool operator==(const char* sval, const name& senum) {\
    return name::s_icase ?\
        detail::tolower(sval) == detail::tolower(senum.m_sval.c_str()) :\
        std::strcmp(sval, senum.m_sval.c_str()) == 0;\
}\
/*operator==(reverse order)*/\
bool operator==(const std::string& sval, const name& senum) {\
    return name::s_icase ?\
        detail::tolower(sval.c_str()) == detail::tolower(senum.m_sval.c_str()) :\
        sval == senum.m_sval;\
}\
/*printing*/\
std::ostream& operator<<(std::ostream& os, const name& senum) {\
    os << senum.m_sval;\
    return os;\
}\
/*bonus printing (with enum_type)*/\
std::ostream& operator<<(std::ostream& os, const name::enum_type& eval) {\
    os << name(eval).m_sval;\
    return os;\
}

Interactive demo: https://onlinegdb.com/mmEu7xZuK

Is the implementation lacking ?

Is the implementation going too far (looking at "out-of-class" friended == operators here) ?

Note: the current implementation doesn't accept non-default values for enum members. It could be implemenented with a std::map instead of std::vector for storing enum string names; a little more macro work is required for that.

Note: with help provided on macros here https://stackoverflow.com/questions/77861361

UPDATE: I have noticed this design can produce duplicate symbols from different compilation units, so I've updated the code to use inline functions, and created a separate compilation unit for detail::tolower. The updated code can be found here https://onlinegdb.com/t77D7PDtxH

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Reduce the reliance on macros

Your code does everything using macros, but I would try to use plain C++ as much as possible, and only rely on macros for the bits that can't be done in C++ itself. You can create templates of the functions and types you want to specialize for each enum you want to declare. For example:

namespace senum {
template<class Enum>
const std::vector<std::string> names;

template<class Enum>
const bool icase;
}

template<class Enum>
std::string to_string(Enum f) {
    return senum::names<Enum>[static_cast<std::size_t>(f)];
}

…

#define senum_t(name, icase, ...)\
enum name { __VA_ARGS__ };\
template<> const bool senum::icase<name> = icase;\
template<> const std::vector<std::string> senum::names<name> {\
    senum_t_VA_ARGS_TO_STRINGS(__VA_ARGS__)\
};

So senum_t() will now declare a real enum, and create a specialization of senum::names<> that will hold the names of all the enum values, and senum::icase<> will tell you if you should treat it case insensitive or not. Then add templated out-of-class function that will do string conversions and other operations as you like. Then the code using it looks like:

senum_t(DUMMY, false, hello, world, leet, other);

int main() {
    DUMMY dummy = hello;
    std::cout << to_string(dummy) << '\n';
}

Note that the above can be improved by using static/inline/constexpr, regular arrays with const char* strings to avoid the overhead of std::vector and std::string, and you might want to constrain to_string() so this definition only triggers for types that actually have a corresponding list of names. This is left as an exercise for the reader.

Have a look at the Magic Enum library

With most compilers, you can in fact do static reflection on enums, without having to use macros to declare those enums. Have a look at the Magic Enum library. You could either use it directly, or look at its source code to draw inspiration.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.