9

I am trying to make an std::unordered_set whose key is of type std::tuple.

The below code compiled by MSVC gives me:

Error   C3848 expression having type 'const _Hasher' would lose some const-volatile qualifiers
 in order to call 'size_t TupleHash::operator ()<int,int,int>(const std::tuple<int,int,int> &)'
struct TupleHash
{
    // Uncomment the const keyword below resolves the error
    template<typename T1, typename T2, typename T3>
    std::size_t operator()(const std::tuple<T1, T2,T3> &t) // const 
    {
        return std::hash<T1>{}(std::get<0>(t)) ^ (std::hash<T2>{}(std::get<1>(t)) << 1) ^ ( ( std::hash<T2>{}(std::get<2>(t)) << 2 ) );
    }
};

int main() {
    std::unordered_set<std::tuple<int, int, int>, TupleHash> test;
    test.insert(std::make_tuple(1, 2, 3));
    test.insert(std::make_tuple(2, 3, 4));
    test.insert(std::make_tuple(2, 3, 1));
    test.insert(std::make_tuple(1, 2, 3));
    for (auto& [field1, field2, field3] : test) {
        printf("%d, %d, %d\n", field1, field2, field3);
    }
}

Apparently, it is complaining that TupleHash::operator() is not declared as const. To fix it, I simply added a const keyword at the function signature.

Why does the regulation exist? I can understand that the argument of the hash function, i.e., std::tuple<T1, T2, T3> &t, needs to be constant, as hash container asks its element to be immutable, but why does the member function of the functor itself needs to be const too?

I just tested and confirmed GCC 12.3 behaves the same.

4
  • 1
    What I can think of is - that guarantee that hash function is somewhat consistent between the calls. Let imagine that it is not const and for example changed the third int after hash value was calculated. That means that subsequent call to hash will return different value. And that may brake object lookup in unordered set or map. Commented Jun 20 at 7:51
  • Side note: it is possible to write TupleHash in a more generic way (like this) Commented Jun 20 at 8:22
  • 1
    When I read that a member method is const, it tells me that method does not modify the object (even if the method lies, and does modify the object). The absence of const tells me the method does mutate the object (even if the method doesn't). It's a matter of expectations and responsibilities. Since "C++: all the defaults are wrong", in my opinion all methods ought to be implicitly const by default, and otherwise need to be explicitly marked mutable. But that ship has sailed. Commented Jun 20 at 11:04
  • 1
    The hasher object is a member of the unordered_map. So it will be const when used by one of the map's const methods, e.g. unordered_map::find(const Key&) const. And you can't call a mutable method on a const object. It would also violate the standard data race guarantees if the library allowed modifying potentially thread-shared members through a const this pointer. Commented Jun 20 at 13:12

2 Answers 2

13

The answer is 'because the C++ standard says so'.

[hash.requirements]:

h is a value of type (possibly const) H, ...
h(k)

(h(k) needs to be valid even if h is of a const-qualified type).


The reason that it needs to be callable as const is because sometimes it will only be accessible as a const object.

Consider this:

using Set = std::unordered_set<std::tuple<int, int, int>, TupleHash>;
Set get_set();

int main() {
    const Set s = get_set();
    return s.contains(std::tuple{0, 0, 0})
}

The set is const, so its subobject TupleHash will also generally be const, so it hashes std::tuple{0, 0, 0} with a const TupleHash.

If you really need mutable state for the hasher, use mutable fields. This is only really useful for caches because of how the result must be consistent.

Sign up to request clarification or add additional context in comments.

1 Comment

Since hash functions are only required to produce the same result for the same input within a single execution of a program so I'd be very suspicious of a hasher that is mutating. :-) Caches is a good example though.
7

What can be the interest to have a non const version of the operator(), to see the bad consequences when modifying the instance on which it is applied? And having a const version allows to be applied on a non const instance, while the reverse is false.


Looking about why the compiler complains, for at least gcc version 12.2.0 the operator() is required in hashtable_policy.h by:

  template<typename _Kt>
__hash_code
_M_hash_code_tr(const _Kt& __k) const
{
  static_assert(__is_invocable<const _Hash&, const _Kt&>{},
    "hash function must be invocable with an argument of key type");
  return _M_hash()(__k);
}

and because _M_hash_code_tr (and _M_hash, etc.) is only defined const your operator() must be const too.


Notice if your operator is not const, the static_assert fails too:

/usr/include/c++/12/bits/hashtable_policy.h:1277:25: error: static assertion failed: hash function must be invocable with an argument of key type
 1277 |           static_assert(__is_invocable<const _Hash&, const _Kt&>{},
      |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/12/bits/hashtable_policy.h:1277:25: note: ‘std::__is_invocable<const TupleHash&, const std::tuple<int, int, int>&>()’ evaluates to false

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.