c++hashlanguage-lawyerspecializationnon-type-template-parameter

Should the type of an argument for a non-type template parameter match the type of the parameter?


I was instantiating a std::unordered_set of std::array<int,3>, I found that the following code cannot pass compilation:

#include <iostream>
#include <unordered_set>
#include <array>

namespace std {
    template <class T>
    inline void hash_combine(size_t& seed, const T& v) {
        hash<T> hasher;
        seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    }

    template <typename T, int N>
    struct hash<array<T, N>>
    {
        size_t operator()(array<T, N> arr) const noexcept {
            size_t hs = 0;
            for (auto iter = arr.begin(); iter != arr.end(); iter++) {
                hash_combine(hs, *iter);
            }
            return hs;
        }
    };
}

int main(int argc, char** argv) {
    std::unordered_set<std::array<int, 3>> arrset;
    std::cout << arrset.size() << std::endl;
    return 0;
}

The error message tells me that the specialization hash<array<T, N>> is not detected. After some hard work, I found that it is caused by the mismatching between the type of non-type template argument N and the type of parameter (size_t). But shouldn't the compiler cast the int N to size_t N automatically? Since the usage of std::array<int, true> also passes the compilation, as I have tested on g++9.4. Is there any specific mention in the C++ standard regarding this particular situation?


Solution

  • std::hash is a red herring, as do I interpret the formal UB described in @Jason's answer to be, w.r.t. to OP's question:

    But shouldn't the compiler cast the int N to size_t N automatically?

    A minimal example:

    #include <cstddef>
    
    template<std::size_t N>
    struct A {};
    
    template<typename T>
    struct S { S() = delete; };
    
    template<int N>
    struct S<A<N>> {};  // #1
    
    S<A<4>> s{};  // error ("use of deleted function")
                  // picks non-defined primary template
    

    [temp.class.spec.match] describes the relevant rules [emphasis mine]:

    /1 When a class template is used in a context that requires an instantiation of the class, it is necessary to determine whether the instantiation is to be generated using the primary template or one of the partial specializations. [...]

    /2 A partial specialization matches a given actual template argument list if the template arguments of the partial specialization can be deduced from the actual template argument list, [...]

    The actual template argument list, from #1 above, is A<4>, from which we can deduce the type A<std::size_t{4}>. The template argument list for the partial specialization, however, is A<int{N}>, which is not a match for the actual template argument list. In fact, the partial specialization #1 will never be used (given the current example), as we cannot produce a template argument list that will match it.