9

I'm using a std::unordered_set for the first time and have a question about the hash function. As far as I understand, if you don't specify a hash function it will default to std::hash<Key>.

I have a mySet member in one of my classes:

typedef std::unordered_set<MyClass> USetType;
USetType mySet;

When I try to build, I get the following error:

error C2440: 'type cast' : cannot convert from 'const MyClass' to 'size_t'

Is it necessary to define a conversion function (to size_t) if you want to use unordered_set with a custom class? Is there any way to avoid writing your own hash function and just using the default?

honk
  • 9,137
  • 11
  • 75
  • 83
user974967
  • 2,928
  • 10
  • 28
  • 45

2 Answers2

15

If you don't specify your own hash functor as template argument, it will default to std::hash<MyClass>, which does not exist unless you define it.

Best define your own specialization of std::hash inside namespace std:

namespace std {
  template <>
  struct hash<MyClass>
  {
    typedef MyClass      argument_type;
    typedef std::size_t  result_type;

    result_type operator()(const MyClass & t) const
    {
       /* ..calculate hash value for t */
    }
  };
}

And make sure you include this code before the declaration of your hash. This way you can declare the hash simply as std::unordered_set<MyClass> with no need for further template arguments.

You didn't specify what MyClass looks like inside, but a typical situation is that your user-defined type simply consists of several simple-type members, for which a default hash function exists. In this case, you will probably want to combine the hash values for the individual types to a hash value for the entire combination. The Boost library provides a function called hash_combine for this purpose. Of course, there is no guarantee that it will work well in your particular case (it depends on the distribution of data values and the likelihood of collisions), but it provides a good and easy-to-use starting point.

Here is an example of how to use it, assuming MyClass consists of two string members:

#include <unordered_set>
#include <boost/functional/hash.hpp>

struct MyClass
{
  std::string _s1;
  std::string _s2;
};

namespace std {
  template <>
  struct hash<MyClass>
  {
    typedef MyClass      argument_type;
    typedef std::size_t  result_type;

    result_type operator()(const MyClass & t) const
    {
      std::size_t val { 0 };
      boost::hash_combine(val,t._s1);
      boost::hash_combine(val,t._s2);
      return val;
    }
  };
}

int main()
{
  std::unordered_set<MyClass> s;
  /* ... */
  return 0;
}
jogojapan
  • 68,383
  • 11
  • 101
  • 131
  • Rather than creating a specialization of std::hash for MyClass, it seems easier to just add a size_t conversion member function which uses hash_combine on its members. My class consists of 7-8 primitive types and I am calling hash_combine on all of them and then returning the seed. – user974967 Nov 21 '12 at 05:05
  • @user974967 Does that work for you? I would be surprised if simply adding a conversion operator was enough to get the unordered set to work. After all, it would still try to instantiate `std::hash`. – jogojapan Nov 21 '12 at 05:13
  • 2
    @user974967 Well. It would work if you declared the set as `std::unordered_set>`. This is a possible solution, I suppose, but it requires that the `operator std::size_t()` is defined, which may mean you get undesired implicit conversions to integer in other parts of the code. I don't recommend doing this. – jogojapan Nov 21 '12 at 05:21
  • I think I'm just going to go with my own functor class. It's not really that much trouble and a lot cleaner than providing a specialization for std::hash. There doesn't seem to be any gain to providing a std::hash specialization other than that you can create unordered_lists without having to specify a functor/function pointer. I know that you were just answering my question, because I asked if I could rely on the default but in this case, it's just a lot of trouble for nothing. Thanks for the info about hash_combine, you really saved me! – user974967 Nov 21 '12 at 06:09
  • 2
    Sure, defining your own functor class is certainly alright. (Although I don't agree there is anything unclean about creating a specialization of `std::hash`.) But I think you are right, the only gain in specializing `std::hash` is that you can use the default template argument then. – jogojapan Nov 21 '12 at 06:20
  • 5
    What 'trouble' is there in specializing `std::hash<>`? A custom hash functor is going to look 90% the same... – ildjarn Nov 25 '12 at 11:31
  • 3
    You need to overload equal_to (or operator==) as well – CashCow Feb 17 '14 at 11:18
3

I'd like to expand on the answer given by jogojapan. As mentioned in a comment by CashCow on that answer, you also have to either overload the equality comparison operator (operator==) for MyClass or define a separate comparison function and provide it to the unordered_set. Otherwise, you will get another error message. For example, VS 2013 throws:

error C2678: binary '==' : no operator found which takes a left-hand operand of type 'const MyClass' (or there is no acceptable conversion)

Moreover, you can use lambda expressions instead of defining the hash and comparison functions. If you don't want to use Boost, then you can also handcraft a hash function. I understand, that you want to use some default function, but a compiler doesn't know how to calculate a meaningful hash for a custom class. However, you can use std::hash for the members of your class. If you put everything together, then your code could be written as follows:

class MyClass {
public:
    int i;
    double d;
    std::string s;
};

int main() {
    auto hash = [](const MyClass& mc){
        return (std::hash<int>()(mc.i) * 31 + std::hash<double>()(mc.d)) * 31 + std::hash<std::string>()(mc.s);
    };
    auto equal = [](const MyClass& mc1, const MyClass& mc2){
        return mc1.i == mc2.i && mc1.d == mc2.d && mc1.s == mc2.s;
    };
    std::unordered_set<MyClass, decltype(hash), decltype(equal)> mySet(8, hash, equal);

    return 0;
}

Code on Ideone

honk
  • 9,137
  • 11
  • 75
  • 83