Question

Is it possible to detect whether an enum has no fixed underlying type?

I have a template function that operates on a generic type T as input. I statically assert that T is an enum.

However, I want to constrain it further so that C-style enums (i.e. without fixed (explicit) underlying type) cannot be passed. Otherwise, the implementation risks invoking Undefined Behavior due to this (emphasis mine):

A value of integer or enumeration type can be converted to any complete enumeration type. If the underlying type is not fixed, the behavior is undefined if the value of expression is out of range (the range is all values possible for the smallest bit-field large enough to hold all enumerators of the target enumeration).

Is it possible to achieve this? If so, how?

 3  154  3
1 Jan 1970

Solution

 1

This can SFINAE away if the underlying type can list-initialize the enum:

template <typename T,
          typename L = std::numeric_limits<std::underlying_type_t<T>>,
          T min = T{L::min()}, T max = T{L::max()}>
consteval void check_enum() {}

This is sufficient to capture enums with a fixed underlying type.

Replacing the list-initialization with a static_cast in theory ought to allow enums without a fixed underlying type but sufficient range (a very unlikely case to be sure). However, for gcc this is bug #95701.

This can check it as a concept:

template <typename T>
concept full_range_enum = std::is_enum_v<T> && requires {
    { check_enum<T>() };
};

See:

enum A { a, b, c };
enum class B { a, b, c };
static_assert(!full_range_enum<A>);
static_assert(full_range_enum<B>);

godbolt

See also std::is_scoped_enum

2024-06-27
Jeff Garrett

Solution

 0

Inspired by the answer from Jeff Garret, I want to also post a slightly simpler solution, that also works on C++17 instead of requiring C++20 concepts. It also does not rely on UB being detected. It simply relies on the fact that only scoped enums or enums with fixed underlying type may be list-initialized.

It currently doesn't check that T is an enum but it would be trivial to add, the important part is distinguishing types of enums.

template <typename T, typename = void >
struct is_c_enum : std::true_type
{};

template <typename T>
struct is_c_enum<T, std::void_t<decltype(T{0})>> : std::false_type
{};

Godbolt

2024-06-30
user1011113