Neargye / magic_enum

Static reflection for enums (to string, from string, iteration) for modern C++, work with any enum type without any macro or boilerplate code
MIT License
4.99k stars 445 forks source link

please add an ADL version of customize::enum_range<> (code included) #381

Open lsemprini opened 1 month ago

lsemprini commented 1 month ago

Hi, thanks so much for magic_enum!

As far as I can tell, the mechanism for turning on is_flags or other enum_range flags (specializing magic_enum::customize::enum_range<T>) only works from global scope (because C++ doesn't let us change magic_enum::customize:: unless the scope we are in (global) encloses magic_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 uses magic_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 inside decltype() 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:

template <typename T, typename = void>
struct has_is_flags : std::false_type {};

template <typename T>
struct has_is_flags<T, std::void_t<decltype(customize::enum_range<T>::is_flags)>> : std::bool_constant<std::is_same_v<bool, std::decay_t<decltype(customize::enum_range<T>::is_flags)>>> {};

Add one more struct to try the ADL case:

// you must define an ADL-reachable function magic_enum_enum_range(T t):
//
//    typedef enum e1 {} e1;
//    struct e1_enum_range
//    {
//        // here is the same stuff you would put into magic_enum::customize::enum_range<T>
//        inline static 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< decltype(decltype(magic_enum_enum_range(T{}))::is_flags), bool > > > : std::true_type {};

Add this new get_is_flags struct to keep the code clean:

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< decltype(decltype(magic_enum_enum_range(T{}))::is_flags), bool > > > : std::bool_constant< decltype(magic_enum_enum_range(T{}))::is_flags > {};

and then later on where the code used to say:

  } else if constexpr (has_is_flags<E>::value) {
    return customize::enum_range<E>::is_flags ? enum_subtype::flags : enum_subtype::common;

change it to:

  } else if constexpr (has_is_flags<E>::value) {
    return get_is_flags<E>::value ? enum_subtype::flags : enum_subtype::common;

and similar small changes for the other enum_range flags like range_min and range_max

Thanks!

lsemprini commented 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 > {};
lsemprini commented 1 week ago

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!

lsemprini commented 1 week ago

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.