Hatted Rooster
Hatted Rooster

Reputation: 36483

std::conditional vs std::enable_if

I have a hashing function that can take any object type and hash it, it uses std::hash internally. Because std::hash does not support enum types I've created overloads of the function, 1 for enumerations using std::underlying_type and 1 for other types:

template <typename T, typename std::enable_if<std::is_enum<T>::value>::type* = nullptr>
static std::size_t Hash(T const & t)
{
    return std::hash<typename std::underlying_type<T>::type>()(t);
}

template <typename T, typename std::enable_if<!std::is_enum<T>::value>::type* = nullptr>
static std::size_t Hash(T const & t)
{
    return std::hash<T>()(t);
}

This is working fine. I then tried to put it all into a single function with std::conditional:

template <typename T>
static std::size_t Hash(T const & t)
{
    typedef typename std::conditional<std::is_enum<T>::value, std::hash<typename std::underlying_type<T>::type>, std::hash<T>>::type Hasher;
    return Hasher()(t);
}

Main:

enum test
{
    TEST = 2
};

int main() {
    Hash<test>(TEST);
    Hash<int>(5);
    std::cin.get();
    return 0;
}

This however, gave me an error :

/usr/include/c++/5/type_traits:2190:38: error: 'int' is not an enumeration type typedef __underlying_type(_Tp) type;

I do understand the error but I don't understand why, I thought std::conditional would prevent these compile-time errors because <int> uses std::hash<T> instead of std::hash<typename std::underlying_type<T>::type> at compile-time.

What am I doing wrong here, is there a way I can merge the 2 functions?

Upvotes: 16

Views: 4244

Answers (4)

T.C.
T.C.

Reputation: 137320

If you really want to use conditional, you'll need to defer evaluation:

template<class T> struct identity { using type = T; };

using Hasher = std::hash<typename std::conditional<std::is_enum<T>::value,
                                                   std::underlying_type<T>,
                                                   identity<T>>::type::type>;

Also, std::hash should natively support enums since LWG 2148.

Upvotes: 8

Niall
Niall

Reputation: 30605

Both types (branches) used in the conditional need to be well formed when they are evaluated at compile time.

The issue is that when the compiler parses the std::conditional itself, the statement typename std::underlying_type<T>::type needs to be well formed (and it is not for the int). It doesn't really matter what the result is yet, it hasn't got that far.

Upvotes: 2

TartanLlama
TartanLlama

Reputation: 65610

The problem is that std::underlying_type is not well-formed if the template argument is not an enum, and both branches of std::conditional need to be valid.

One possibility would be to make a safe_underlying_type trait which just returns void if T is not an enum:

template <typename T, typename = typename std::is_enum<T>::type>
struct safe_underlying_type {
    using type = void;
};

template <typename T>
struct safe_underlying_type<T, std::true_type> {
    using type = std::underlying_type_t<T>; 
};

Or you could just write a trait to get the hash type you want:

template <typename T, typename = typename std::is_enum<T>::type>
struct hash_type {
    using type = std::hash<T>;
};

template <typename T>
struct hash_type<T, std::true_type> {
    using type = std::hash<std::underlying_type_t<T>>;  
};

Upvotes: 13

Not a real meerkat
Not a real meerkat

Reputation: 5729

typename std::underlying_type<T>::type

This is not well-formed for an int, but it is still evaluated at compile time. That's why you're getting this error. You already gave a solution using std::enable_if, so I'm not go into details there. I think it is fine and using std::conditional would just make it harder to read anyway.

Upvotes: 2

Related Questions