23

I would like to make a type trait for checking if a particular type is hashable using the default instantiations of the standard library's unordered containers, thus if it has a valid specialization for std::hash. I think this would be a very useful feature (e.g. for using std::set as failsafe for std::unordered_set in generic code). So I, thinking std::hash is not defined for each type, started making the following SFINAE solution:

template<typename T> std::true_type hashable_helper(
    const T&, const typename std::hash<T>::argument_type* = nullptr);

template<typename T> std::false_type hashable_helper(...);

//It won't let me derive from decltype directly, why?
template<typename T> struct is_hashable 
    : std::is_same<decltype(hashable_helper<T>(std::declval<T>())),
                   std::true_type> {};

(Forgive my modest SFINAE-abilities if this is not the best solution or even wrong.)

But then I learned, that both gcc 4.7 and VC++ 2012 define std::hash for any type T, just static_asserting in the non-specialized version. But instead of compiling conditionally they (and also clang 3.1 using gcc 4.7's libstdc++) fail the assertion resulting in a compile error. This seems reasonable since I think static_asserts are not handled by SFINAE (right?), so an SFINAE solution seems not possibly at all. It's even worse for gcc 4.6 which doesn't even have a static_assert in the general std::hash template but just doesn't define its () operator, resulting in a linker error when trying to use it (which is always worse than a compile error and I cannot imagine any way to transform a linker error into a compiler error).

So is there any standard-conformant and portable way to define such a type trait returning if a type has a valid std::hash specialization, or maybe at least for the libraries static_asserting in the general template (somehow transforming the static_assert error into a SFINAE non-error)?

Christian Rau
  • 45,360
  • 10
  • 108
  • 185
  • EDIT: Ok, my statement about *VC++* compiling it was from some different older version, in fact *VC++* behaves like *gcc*, choking on the `static_assert` – Christian Rau Oct 05 '12 at 21:17
  • looks like the GCC people are aware of the problem meanwhile. It is said that Gcc 4.8 doesn't have this static assert anymore, but that they consider to roll a standard implementation in future, somewhat similar to Boost hash, where the implementation is picktd up by ADL. – Ichthyo Aug 09 '14 at 14:58
  • https://gcc.gnu.org/ml/libstdc++/2013-03/msg00029.html – Ichthyo Aug 09 '14 at 14:59
  • On a side note, I've logged a bug for this to request standard integration in boost: https://github.com/boostorg/container_hash/issues/4 – JVApen Jan 20 '19 at 11:15

4 Answers4

12

Since C++17 it is now possible to do this in a more elegant way. From cppreference about std::hash:

Each specialization of this template is either enabled ("untainted") or disabled ("poisoned"). For every type Key for which neither the library nor the user provides an enabled specialization std::hash, that specialization exists and is disabled. Disabled specializations do not satisfy Hash, do not satisfy FunctionObject, and std::is_default_constructible_v, std::is_copy_constructible_v, std::is_move_constructible_v, std::is_copy_assignable_v, std::is_move_assignable_v are all false. In other words, they exist, but cannot be used.

This meant that the STL had to remove the static_assert in C++17. Here is a working solution with 'Clang-6.0.0 -std=c++17':

#include <functional>
#include <ios>
#include <iostream>
#include <type_traits>

template <typename T, typename = std::void_t<>>
struct is_std_hashable : std::false_type { };

template <typename T>
struct is_std_hashable<T, std::void_t<decltype(std::declval<std::hash<T>>()(std::declval<T>()))>> : std::true_type { };

template <typename T>
constexpr bool is_std_hashable_v = is_std_hashable<T>::value; 

struct NotHashable {};

int main()
{
    std::cout << std::boolalpha;
    std::cout << is_std_hashable_v<int> << std::endl;
    std::cout << is_std_hashable_v<NotHashable> << std::endl;
    return 0;
}

This might for example come in handy when you use boost::hash_combine or boost::hash_range. If you include a header containing the following code sample you do not need to define boost hashes for specific types anymore.

#include <boost/functional/hash_fwd.hpp>

template <typename T, typename = std::void_t<>>
struct is_boost_hashable : std::false_type { };

template <typename T>
struct is_boost_hashable<T, std::void_t<decltype(boost::hash_value(std::declval<T>()))>> : std::true_type { };

template <typename T>
constexpr bool is_boost_hashable_v = is_boost_hashable<T>::value;  

namespace boost
{
    template <typename T>
    auto hash_value(const T &arg) -> std::enable_if_t<is_std_hashable_v<T> &&
                                                      !is_boost_hashable_v<T>, std::size_t>
    {
        return std::hash<T>{}(arg);
    }
}

Notice the is_boost_hashable_v, this is necessary to avoid ambiguity as boost already provides hashes for a lot of hashes.

The Shmoo
  • 358
  • 1
  • 16
  • This answer looks ok, however, when using it, I get some ADL issues where this function doesn't appear to be used. – JVApen Dec 08 '18 at 23:23
8

It seems we have two conflicting requirements:

  1. SFINAE is meant to avoid any instantiation of a template if the instantiation might fail and remove the corresponding function from the overload set.
  2. static_assert() is meant to create an error, e.g., during instantiation of a template.

To my mind, 1. clearly trumps 2., i.e., your SFINAE should work. From the looks of two separate compiler vendors disagree, unfortunately not between themselves but with me. The standard doesn't seem to specify how the default definition of std::hash<T> looks like and seems to impose constraints only for the cases where std::hash<T> is specialized for a type T.

I think your proposed type traits is a reasonable idea and it should be supported. However, it seems the standard doesn't guarantee that it can be implemented. It may be worth bringing this up with the compiler vendors and/or filing a defect report for the standard: The current specification doesn't give clear guidance what should happen, as far as I can tell. ... and if the specification currently mandates that a type traits as above fails it may be a design error which needs to be corrected.

sehe
  • 374,641
  • 47
  • 450
  • 633
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • 5
    _From the looks of two separate compiler vendors disagree, unfortunately not between themselves but with me_ - lol – sehe Oct 05 '12 at 22:06
  • To your mind 1. might trump 2., but the standard disagrees, unfortunately. Compilers are thus completely correct to reject this. – fgp Oct 06 '12 at 11:11
  • @fgp Could you provide some quotes from the standard proving this disagreement? – Christian Rau Oct 07 '12 at 00:13
  • @ChristianRau The `static_assert` goes into the *body* of the template specialization and not to its declaration, so it's hard to trigger SFINAE with it, since it does not even have to be in the same translation unit. Though I'm still with you that it should somehow have been possible to trigger SFINAE with a `static_assert`. – enobayram Oct 07 '12 at 03:55
  • On second thought, an `enable_if` can be used for any condition that a `static_assert` can be. In a way, it provides extended functionality by trapping the template specialization resolution. So, the "mistake" here is to use the `static_assert` in the body of `hash`. – enobayram Oct 07 '12 at 04:25
4

Here is a VERY dirty solution to your problem: It works for GCC 4.7 (and not 4.6, due to missing C++11 feature: mangling overload)

// is_hashable.h
namespace std {
    template <class T>
    struct hash {
        typedef int not_hashable;
    };

}

#define hash hash_
#define _Hash_impl _Hash_impl_
#include<functional>
#undef hash
#undef _Hash_impl

namespace std {
    struct _Hash_impl: public std::_Hash_impl_{
        template <typename... Args>
            static auto hash(Args&&... args) 
                -> decltype(hash_(std::forward<Args>(args)...)) {
             return hash_(std::forward<Args>(args)...);
        }
    };
    template<> struct hash<bool>: public hash_<bool> {};
    // do this exhaustively for all the hashed standard types listed in:
    // http://en.cppreference.com/w/cpp/utility/hash
}

template <typename T>
class is_hashable
{
    typedef char one;
    typedef long two;

    template <typename C> static one test( typename std::hash<C>::not_hashable ) ;
    template <typename C> static two test(...);


public:
    enum { value = sizeof(test<T>(0)) == sizeof(long) };
};


// main.cpp
// #include "is_hashable.h"
#include<iostream>
#include<unordered_set>

class C {};

class D {
public:
    bool operator== (const D & other) const {return true;}
};

namespace std {
    template <> struct hash<D> {
        size_t operator()(const D & d) const { return 0;}
    };
}

int main() {
    std::unordered_set<bool> boolset; 
    boolset.insert(true);
    std::unordered_set<D> dset; 
    dset.insert(D());// so the hash table functions
    std::cout<<is_hashable<bool>::value<<", ";
    std::cout<<is_hashable<C>::value << ", ";
    std::cout<<is_hashable<D>::value << "\n";
}

And the output is:

1, 0, 1

We basically "hijack" the hash symbol and inject some helper typedef in it. You'll need to modify it for VC++, in particular, the fix for _Hash_impl::hash() since it's an implementation detail.

If you make sure that the section labelled as is_hashable.h is included as the first include this dirty trick should work...

enobayram
  • 4,650
  • 23
  • 36
0

I hit this too. I tried a few workarounds and went with a whitelist filter for std::hash<>. the whitelist is not pleasant to maintain, but it is safe and it works.

I tried this on VS 2013, 2015, clang and gcc.

#include <iostream>
#include <type_traits>

// based on Walter Brown's void_t proposal
// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3911.pdf
namespace detail {
    template<class... TN> struct void_t {typedef void type;};
}
template<class... TN>
struct void_t {typedef typename detail::void_t<TN...>::type type;};

// extensible whitelist for std::hash<>
template <class T, typename = void> 
struct filtered_hash;
template <class T> 
struct filtered_hash<T, 
    typename std::enable_if<std::is_enum<T>::value>::type> 
    : std::hash<T> {
};
template <class T> 
struct filtered_hash<T, 
    typename std::enable_if<std::is_integral<T>::value>::type> 
    : std::hash<T> {
};
template <class T> 
struct filtered_hash<T, 
    typename std::enable_if<std::is_pointer<T>::value>::type> 
    : std::hash<T> {
};

template<typename, typename = void>
struct is_hashable
    : std::false_type {};

template<typename T>
struct is_hashable<T, 
    typename void_t<
        typename filtered_hash<T>::result_type, 
        typename filtered_hash<T>::argument_type, 
        typename std::result_of<filtered_hash<T>(T)>::type>::type>
    : std::true_type {};

// try it out..
struct NotHashable {};

static_assert(is_hashable<int>::value, "int not hashable?!");
static_assert(!is_hashable<NotHashable>::value, "NotHashable hashable?!");

int main()
{
    std::cout << "Hello, world!\n";
}
Kirk Shoop
  • 1,274
  • 9
  • 13
  • But why the complicated workaround with `filtered_hash` in the first place if you need to list all the specializations anyway. Why not just specialize `is_hashable` for all the types then? – Christian Rau Feb 04 '16 at 11:10
  • It provides flexibility in usage. I can use filtered_hash in place of std::hash or use is_hashable to protect std::hash usage. – Kirk Shoop Feb 04 '16 at 14:34