Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 556
Paolo M Avatar asked Apr 12 '16 08:04

Paolo M


2 Answers

Here's a solution inspired from T.C.'s solution that worked for my use case:

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;
};

Example Usage:

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;
}

like image 25
Thomas Eding Avatar answered Sep 24 '22 23:09

Thomas Eding


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 */> {};
like image 144
T.C. Avatar answered Sep 25 '22 23:09

T.C.