Yet another implementation
First, some boilerplate. type_sink
and TypeSink
let us evaluate types, and discard them.
template<typename... T> struct type_sink {typedef void type;};
template<typename... T> using TypeSink = typename type_sink<T>::type;
We then write a really simple has_hash
that uses SFINAE. The default choice is "no hash", and the specialization is valid iff std::hash<T>()( t )
is a valid expression for a variable t
of type T
:
template<typename T, typename=void> struct has_hash:std::false_type {};
template<typename T> struct has_hash<
T, TypeSink< decltype( std::hash<T>()( std::declval<T&>() ) ) >
>: std::true_type {};
we then write our universal hasher:
template<typename T>
struct Hasher
{
private:
typedef has_hash< typename std::decay<T>::type > test_for_hash;
public:
std::size_t operator()( T t ) const {
return hash( std::forward<T>(t), test_for_hash() );
}
private:
std::size_t hash( T t, std::true_type has_hash ) const {
return std::hash<T>()( std::forward<T>(t) );
}
std::size_t hash( T t, std::false_type no_hash ) const {
// TODO: static_assert that t.to_string exists, and if not give a useful error message
return std::hash<std::string>()( std::forward<T>(t).to_string() )
}
};
where we use the has_hash
trait to do tagged dispatching to either the "use hash
directly" or "to_string
then hash
".
We can do more layers of this dispatching.
I took the liberty of making it somewhat move-aware, in that T
can be T&
or T const&
or T
and it behaves reasonably. (I don't know about T&&
off the top of my head).
As noted in other comments, some work to generate better error messages can be done. We want the compiler's error message to complain about the lack of a hash<T>
implementation rather than a to_string
implementation probably. That would involve writing a has_to_string
traits class and either doing another layer of tag dispatching, or doing a static_assert
to generate a useful message telling the end user to implement either the hash<T>
specialization or T::to_string
.
Another option would be to make a universal hasher:
template<typename T>
struct Hasher
{
private:
typedef has_hash< T > test_for_hash;
public:
template<typename U>
std::size_t operator()( U&& u ) const {
return hash( std::forward<U>(u), test_for_hash() );
}
private:
template<typename U>
std::size_t hash( U&& u, std::true_type has_hash ) const {
return std::hash<T>()( std::forward<U>(u) ); // conversion occurs here
}
template<typename U>
std::size_t hash( U&& u, std::false_type no_hash ) const {
// TODO: static_assert that t.to_string exists, and if not give a useful error message
T t = std::forward<U>(u); // conversion occurs here -- note, implicit on purpose!
return std::hash<std::string>()( std::move(t).to_string() )
}
};
which defers transformation-to-T
until the last moment.