13

Suppose I have a simple boolean traits class, MyTrait. That is, for any type T, I can do MyTrait<T>::value and get either true or false. I would like to specialize std::hash for all types T where MyTrait<T>::value is true. Is there any way to do this? Some failed attempts:

template <class T, typename std::enable_if<
                                MyTrait<T>::value, int
                            >::type = 0>
struct hash<T> {
...
}

Failed because:

error: default template argument in a class template partial specialization

I also tried putting all the partial specialization stuff after hash, but then there is an error message for T being in a non-deduced context.

Is there any way to do this? At least one previous question on SO suggests there's not: Specializing std::hash to derived classes.

Either a solution, or a definitive 'No' followed by a brief explanation would be a great answer.

Community
  • 1
  • 1
Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • 2
    Since `std::hash` only has a single template argument, the only kind of partial specialization you can do is e.g. `template struct hash> { ... }`. I don't see any way to cause a substitution failure there to disable the specialization. – melak47 Apr 21 '16 at 16:12
  • 3
    However, you can provide a custom hash type to `std::unordered_map` etc., which you are free to equip with a dummy template argument for specializations to use. – melak47 Apr 21 '16 at 16:14
  • @melak47 Yeah, certainly that is an option, doesn't rank too highly for ease of use though. – Nir Friedman Apr 21 '16 at 16:27

3 Answers3

7

Not sure it is legal, but with concept of C++20, you might do something like:

template <typename T>
concept MyConcept = MyTrait<T>::value;

namespace std
{

    template <MyConcept T>
    struct hash<T>
    {
        std::size_t operator()(const T& t) const { /*..*/ }
        // ...
    };

}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
2

Given templates in the std namespace may be specialised for any user defined type (1)

in order to specialise std::hash for some type T if a trait is true we could naively write something like this (note: does not work):

namespace std
{
  template<class T>
    struct hash<std::enable_if_t<IsCustomHashable<T>, T>>
    {
       ...
    };
}

it doesn't work of course because

23 : error: template parameters not deducible in partial specialization:

But even if it did, it would put us at risk of violating (1) above.

Because what if someone simply specialised our IsCustomHashable metafunction so that it returned true for an int?

Now we would be specialising std::hash for a non-user-defined type, which is forbidden.

one quick and painless way to do what you want is to derive your std::hash specialisations from a helper base class which defers to a free function:

#include <functional>
#include <utility>
#include <unordered_set>


// a base class that defers to a free function.
// the free function can be found via ADL and it can be 
// enabled/disabled with enable_if. it's up to you.

template<class T>
  struct impl_hash
  {
    using argument_type = T;
    using result_type = std::size_t;
    result_type operator()(const argument_type& arg) const {
      return hash_code(arg);
    }
  };


// a test class
struct my_hashable
{
  bool operator==(const my_hashable&) const {
    return true;
  }
};

// implement the free function in the same namespace as the argument
// type's definition
std::size_t hash_code(const my_hashable& mh)
{
  // calculate hash here
  return 0;
}

// now defining a hash specialisation becomes easy
// you could even macroify it
namespace std
{
  template<>
    struct hash<my_hashable>
      : impl_hash<my_hashable>
    {
    };
}

// check it compiles
int main()
{
  std::unordered_set<my_hashable> my_set;
  my_set.emplace();
  return 0;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Useful info, thanks. I was aware of the macro approach in broad terms; I was hoping to avoid it for the usual reasons. – Nir Friedman Apr 21 '16 at 18:04
1

They say that all problems in computer science can be solved by another level of indirection.

If you are willing, we can implement Sean Parent's Runtime Polymorphism technique, which uses type erasure and a teensy bit of polymorphism to delegate to a free function. We can specialize std::hash on the erased type.

Usage looks like this:

template<> struct MyTrait<Foo> : std::true_type{};
template<> struct MyTrait<Bar> : std::true_type{};
// ...
Foo a;
Bar b;
Bad c; // MyTrait<Bad>::value is false

std::cout << std::hash<my_hashable>{}(my_hashable{a}) << std::endl;
std::cout << std::hash<my_hashable>{}(my_hashable{b}) << std::endl;
// compiler error
//std::cout << std::hash<my_hashable>{}(my_hashable{c}) << std::endl;

Demo

Refer to Sean's talk for a deep dive on the approach, but here's the code (with my abbreviated explanation to follow).

First, our type-erasure class that holds a pointer to any T for which there is a free function std::size_t do_hash(const T&)

class my_hashable
{
  public:
  template <class T>
  my_hashable(T& x) : self_(std::make_shared<impl_hashable<T>>(&x))
  {}

  friend std::size_t do_hash(const my_hashable& x)
  {
      return x.self_->hash_();
  }

  private:
  struct base_hashable
  {
    virtual ~base_hashable() = default;
    virtual std::size_t hash_() const = 0;
  }; // base_hashable

  template <class T>
  struct impl_hashable final : base_hashable
  {
    impl_hashable(T* x) : data_(x) { }
    std::size_t hash_() const override
    {
        return do_hash(*data_); // call to free function
    }

    T* data_;
  }; // impl_hashable

 std::shared_ptr<const base_hashable> self_;
}; 

Next, our only specialization of std::hash on the type-erased class:

namespace std
{
  template<>
  struct hash<my_hashable>
  {
    std::size_t operator()(const my_hashable& h) const{return do_hash(h);}
  };
}

How it works:

  • my_hashable is a non-templated class with no virtual methods (good).
  • it's only member is std::shared_ptr<const base_hashable> self_; where base_hashable is a private abstract class that requires that children implement a function std::size_t _hash() const
  • impl_hashable is the workhorse here; a templated class whose instances all derive from bash_hashable and they all delegate their std::size_t hash_() const override function to a free function that accepts a const T&
  • when we construct a my_hashable with an arbitrary type T, we take the address of T and construct a impl_hashable<T> with that pointer. We hide this impl_hashable in a pointer to base class base_hashable.
  • calling do_hash(const my_hashable&) will delegate the call to the appropriate impl_hashables hash_ function via dynamic dispatch.
  • we only need to specialize std::hash on my_hashable and have it delegate to the my_hashable's do_hash friend function.

My approach deviates a bit from Sean's in that the type-erased object doesn't own the T we give to it, but rather takes a non-owning pointer to a pre-existing one. This will allow you to construct a (lightweight) my_hashable only when you need it.

Now we can define our free function that will only work for types where MyTrait<T>::value is true:

template<class T>
std::size_t do_hash(const T& t)
{
    static_assert(MyTrait<T>::value, "Can only call do_hash for types for which MyTrait is true");
    return std::hash<typename T::data_t>{}(t.data);
}

Then, as I showed in the start of this post, we can define our classes and decide which ones satisfy the trait. Here, T takes on types of Foo and Bar (not my_hashable since we already delegated to impl_hashable which recovers the type we passed in when we constructed the my_hashable instance)

AndyG
  • 39,700
  • 8
  • 109
  • 143
  • I see that std::hash can be created wrapping a T, however there still seems to be no std::hash. Does this approach allow for the types which specialize MyTrait to be true to be used directly in something like std::unordered_set, without having to specify a non-default hash type in the set’s template arguments? – jtbandes Feb 07 '20 at 16:45
  • @jtbandes: Unfortunately not. We could create a version of `my_hashable` that owns the underlying `T`, and then we could create a `unordered_set` for example. I realize the additional level of indirection is not ideal, hence the qualification at the top of my post. – AndyG Feb 07 '20 at 16:55
  • 1
    I am familiar with SP polymorphism, that said, this doesn't answer the question, though I appreciate the effort. This isn't specializing on T meeting a trait, but on some other type. This is critical because the goal of this exercise, really the only reason I can think of to specialize std::hash, is to make std::unordered_map/set, "just work". I can only use your solution if I make the key type `my_hashable` which has lots and lots of undesirable consequences. – Nir Friedman Feb 07 '20 at 20:17
  • @NirFriedman: Granted. I agree with you. I lean towards saying that Jarod42's answer is correct despite the fact that the standard doesn't really allow or disallow it. Depending on the trait, it's possible that it is true for a type in the standard library, which may be illegal. Otherwise our hands are kind of tied to either using a macro or type erasure. – AndyG Feb 07 '20 at 20:19
  • @AndyG Yeah I figure it would be on me to ensure that the trait never evaluates true for a type that I don't control. In this particular example, the trait was inheriting from one of my types so I think that would be safe in that regard. – Nir Friedman Feb 07 '20 at 20:56