问题
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_assert
ing 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_assert
s 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_assert
ing in the general template (somehow transforming the static_assert
error into a SFINAE non-error)?
回答1:
It seems we have two conflicting requirements:
- SFINAE is meant to avoid any instantiation of a template if the instantiation might fail and remove the corresponding function from the overload set.
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.
回答2:
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...
回答3:
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_hashable_v<int> << std::endl;
std::cout << is_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.
回答4:
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";
}
来源:https://stackoverflow.com/questions/12753997/check-if-type-is-hashable