c++template-specializationc++23stdhash

How to partially specialize std::hash template?


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

Solution

  • 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.