In gcc 12.2, the following C++ code gives me an error. The gist of the complaint is that "hash function must be invocable with an argument of key type":

#include <cstddef>
#include <unordered_map>
#include <functional>

struct OUTER {
    class KEY {
    public:
        bool operator==(KEY const&) const;
        struct Hash {
            auto operator()(KEY const&) const {
                // Give the hash for ten -- why not?
                return std::hash<size_t>{}(10);
            };
        };
    };
    std::unordered_map<KEY, int, KEY::Hash> m;
    bool test(KEY const& ident) { return m.end() != m.find(ident); }
};

The error message is:

In file included from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/hashtable.h:35,
                 from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/unordered_map:46,
                 from <source>:2:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/hashtable_policy.h: In instantiation of 'std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Hash, _RangeHash, _Unused, __cache_hash_code>::__hash_code std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Hash, _RangeHash, _Unused, __cache_hash_code>::_M_hash_code(const _Key&) const [with _Key = OUTER::KEY; _Value = std::pair<const OUTER::KEY, int>; _ExtractKey = std::__detail::_Select1st; _Hash = OUTER::KEY::Hash; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; bool __cache_hash_code = true; __hash_code = long unsigned int]':
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/hashtable.h:1653:46:   required from 'std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::iterator std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::find(const key_type&) [with _Key = OUTER::KEY; _Value = std::pair<const OUTER::KEY, int>; _Alloc = std::allocator<std::pair<const OUTER::KEY, int> >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<OUTER::KEY>; _Hash = OUTER::KEY::Hash; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>; iterator = std::__detail::_Insert_base<OUTER::KEY, std::pair<const OUTER::KEY, int>, std::allocator<std::pair<const OUTER::KEY, int> >, std::__detail::_Select1st, std::equal_to<OUTER::KEY>, OUTER::KEY::Hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::iterator; key_type = OUTER::KEY]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unordered_map.h:869:25:   required from 'std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::iterator std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::find(const key_type&) [with _Key = OUTER::KEY; _Tp = int; _Hash = OUTER::KEY::Hash; _Pred = std::equal_to<OUTER::KEY>; _Alloc = std::allocator<std::pair<const OUTER::KEY, int> >; iterator = std::__detail::_Insert_base<OUTER::KEY, std::pair<const OUTER::KEY, int>, std::allocator<std::pair<const OUTER::KEY, int> >, std::__detail::_Select1st, std::equal_to<OUTER::KEY>, OUTER::KEY::Hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::iterator; key_type = OUTER::KEY]'
<source>:17:59:   required from here
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/hashtable_policy.h:1268:23: error: static assertion failed: hash function must be invocable with an argument of key type
 1268 |         static_assert(__is_invocable<const _Hash&, const _Key&>{},
      |                       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/hashtable_policy.h:1268:23: note: 'std::__is_invocable<const OUTER::KEY::Hash&, const OUTER::KEY&>()' evaluates to false
Compiler returned: 1

Since OUTER::KEY::Hash::operator() takes KEY const& as its argument type, shouldn't I be good?

I expected the above code to work since my hash function DOES accept the key type as an argument.

In fact, I received an error!

2

There are 2 best solutions below

0
William Navarre On

Declaring an explicit return type for OUTER::KEY::Hash::operator() seems to fix the problem.

        struct Hash {
            size_t operator()(KEY const&) const {
                // Give the hash for zero.
                return std::hash<size_t>{}(10);
            };
        };

std::hash objects generally return size_t, so that is a good candidate for the return type here.

I don't know the exact cause of the error, though, since very similar code seems to compile if class OUTER is removed.

0
aschepler On

It looks like this is related to C++ language DR 1890. The example there is

struct A {
  struct B {
    auto foo() { return 0; }
  };
  decltype(B().foo()) x;
};

Although nothing in the C++ Standard clearly says that this code is incorrect, g++, clang++, and MSVC all reject it.

Your example is similar in that instantiating the std::unordered_map template specialization involves a check that the key type can be passed to the hash type. That is, that decltype(std::declval<OUTER::KEY::Hash const&>()(std::declval<OUTER::KEY const&>())) is a valid type.

I think this next example shows why this sort of thing is related to a more general problem with no clear answer:

int T() { return 1; }
struct A {
  struct B {
    static auto foo() { return T(); }
  };
  using T = double;
  using U = decltype(B::foo());
};

Within the body of a member function, enclosing classes are supposed to be considered complete. So we expect the return T(); to interpret T as type A::T (which is double), and not as function ::T, which can only happen if a compiler processes the using T before the body of foo. But processing the using U can only work after processing the body of foo. If foo were to involve the name U, things would be even more of a mess.