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
1 Answer 1
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.