Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Specializing `std::hash` for classes meeting condition

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.

like image 312
Nir Friedman Avatar asked Apr 21 '16 15:04

Nir Friedman


3 Answers

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

like image 192
Jarod42 Avatar answered Nov 10 '22 04:11

Jarod42


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;
}
like image 22
Richard Hodges Avatar answered Nov 10 '22 04:11

Richard Hodges


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)

like image 30
AndyG Avatar answered Nov 10 '22 03:11

AndyG