Open lsemprini opened 1 month ago
Correction: I was missing a std::decay_t<>
, and with that added, is_flags
can be defined as static constexpr bool is_flags = true
just like when using magic_enum::customize::enum_range<T>
Corrected has_is_flags
:
// you must define an ADL-reachable function magic_enum_enum_range(e):
//
// typedef enum e1 {} e1;
// struct e1_enum_range
// {
// // here is the same stuff you would put into customize::enum_range<T>
// static constexpr bool is_flags = true;
// };
// // include "friend" if this function is in a class
// // exclude "friend" if this function is in a namespace (including global scope)
// friend inline constexpr auto magic_enum_enum_range(e1) { return e1_enum_range(); }
//
template <typename T>
struct has_is_flags
<
T,
std::enable_if_t
<
std::is_same_v
<
bool,
std::decay_t /*const bool->just bool*/
<
decltype(decltype(magic_enum_enum_range(T{}))::is_flags)
>
>
>
>
: std::true_type {};
Corrected get_is_flags
:
template <typename T, typename = void>
struct get_is_flags : std::false_type {};
// - return type already checked in has_is_flags
template <typename T>
struct get_is_flags<T, std::void_t<decltype(customize::enum_range<T>::is_flags)>> : std::bool_constant<customize::enum_range<T>::is_flags> {};
template <typename T>
struct get_is_flags
<
T,
std::enable_if_t
<
std::is_same_v
<
bool,
std::decay_t /*const bool->just bool*/
<
decltype(decltype(magic_enum_enum_range(T{}))::is_flags)
>
>
>
>
:
std::bool_constant< decltype(magic_enum_enum_range(T{}))::is_flags > {};
Hi,
I needed to adjust range_min
and range_max
by ADL too, so I finished the code for all enum_range
parameters.
While doing that, I realized that we need to make sure ADL decltype(magic_enum_enum_range(e))::parameter
takes priority over namespace customize::enum_range<E>::parameter
specialization if both are defined (rather than generating a compiler error).
This is important in part because the existing magic_enum.hpp
code already defines a min
and max
member for all E
:
namespace customize {
...
template <typename E>
struct enum_range {
static constexpr int min = MAGIC_ENUM_RANGE_MIN;
static constexpr int max = MAGIC_ENUM_RANGE_MAX;
};
and so if we didn't make ADL take priority, then customize::enum_range<E>
would always override the ADL setting for min
and max
or always generate compiler errors due to conflict.
Actually, the definitions of min
and max
here are not needed (since range_min
and range_max
fall back to MAGIC_ENUM_RANGE_*
) but it's probably too late to remove them since someone might have written code that relies on them.
So here is the code that handles is_flags
, min
, and max
.
It is much shorter and easier to understand than the old code because it uses if constexpr
I wrote some better documentation that you can use in your README (see comments):
// HAS_MEMBER() an improvement on https://stackoverflow.com/a/62292282/1046167
// - lets you quickly test for existence of a member or ADL func with a type CONTAINER
// - the test expression EXPR must use "_" where it wants to look in CONTAINER, e.g.:
// HAS_MEMBER_V(T, _::mymember)
// HAS_MEMBER_V(T, my_traits_thing<_>::mymember)
// HAS_MEMBER_V(T, my_adl_func(_)::mymember)
//
template<typename CONTAINER, typename FUNC>
consteval auto _has_member(FUNC&& func) ->
decltype(func.template operator()<CONTAINER>(), bool())
{
return true;
}
template<typename>
consteval bool _has_member(...)
{
return false;
}
#define HAS_MEMBER_T(CONTAINER, EXPR) \
_has_member<CONTAINER>( []<class _>() consteval -> EXPR {})
#define HAS_MEMBER_V(CONTAINER, EXPR) \
_has_member<CONTAINER>( []<class _>() consteval -> decltype(EXPR) {})
// --------------------------------------------------------------------------------------
// two ways of accessing per-T enum_range parameters:
// - #2 (ADL) takes priority over #1 (customize::) if present
//
// #1. partially specialize customize::enum_range<T> struct
// - must specialize in global scope
//
// typedef enum e1 {} e1;
// template <>
// struct magic_enum::customize::enum_range< e1 >
// {
// static constexpr bool is_flags = true;
// static constexpr int min = 0;
// static constexpr int max = 100;
// };
//
template<typename T>
using enum_range_customize_struct = magic_enum::customize::enum_range<T>;
//
// #2. decltype(magic_enum_enum_range(T{})) ADL struct
//
// define an ADL-reachable function magic_enum_enum_range(e):
// - can define func in same class/namespace as the enum itself
// - func returns the type of enum_range struct with static members
// - func will always be called inside decltype()
// - func must be constexpr
//
// typedef enum e1 {} e1;
// struct e1_enum_range
// {
// static constexpr bool is_flags = true;
// static constexpr int min = 0;
// static constexpr int max = 100;
// };
// // include "friend" if this function is in a class
// // exclude "friend" if this function is in a namespace (including global scope)
// friend inline constexpr auto magic_enum_enum_range(e1) { return e1_enum_range(); }
//
template<typename T>
using enum_range_adl_struct = decltype(magic_enum_enum_range(T{}));
// error-check the enum_range member types the programmer provided
template <typename T>
constexpr bool _is_flags_sanity()
{
if constexpr (HAS_MEMBER_V(T, enum_range_adl_struct<_>::is_flags))
{
static_assert(std::is_same_v<bool,
std::decay_t< decltype(enum_range_adl_struct<T>::is_flags) > >);
}
else if constexpr (HAS_MEMBER_V(T, enum_range_customize_struct<_>::is_flags))
{
static_assert(std::is_same_v<bool,
std::decay_t< decltype( enum_range_customize_struct<T>::is_flags) > >);
}
return true;
}
template <typename T>
constexpr bool _has_is_flags()
{
static_assert(_is_flags_sanity<T>());
return (HAS_MEMBER_V(T, enum_range_adl_struct<_>::is_flags) ||
HAS_MEMBER_V(T, enum_range_customize_struct<_>::is_flags));
}
template <typename T>
using has_is_flags = std::bool_constant< _has_is_flags<T>() >;
template <typename T>
constexpr bool _get_is_flags()
{
static_assert(_is_flags_sanity<T>());
if constexpr (HAS_MEMBER_V(T, enum_range_adl_struct<_>::is_flags))
{
return enum_range_adl_struct<T>::is_flags;
}
else if constexpr (HAS_MEMBER_V(T, enum_range_customize_struct<_>::is_flags))
{
return enum_range_customize_struct<T>::is_flags;
}
else
{
// default non-flags
return false;
}
}
template <typename T>
using get_is_flags = std::bool_constant< _get_is_flags<T>() >;
template <typename T>
constexpr auto _range_min()
{
if constexpr (HAS_MEMBER_V(T, enum_range_adl_struct<_>::min))
{
static_assert(std::is_integral_v
< std::decay_t < decltype(enum_range_adl_struct<T>::min) > >);
return enum_range_adl_struct<T>::min;
}
else if constexpr (HAS_MEMBER_V(T, enum_range_customize_struct<_>::min))
{
static_assert(std::is_integral_v
< std::decay_t< decltype( enum_range_customize_struct<T>::min) > >);
return enum_range_customize_struct<T>::min;
}
else
{
return MAGIC_ENUM_RANGE_MIN;
}
}
template <typename T>
using range_min = std::integral_constant < std::decay_t<decltype(_range_min<T>())>, _range_min<T>() >;
template <typename T>
constexpr auto _range_max()
{
if constexpr (HAS_MEMBER_V(T, enum_range_adl_struct<_>::max))
{
static_assert(std::is_integral_v
< std::decay_t < decltype(enum_range_adl_struct<T>::max) > >);
return enum_range_adl_struct<T>::max;
}
else if constexpr (HAS_MEMBER_V(T, enum_range_customize_struct<_>::max))
{
static_assert(std::is_integral_v
< std::decay_t< decltype( enum_range_customize_struct<T>::max) > >);
return enum_range_customize_struct<T>::max;
}
else
{
return MAGIC_ENUM_RANGE_MAX;
}
}
template <typename T>
using range_max = std::integral_constant < std::decay_t<decltype(_range_max<T>())>, _range_max<T>() >;
#if 1 // TESTING
typedef enum e1 {} e1;
typedef struct e1_enum_range
{
static constexpr bool is_flags = true;
static constexpr int min = 5;
static constexpr int max = 55;
} e1_enum_range;
inline constexpr auto magic_enum_enum_range(e1) { return e1_enum_range(); } // adl func
static_assert(has_is_flags<e1>::value);
static_assert(get_is_flags<e1>::value);
static_assert(range_min<e1>::value == 5);
static_assert(range_max<e1>::value == 55);
#endif
Enjoy!
The code above is almost all C++17 but I just realized that HAS_MEMBER_*()
is using C++20 template lambda. This is just for aesthetics though; HAS_MEMBER_*()
could be adapted to pass the type in via an auto
parameter, say container
, and then instead of referring to the type _
from the EXPR
you would have to refer to decltype(container)
from the EXPR. Uglier but I think it would work with C++17.
Hi, thanks so much for magic_enum!
As far as I can tell, the mechanism for turning on
is_flags
or otherenum_range
flags (specializingmagic_enum::customize::enum_range<T>
) only works from global scope (because C++ doesn't let us changemagic_enum::customize::
unless the scope we are in (global) enclosesmagic_enum
).This is quite inconvenient when the enum being customized is deep inside one or more classes and/or namespaces. And it's a little dangerous too, because if we accidentally use the enum with magic_enum before the specialization, magic_enum will do the wrong thing. It's good to have the magic_enum customize code right after where the enum is defined.
ADL is exactly the right tool for this because it can give magic_enum access to whatever scope contains the enum (even if it's deep inside namespaces and classes)
So, I have a simple proposal to add a second, ADL-based mechanism for magic_enum to find
enum_range
parameters.The idea is for magic_enum to also check for the ability to call the function
magic_enum_enum_range(t)
and if a call is possible, use the struct returned by the function in exactly the same way that magic_enum currently usesmagic_enum::customize::enum_range<T>
The function is never actually called at runtime (and it's constexpr) and it's never used in evaluated context, only used insidedecltype()
so it doesn't add any overhead.Here is the code for how to do this to fetch
is_flags
(tested on MSVC19.41 with-std:c++latest
, clang 18.0.2 with-std=c++2c
, gcc 13.1.0 with-std=c++20
)Here is the existing, unmodified code from magic_enum.hpp:
Add one more struct to try the ADL case:
Add this new
get_is_flags
struct to keep the code clean:and then later on where the code used to say:
change it to:
and similar small changes for the other
enum_range
flags likerange_min
andrange_max
Thanks!