magic_enum
magic_enum copied to clipboard
Support converting enum value from non-constexpr to constexpr context
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
Unfortunately, I do not yet know how to do this using the library.
@andrewkcorcoran https://godbolt.org/z/PzYs6861h
@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.
New idea, thanks @grishavanika https://godbolt.org/z/1j4c7W5qr
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
It remains to understand how to name this function.
https://godbolt.org/z/cGMjq3e6W maybe a more general case?
https://godbolt.org/z/cGMjq3e6W maybe a more general case?
No longer looks to be working correctly? e.g. no output to stdout
Fixed https://godbolt.org/z/v39rEeK4P, my bad
I would add maybe two options(for_each & const_switch), but we should come up with concise names
I would add maybe two options(for_each & const_switch), but we should come up with concise names
They both seem reasonable
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)
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 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.
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 isstd::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
}
};
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.
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>
>>);
}
}
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
withlog2
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.
Looks like it can be closed?