How to partially specialize std::hash template?

148 Views Asked by At

Let's say we have a snippet of code as follows

#include <list>
#include <string>
#include <unordered_map>

int main() {
    std::list<int> myList = {4, 1, 3, 2};
    std::unordered_map<std::list<int>::iterator, std::string> myMap;
    myMap[myList.begin()] = "first element";
}

This code in itself wouldn't work since we have to define a template specialization for std::hash, like the one below

template<>
class std::hash<std::list<int>::iterator> {
public:
    size_t operator()(std::list<int>::iterator const& it) const noexcept {
        return hash<int*>()(&*it);
    }
};

This works well.

But when I tried to generalize it and define the following instead,

template<typename T>
class std::hash<std::list<T>::iterator> {
public:
    size_t operator()(std::list<T>::iterator const& it) const noexcept {
        return hash<T*>()(&*it);
    }
};

I see a static assertion failure again from the compiler. Why doesn't this work, and what is the right way to define std::hash for any list-iterator?


Compiler details
  • g++ (GCC) 13.2.1 20230801
  • -std=c++23 flag (C++23 standard)
2

There are 2 best solutions below

1
Barry On BEST ANSWER

There's just no way to make this work:

template<typename T>
class std::hash<std::list<T>::iterator>

This is a non-deduced context, so deduction will always fail†. So you need to come up with some way to make your deduction viable.

The simplest approach is just to wrap:

template <typename T>
struct ListIterator {
    std::list<T>::iterator;
};

template <typename T>
class std::hash<ListIterator<T>> { ... };

This is straightforwardly viable and works just fine, it's just that you need to have a unordered_map<ListIterator<T>, V> instead of an unordered_map<std::list<T>::iterator, V>. On the insertion side, shouldn't be a difference once you add an implicit constructor - but on the extraction side, yeah you might have to do an extra accessor here and there, but this'll get the job done.


†Although I feel like the language should be extended to cover the case of matching types whose name is actually C<T>::D - there are going to be contexts that cannot be deducible at all (like add_const<T>::type), but otherwise for the cases where you want stuff like this to work (and they do exist) you need to try to come up with wacky workarounds.

2
康桓瑋 On

You can use concepts to constrain the type It to be an iterator of std::list

#include <list>
#include <unordered_map>

template<std::bidirectional_iterator It>
  requires std::same_as<It, typename std::list<std::iter_value_t<It>>::iterator>
class std::hash<It> {
 public:
  size_t operator()(const It& it) const noexcept {
    return hash<std::iter_value_t<It>*>()(std::addressof(*it));
  }
};

Demo