Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the tuple hash function itself, i.e., operator(), need to be const?

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.

like image 684
PkDrew Avatar asked May 11 '26 23:05

PkDrew


1 Answers

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.

like image 119
Artyer Avatar answered May 13 '26 15:05

Artyer