SFINAE not happening with std::underlying_type
Asked Answered
B

2

16

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?

Bots answered 12/4, 2016 at 8:48 Comment(2)
Why don't you simply use std::enable_if<std::is_enum<H>::value>::type? It compiles fine. Are you specific for enums with underlying types of uint8_t only? In that case, you can add 1 more condition of sizeof(H) == sizeof(uint8_t). i.e.. std::is_enum<H>::value && (sizeof(H) == sizeof(uint8_t)). That's covered in the ideone example above.Llanes
@iammilind: good advice, thanksBots
B
20

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 */> {};
Beryl answered 12/4, 2016 at 9:7 Comment(1)
Do you happen to know why std::underlying_type is implemented as not SFINAE friendly? What is gained from implementing type traits in such a way?Tappet
D
1

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

Dentelle answered 30/1, 2019 at 17:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.