SFINAE не происходит с std:: basic_type

Ниже код SFINAE с вариативными шаблонами компилируется с использованием clang 3.7.1, С++ 14:

#include <array>
#include <iostream>
#include <vector>
#include <cstdint>

enum class Bar : uint8_t {
    ay, bee, see
};

struct S {

static void foo() {}

// std::begin(h) is defined for h of type H
template<typename H, typename... T>
static typename std::enable_if<std::is_pointer<decltype(std::begin(std::declval<H>()))*>::value>::type 
foo(const H&, T&&... t) 
{ std::cout << "container\n"; foo(std::forward<T>(t)...); }

// H is integral
template<typename H, typename... T>
static typename std::enable_if<std::is_integral<typename std::remove_reference<H>::type>::value>::type 
foo(const H&, T&&... t) 
{ std::cout << "integer\n"; foo(std::forward<T>(t)...); }

// H is an enum with underlying type = uint8_t
/*
template<typename H, typename... T>
static typename std::enable_if<std::is_same<typename std::underlying_type<H>::type,uint8_t>::value>::type 
foo(const H&, T&&... t)
{ std::cout << "enum\n"; foo(std::forward<T>(t)...); }
*/
};


int main()
{
    S::foo(std::array<int,8>(), 5, 5L, std::vector<int>{}, 5L);
}

Я хочу, чтобы правильная перегрузка foo вызывалась рекурсивно, в зависимости от типа H:

  • Если std::begin(h) определено для H типа H, я хочу, чтобы перегрузка № 1, которая будет выбрана
  • Если H является "интегральным типом", я хочу перегрузить номер 2.

Это работает так, как есть. Но если я добавлю еще одну перегрузку для типов перечисления (вы можете попытаться отменить третью перегрузку), я получаю:

ошибка: только типы перечисления имеют базовые типы

Я согласен, что только перечисления получили базовый тип, поэтому почему не третья перегрузка (с std::underlying_type) не получает SFINAE-d?

Ответ 1

std::underlying_type не относится к SFINAE. Попытка доступа к std::underlying_type<T>::type для типа без перечисления приводит к поведению undefined (часто жесткая ошибка), а не к отказу замены.

Вам нужно убедиться, что тип проблемы является первым типом перечисления, прежде чем пытаться получить доступ к его базовому типу. Написание этого в строке будет чем-то вроде строк typename std::enable_if<std::is_enum<H>::value, std::underlying_type<H>>::type::type. Заменяя typename std::underlying_type<H>::type в вашем обратном типе с этим отвратительным беспорядком, и вы получаете еще более отвратительный беспорядок, который работает:)

Если вам нужно часто это делать - или просто не хотите писать typename std::enable_if<std::is_same<typename std::enable_if<std::is_enum<H>::value, std::underlying_type<H>>::type::type, uint8_t>::value>::type - вы можете написать SFINAE-дружественный underlying_type:

template<class T, bool = std::is_enum<T>::value>
struct safe_underlying_type : std::underlying_type<T> {};
template<class T>
struct safe_underlying_type<T, false /* is_enum */> {};

Ответ 2

Вот решение, вдохновленное решением TC, которое сработало для моего варианта использования:

template <typename T, bool = std::is_enum<T>::value>
struct relaxed_underlying_type {
    using type = typename std::underlying_type<T>::type;
};

template <typename T>
struct relaxed_underlying_type<T, false> {
    using type = T;
};

Пример использования:

template <typename T>
struct UnwrapEnum {
    using type =
        typename std::conditional<
        std::is_enum<T>::value,
        typename relaxed_underlying_type<T>::type,
        T>
        ::type;
};

enum class MyEnum : int {};

class MyClass {};

int main() {
    UnwrapEnum<MyEnum>::type x;
    static_assert(std::is_same<decltype(x), int>::value);

    UnwrapEnum<MyClass>::type y;
    static_assert(std::is_same<decltype(y), MyClass>::value);

    return 0;
}