4

I am trying to specialize std::hash for derved classes. The best approach so far is based on this answer:

#include <type_traits>
#include <functional>
#include <unordered_set>

namespace foo
{
    template<class T, class E>
    using first = T;

    struct hashable {};
    struct bar : public hashable {};
}

namespace std
{
    template <typename T>
    struct hash<foo::first<T, std::enable_if_t<std::is_base_of<foo::hashable, T>::value>>>
    {
        size_t operator()(const T& x) const { return 13; }
    };
}

int main() {
    std::unordered_set<foo::bar> baz;
    return 0;
}

This compiles with g++ 5.2.0 with no warnings (-Wall -pedantic), but with clang++ 3.7.0 it results in the following error:

first.cpp:17:12: error: class template partial specialization does not specialize any template argument; to define the primary template, remove the template argument list
    struct hash<foo::first<T, std::enable_if_t<std::is_base_of<foo::hashable, T>::value>>>
           ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Is this a compiler error or an error in the code?

This question, proposes a SFINAE solution that technically works with both my gcc and clang versions. However, because it only disabled the operator, not the class, It starts to yield very confusing error messages when one tries to hash any non-hashable class:

template <typename T>
struct hash
{
    typename std::enable_if_t<std::is_base_of<foo::hashable, T>::value, std::size_t>
    operator()(const T& x) const { return 13; }
};
...
struct fail {};
std::unordered_set<fail> bay;
...
type_traits:2388:44: error: no type named 'type' in 'std::enable_if<false, unsigned long>';
  'enable_if' cannot be used to disable this declaration

I would like to not consider the macro solution. I have further tried the following approaches:

template <typename T>
struct hash<std::enable_if_t<std::is_base_of<foo::hashable, T>::value, T>>

Both compiler complain that they cannot deduce the type, which I find rather irritating because I don't see much of a difference towards the first solution.

My first attempt was the usual common pattern for enable_if:

template <typename T,
          typename DUMMY = std::enable_if_t<std::is_base_of<foo::hashable, T>::value>>
struct hash<T>

Which fails with default template argument in a class template partial specialization.

Is there a clean template metaprogramming way to acchieve this in C++14?

Community
  • 1
  • 1
Zulan
  • 21,896
  • 6
  • 49
  • 109
  • I think `template first = T;` is really stretching it, and specializing that on `E` is not obviously a great idea. Instead of deriving your classes from `hashable` you could create a `hash_base` and then add `template<> struct hash : hash_base {};` for the hashable types. – Bo Persson Oct 21 '15 at 11:43
  • 1
    Interesting link: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1558 – Jarod42 Oct 21 '15 at 12:39
  • @BoPersson I wanted to avoid the additional line for each type, especially as it would have to be in the ugly namespace. – Zulan Oct 21 '15 at 12:43
  • @Jarod42 That means, that the standard does not even describe (currently) how the `first`-solution should compile, right? Edit: And clang seems to treat it differently, while gcc adhers to the proposed solution to use SF. – Zulan Oct 21 '15 at 12:44
  • @Zulan I guess it's not the point here. Your code does trigger a substitution failure, but (on success) an alias template is equivalent to the type it points at, so you end up with `hash` that is not a specialization – Piotr Skotnicki Oct 21 '15 at 16:49

1 Answers1

4

first a little rant:

the design of std::hash is awful. Partial specialisations are not allowed. the committee should have simply copied the boost implementation in full.

(rant over)

I think one elegant solution is to approach it from a different angle:

#include <type_traits>
#include <functional>
#include <unordered_set>

namespace foo
{
    template<class T, class E>
    using first = T;

    struct hashable {};
    struct bar : public hashable {};

    template<class T, typename = void>
    struct hashable_hasher;

    template<class T>
    struct hashable_hasher<T, std::enable_if_t<std::is_base_of<hashable, T>::value>>
    {
        size_t operator()(const T& x) const { return 13; }
    };


    template<class T, typename = void>
    struct choose_hash {
        using type = std::hash<T>;
    };

    template<class T>
    struct choose_hash<T, std::enable_if_t<std::is_base_of<hashable, T>::value>> {
        using type = hashable_hasher<T>;
    };

    template<class T>
    using choose_hash_t = typename choose_hash<T>::type;

    template<class T>
    using choose_set_t = std::unordered_set<T, choose_hash_t<T>>;
}

int main() {
    foo::choose_set_t<foo::bar> baz;
    return 0;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • I'm certainly with you when it comes to the rant. I was *hoping* to be able to preserve the full `std` interface. I always have a very bad feeling when I'm introducing a replacement for a `std` type, because it usually leads to suprises. – Zulan Oct 21 '15 at 12:49
  • 3
    boost::hash is more than a valid replacement for std::hash. All you have to do is define hash_value(const X&) in X's namespace. ADL does everything else for you. Why the std's committee did not follow this obvious line of logic is a mystery to me. They're normally a bright bunch of chaps.... and boost::range also needs to be in the standard! uh oh. you've got me started... – Richard Hodges Oct 21 '15 at 13:04