Search code examples
c++enumsvariadic-templatessfinaeoverload-resolution

SFINAE not happening with std::underlying_type


Below SFINAE code with variadic templates compiles nicely using clang 3.7.1, C++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);
}

I want the correct overload of foo to be called recursively, based on the type H:

  1. if std::begin(h) is defined for an h of type H, I want the overload number 1 to be chosen
  2. if H is an "integral type", I want overload number 2.

This works as it is. But if I add another overload for enum types (you can try to un-comment the third overload), then I get:

error: only enumeration types have underlying types

I agree that only enums got an underlying type, hence why is Not the third overload (with std::underlying_type) get SFINAE-d away?


Solution

  • std::underlying_type is not SFINAE friendly. Attempting to access std::underlying_type<T>::type for a non-enumeration type results in undefined behavior (often a hard error), not substitution failure.

    You need to ascertain that the type at issue is an enumeration type first, before attempting to access its underlying type. Writing this in line would be something along the lines of typename std::enable_if<std::is_enum<H>::value, std::underlying_type<H>>::type::type. Replacing the typename std::underlying_type<H>::type in your return type with this hideous mess and you get an even more hideous mess that works :)

    If you find yourself needing to do this often - or just don't want to write 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 - you can write a SFINAE-friendly 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 */> {};