New in Qt 6.1: std::hash Support for QHash
In the previous blog post of this series, we discussed KDToolBox::QtHasher, a helper class that allows us to use unordered associative containers datatypes which have a qHash() overload but not a std::hash specialization. The majority of Qt value classes indeed are lacking such a specialization, even if they do provide a qHash() overload.
For our datatypes, having to provide both qHash and std::hash (if you want to store them in either QHash/QSet or std::unordered_map/set without the need of a customer hasher) is…mildly annoying. In a project, I’ve even resorted to using a macro to implement one function in terms of the other one.
This consideration led me to ask myself, “why can’t QHash itself support std::hash directly?” This would allow me to implement just one hashing function, and not two. Cherry on top: it would allow us to use QHash/QSet datatypes that only offer std::hash and not qHash, such as the ones coming from the Standard Library! (You may want to read here about why it’s actually impossible to reliably add a qHash overload for them.)
A quick patch later, and now, finally, Qt also supports the usage of std::hash as a hashing function. For a given datatype, Qt will now implement a “fallback” mechanism — it’s going to call the first callable option between:
- qHash(T, seed)
- qHash(T)
- std::hash()(t, seed)
- std::hash()(t)
Using the third option with a defaulted seed argument allows Qt to still provide a seed to your hashing function, and your type to still be usable in a Standard Library associative container:
class MyClass { // ... }; namespace std { template <> struct hash<MyClass> { size_t operator()(const MyClass &c, size_t seed = std::hash<int>{}(0)) const noexcept { // ~~~ } }: } // std QHash<MyClass, Data> hash; // OK std::unordered_map<MyClass, Data> hash2; // OK
The support for std::hash in Qt’s containers comes with a minor limitation: you can’t use a custom hashing object, like you can in containers from the Standard Library. Qt is going to default construct your std::hash specialization. Honestly, I’ve practically never seen a specialization that isn’t trivially default constructible anyhow.
The end of qHash?
If the Qt containers support std::hash, what does it mean for qHash? Well, the support for qHash inside Qt’s associative containers is likely to never go away — this is part of the strong source compatibility promise of Qt. (If I had to imagine, qHash may be deprecated in favour of std::hash in Qt 7, and removed in Qt 8. But this is 100% speculation, at this point in time…). But even offering qHash overloads for Qt datatypes is part of the public API of Qt, which means we can’t just remove those overloads. For instance, they are used all over the place in the implementation of qHash for end-user datatypes:
size_t qHash(const MyType &t, size_t seed = 0) noexcept { return qHash(t.key, seed); // <-- qHash used as public API }
Can Qt easily offer std::hash specializations for its own types?
Not so easily, I’m afraid — it would require a massive effort to complement each and every qHash overload with a corresponding std::hash implementation (or outright replace those overloads, if you think qHash should go away). I just don’t see this as being done anytime soon, and that’s why a solution like KDToolBox::QtHasher still makes sense, for the time being.
A similar effort would also be needed if we wanted to offer a constrained version of std::hash. We would need a way to “tag” each Qt datatype (by using some type trait). Then, Qt could offer a centralized:
// Assume we have a magic IsQtClass<T> trait // that tells us if T is a class that belongs to Qt template <typename T> concept QtHashableClass = requires(const T &t) { IsQtClass<T>; { qHash(t, std::declval<size_t>()) } -> std::same_as<size_t>; }; namespace std { template <typename T> requires QtHashableClass<T> struct hash<T> { size_t operator()(const T &t, size_t seed = std::hash<int>{}(0)) const noexcept { return qHash(t, seed); } }; } // std std::unordered_set< /* Any Qt datatype here */ > set; // just works
Did you notice that I’m using constraints and concepts (C++20) here? Such a solution is also unfeasible in C++17, because std::hash does not offer us a “SFINAE hook.” But that’s a discussion for another time…
Thanks for reading!
If you like this article and want to read similar material, consider subscribing via our RSS feed.
Subscribe to KDAB TV for similar informative short video content.
KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.