Can I cast a number into an enum, safely, with a fallback value?

214 views Asked by At

I would like to write something like:

template <typename E, E fallback>
E safe_cast_to_enum(std::underlying_type_t<E> e);

which, for an enum class (or just an enum?) E, casts e to a corresponding value of E, if such a value exists, and to value fallback otherwise.

Can I do this generically?

Notes:

  • I suspect the answer is negative until at least C++20, but regardless - please (also) answer using the earliest C++ version with which this is possible.
  • If you like, you may assume fallback is a 'valid' value of the enum, i.e. it has an associated named identifier.
  • If you can offer a solution based on somewhat stronger assumptions (underlying type size, nature of the set of values covered by the enum identifiers, known last value etc.), but which is still a general template rather than a different per-enum implementation - that is a useful, albeit imperfect, answer.

See also:

2

There are 2 answers

4
Sebastian Redl On

As of C++23, no standard mechanism for obtaining a list of valid values for an enum exists.

However, it is possible to use some incredibly janky compiler-specific code to extract this information.

The magic_enum library implements this code for the mainstream compilers.

3
Red.Wave On

Let's make a function that finds enum names:

template<auto value>
requires std::is_enum_v<std::decay_t<decltype(value)>>
consteval std::string_view enum_name() {
    constexpr static auto location=std::source_location::current();
    constexpr static std::string_view full_name= location.function_name();
    constexpr static auto closeb = full_name.rfind('>');
    constexpr static auto openb = full_name.find("<", 0, closeb);
    return full_name.substr(openb, closeb - openb);
};

Next we must make sure the result of above function is not a conversion expression. This part is greatly platform dependent:

constexpr bool is_enum_name(std::string_view name){
    if (auto brace = name.find_last_of("})"); brace==name.npos)
        return true;
    else
        return name.find(':',brace)!=name.npos;
};

Conversion expression is impossible without braces. If after the last closing brace, there's a scope operator(::), then it must be part of the enum class name qualifier; otherwise a conversion has happened. Next we need to put the names and validators in a constexpr array. We can do different things now; I choose to store the raw values:

template<auto value>
requires std::is_enum_v<std::decay_t<decltype(value)>>
using enum_limits =
      std::numeric_limits<
      std::underlying_type_t<
      std::decay_t< decltype(value)
>>>;
template<typename enm>
requires std::is_enum_v<enm>
constexpr auto enum_strings = []<std::size_t ...i>{
    return std::array{
           enum_name< enm{ static_cast<
           std::underlying_type_t<enm>>(i)
}>()...};
}( std::make_index_sequence
 < std::min(1<<14
 , std::size_t
 { std::enum_limits<enm{}>::max()
 - std::enum_limits<enm{}>::min()
})>);

Now you can use the above LUT to your liking. It is possible to create a validator array of bool as well, but I leave that one for now. It is also possible to use std::optional or store empty string_view for invalid values.

template<auto value>
requires std::is_enum_v<std::decay_t<decltype(value)>>
constexpr decltype(value) 
to_enum( std::underlying_type_t
       < std::decay_t
       < decltype(value)>> num){
   if (std::bit_cast<std::size_t>(num)
   auto idx = static_cast<std::size_t>(num) 
            - enum_limits<value>::min();
   return is_enum_name
        ( enum_string
        < std::decay_t
        < decltype(value)
        >>[idx] )
        ? decltype(value){num}
        : value;
};

I have not run a test on this code; so there are probably lots of bugs and errors. But that happens when a header only library is composed as an answer. But it can give ideas on how some basic metadata about enums can be reflected. This is also pretty ugly and needs a lot of refactoring for readability.