magic_enum icon indicating copy to clipboard operation
magic_enum copied to clipboard

Support converting enum value from non-constexpr to constexpr context

Open andrewkcorcoran opened this issue 2 years ago • 19 comments

Hi,

Is there any way magic_enum could support converting a non constexpr enum value into a constexpr context for all values of a given enum. It's currently possible to do this manually (or automatically if you have access to the enum source and can change the definition to use X Macros) but it would be great if it could be supported natively by the lib.

Motivating example using X Macros: https://godbolt.org/z/rr4n15cEG

andrewkcorcoran avatar Sep 29 '21 10:09 andrewkcorcoran

Unfortunately, I do not yet know how to do this using the library.

Neargye avatar Sep 29 '21 18:09 Neargye

@andrewkcorcoran https://godbolt.org/z/PzYs6861h

Neargye avatar Oct 08 '21 23:10 Neargye

@andrewkcorcoran https://godbolt.org/z/PzYs6861h

Looks good. Unfortunately I don't think there's a way to make the solution generic (e.g. so the user could pass the work function into the switch as a callback) so it could be included as a library helper method.

andrewkcorcoran avatar Oct 10 '21 16:10 andrewkcorcoran

New idea, thanks @grishavanika https://godbolt.org/z/1j4c7W5qr

Neargye avatar Feb 13 '22 18:02 Neargye

Looks good - if we're considering adding this into the library then we should probably using &&, std::forward, and std::invoke - https://godbolt.org/z/Y5K6bfboz

andrewkcorcoran avatar Feb 15 '22 04:02 andrewkcorcoran

It remains to understand how to name this function.

Neargye avatar Feb 15 '22 08:02 Neargye

https://godbolt.org/z/cGMjq3e6W maybe a more general case?

Neargye avatar Feb 15 '22 09:02 Neargye

https://godbolt.org/z/cGMjq3e6W maybe a more general case?

No longer looks to be working correctly? e.g. no output to stdout

andrewkcorcoran avatar Feb 15 '22 09:02 andrewkcorcoran

Fixed https://godbolt.org/z/v39rEeK4P, my bad

Neargye avatar Feb 15 '22 10:02 Neargye

I would add maybe two options(for_each & const_switch), but we should come up with concise names

Neargye avatar Feb 15 '22 10:02 Neargye

I would add maybe two options(for_each & const_switch), but we should come up with concise names

They both seem reasonable

andrewkcorcoran avatar Feb 15 '22 10:02 andrewkcorcoran

note for constexpr switch: GCC cannot able to optimize constexpr for like clang, It generates if (check) for every element, which is not that efficient like pure switch.

I solved this with a lookup table:

template<class Lambda, auto constVal>
void function_caller(Lambda&& lambda) {
     lambda(std::integral_constant<decltype(constVal), constVal>{});
}


template<class Lambda, class EnumType, EnumType ... allValue>
void constexpr_switch_impl(Lambda&& lambda, EnumType searched) {

#ifdef gcc

    using FunPtr = void (*const)(Lambda&&);
    constexpr /*static*/ FunPtr lookup_table[] = {&function_caller<Lambda, allValue>...};
    lookup_table[*magic_enum::enum_index(searched)](std::forward<Lambda>(lambda));

#else

        (void) ((searched == allValue && (function_caller<Lambda, allValue>(std::forward<Lambda>(lambda)), true)) || ...);

#endif
}

This gcc solution has only one caveat: it has a runtime function pointer indirection (but constant time).

(it can be made index-safe)

schaumb avatar Feb 16 '22 15:02 schaumb

Another "efficient" (platform + compiler independent) constexpr switch solution can be like boost::lambda::switch. It generates predefined switch function with variadic parameters (only support maximum 9 case value with default, but this can be a #define parameter on a different boostless solution). With default chaining it can be reached efficient code like:

switch (val):
case 0: lambda0(); break;
case 1: lambda1(); break;
case 2: lambda2(); break;
case 3: lambda3(); break;
case 4: lambda4(); break;
case 5: lambda5(); break;
case 6: lambda6(); break;
case 7: lambda7(); break;
case 8: lambda8(); break;
default:
    switch(val) {
        case 9: lambda9(); break;
        case 10: lambda10(); break;
        case 11: lambda11(); break;
        case 12: lambda12(); break;
        case 13: lambda13(); break;
        case 14: lambda14(); break;
        case 15: lambda15(); break;
        case 16: lambda16(); break;
        case 17: lambda17(); break;
        default:
            ...
    }
}

(which is 9 times "faster" than original constexpr switch on gcc)

schaumb avatar Feb 16 '22 15:02 schaumb

@schaumb wow, good catch! To be honest, I need to look again at how compilers deal with the library, especially with compilation time, but this is a separate issue.

Neargye avatar Feb 16 '22 17:02 Neargye

After this PR merge https://github.com/Neargye/magic_enum/pull/140 I start to implement the following:

enum_constexpr_switch(lambda, Enum::value) -> call lambda, no result. enum_constexpr_switch<ResultType>(lambda, Enum::value) -> call lambda + result. If no enum, return default constructed ResultType enum_constexpr_switch(lambda, Enum::value, default_val) -> call lambda + result. If no enum, return default_val enum_constexpr_switch<Enum>(lambda, "value") -> call lambda if exists, no result enum_constexpr_switch<Enum, ResultType>(lambda, "value") -> call lambda if exists + result. enum_constexpr_switch<Enum>(lambda, "value", default_val) -> call lambda if exists + result.

all function are constant O(1) time.

lambda can be:

  • [](auto arg) {} . arg is std::integral_constant<Enum, Enum::value>{}. only if Enum::value is valid. Constexpr time can be get the Enum::value.
  • [](Enum val) {}. calls only if Enum::value is valid
  • [](auto arg) -> std::enable_if_t<Enum::value1 == decltype(arg)() || Enum::value2 == decltype(arg)()> {}. only calls if value1 or value2
struct Functor {
  constexpr void operator()(std::integral_constant<Enum, Enum::value1>) const {
      // value1
  }
  constexpr void operator()(std::integral_constant<Enum, Enum::value2>) const {
      // value2
  }

  // not necessarily need default, but can be:  
  template<Enum val>
  constexpr void operator()(std::integral_constant<Enum, val>) const {
      // default
  }
  // OR 
  constexpr void operator()(Enum) const {
      // default 2
  }
};

schaumb avatar Feb 19 '22 01:02 schaumb

So I refactored the constexpr switch, which has string_view switch-cases for the enum names.

After that, only need the public functions

The code is functionally done. It needs review, maybe tests.

schaumb avatar Mar 10 '22 16:03 schaumb

I made some testcase from the original motivating example:

template<Color C>
constexpr std::string_view DoWork() {
  return "default";
}

template<>
constexpr std::string_view DoWork<Color::GREEN>() {
  return "override";
}

TEST_CASE("enum_switch") {
  constexpr auto bind_enum_switch = [] (Color c) { // helper to bind enum_switch parameters

    return enum_switch([](auto val) {     // the main part: it is enough to pass a lambda, with auto parameter
      return DoWork<val>();               // then this can be passed as template argument
    }, c, string_view{"unrecognized"});   // return type deduced from "default" parameter

  };

  constexpr auto def = bind_enum_switch(Color::BLUE);
  REQUIRE(def == "default");
  REQUIRE(bind_enum_switch(Color::RED) == "default");
  REQUIRE(bind_enum_switch(Color::GREEN) == "override");
  REQUIRE(bind_enum_switch(static_cast<Color>(0)) == "unrecognized");
}

I created the underlying_type enum_switch overloads too. And I implemented an enum_for_each function, with testcases:

TEST_CASE("enum_for_each") {
  SECTION("no return type") {
    underlying_type_t<Color> sum{};
    enum_for_each<Color>([&sum](auto val) {
      constexpr underlying_type_t<Color> v = enum_integer(val());
      sum += v;
    });
    REQUIRE(sum == 10);
  }

  SECTION("same return type") {
    constexpr auto workResults = enum_for_each<Color>([](auto val) {
      return DoWork<val>();
    });
    REQUIRE(workResults == std::array<std::string_view, 3>{"default", "override", "default"});
  }

  SECTION("different return type") {
    constexpr auto colorInts = enum_for_each<Color>([](auto val) {
      return val;
    });
    REQUIRE(std::is_same_v<std::remove_const_t<decltype(colorInts)>, std::tuple<
      std::integral_constant<Color, Color::RED>,
      std::integral_constant<Color, Color::GREEN>,
      std::integral_constant<Color, Color::BLUE>
    >>);
  }
}

schaumb avatar Mar 10 '22 21:03 schaumb

So we are waiting for the PR

I found another boost:: solution for the constexpr switch.

It generates 1..16 the switch cases, and for bigger intervals does a logarithmic search with runtime halving. the code is here.

It can adapt to this library. For the optimal run, the current solution needs to extend:

  • ~~unreachable macro~~ (not usable on enum existence check)
#if defined( __GNUC__ ) || defined( __clang__ )
# define MAGIC_ENUM_UNREACHABLE_CASE __builtin_unreachable();
#elif defined( _MSC_VER )
# define MAGIC_ENUM_UNREACHABLE_CASE __assume(false);
#else
# define MAGIC_ENUM_UNREACHABLE_CASE
#endif
  • for bigger enums (more than 256 elements) run a std::lower_bound on the first elements of all "256 pages" (only appliable for the sorted values).

  • constexpr time sorting the enum name hashes. This is tricky because the index of the enum got from the index of the enum name hash index.

  • change "fill" strategy (the unused elements) with not after the last one, but with intermediate elements

  • optimization for enum_flags with log2 hash.

My solution probably is not the "perfect" one, so it needs some performance tests:

  • measure the 256 switch-case implementation runtime overhead (only on sparse enums). (enum existence and get by name):
    • sorted enum values with one special element
    • randomized enum values
    • big enums

I made some experiment on different compilers with different switch-case, with the "O2" optimization level. I only check the generated assembly code.

I found that:

  • GCC works well with MAX-MIN = 47 constant. Other compilers create a bigger hash table. So probably it is enough to make a 32-way switch for smaller enums.
  • For the bigger switch-cases, it is possible to compilers generate not constant (lookup table) switch code.
  • MAGIC_ENUM_UNREACHABLE_CASE works well on GCC and msvc, but not on clang.

schaumb avatar Mar 17 '22 17:03 schaumb

Looks like it can be closed?

Neargye avatar Jun 28 '22 11:06 Neargye