C++ std::conditional_t wont compile with std::is_enum_v and std::underlying_type_t

I am trying to wite type traits for types that can index into e.g. an std::vector, which should include enum types because i can cast them to their underlying type.

I have written following traits so far.

#include <iostream>
#include <type_traits>
#include <cinttypes>
#include <vector>
#include <utility>

template<typename T>
struct is_unsigned_integral :
    std::integral_constant<
        bool,
        std::is_integral<T>::value &&
        std::is_unsigned<T>::value
    > {};

template<typename T>
inline constexpr auto is_unsigned_integral_v = 
    is_unsigned_integral<T>::value;

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

template<typename T>
struct is_index<
    T, 
    std::enable_if_t<
        is_unsigned_integral_v<
            std::conditional_t<
                std::is_enum_v<T>,
                std::underlying_type_t<T>,
                T
            >
        >
    >
> : std::true_type {};

template<typename T>
inline constexpr auto is_index_v = is_index<T>::value;

enum class idx : unsigned int {};

int main() {
    static_assert(is_index_v<unsigned int>, "");
    static_assert(is_index_v<idx>, "");
    
    return 0;
}

But im getting following error message

type_traits:2009:15: error: 
      only enumeration types have underlying types
      typedef __underlying_type(_Tp) type;

I would expect the following

std::conditional_t<
    std::is_enum_v<T>,
    std::underlying_type_t<T>,
    T
>

to evaluate eighter to T or to underlying type is T is an enum.

How would i make this work?

Answer

Substitution fails because there is no std::underlying_type_t<unsigned int>.

You can specialize seperately:

#include <iostream>
#include <type_traits>
#include <cinttypes>
#include <vector>
#include <utility>

template<typename T>
struct is_unsigned_integral :
    std::integral_constant<
        bool,
        std::is_integral<T>::value &&
        std::is_unsigned<T>::value
    > {};

template<typename T>
inline constexpr auto is_unsigned_integral_v = 
    is_unsigned_integral<T>::value;

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

template<typename T>
struct is_index<
    T, 
    std::enable_if_t< is_unsigned_integral_v<T> >    
> : std::true_type {};

template <typename T>
struct is_index<T,std::enable_if_t<std::is_enum_v<T> >> : is_index<std::underlying_type_t<T> > {};


template<typename T>
inline constexpr auto is_index_v = is_index<T>::value;

enum class idx : unsigned int {};

int main() {
    static_assert(is_index_v<unsigned int>, "");
    static_assert(is_index_v<idx>, "");
    
    return 0;
}

PS: From cppreference/std::underlying_type

If T is a complete enumeration (enum) type, provides a member typedef type that names the underlying type of T.

Otherwise, the behavior is undefined. (until C++20)

Otherwise, if T is not an enumeration type, there is no member type. Otherwise (T is an incomplete enumeration type), the program is ill-formed. (since C++20)

I have to admit that I am not sure how that undefined beahvior (before C++20) plays with SFINAE in your example. Though, note that it is not an issue in the above, because it only uses std::underlying_type_t<T> when T is actually an enum type.