QHash: add support for heterogeneous key lookups

This implements support in QHash and QMultiHash for lookups of
heterogeneous key types that produce the same hash value. This is
implemented by duplicating each of the following functions into an
overload on Key and one a template that is enable_if-constrained to a
key type that meets the requirement:
 * contains
 * count
 * equals_range
 * find
 * operator[] (const only)
 * remove
 * take
 * value
 * values (QMultiHash)

The non-const operator[] may insert into the hash, so it's not part of
this commit.

Change-Id: I664b9f014ffc48cbb49bfffd17b037852f0fd192
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
bb10
Thiago Macieira 2024-02-02 17:55:03 -08:00
parent 52abff8cc1
commit b53c153a10
2 changed files with 473 additions and 24 deletions

View File

@ -677,8 +677,10 @@ struct Data
return size >= (numBuckets >> 1);
}
Bucket findBucket(const Key &key) const noexcept
template <typename K> Bucket findBucket(const K &key) const noexcept
{
static_assert(std::is_same_v<std::remove_cv_t<Key>, K> ||
QHashHeterogeneousSearch<std::remove_cv_t<Key>, K>::value);
Q_ASSERT(numBuckets > 0);
size_t hash = QHashPrivate::calculateHash(key, seed);
Bucket bucket(this, GrowthPolicy::bucketForHash(numBuckets, hash));
@ -697,7 +699,7 @@ struct Data
}
}
Node *findNode(const Key &key) const noexcept
template <typename K> Node *findNode(const K &key) const noexcept
{
auto bucket = findBucket(key);
if (bucket.isUnused())
@ -954,6 +956,11 @@ public:
}
bool remove(const Key &key)
{
return removeImpl(key);
}
private:
template <typename K> bool removeImpl(const K &key)
{
if (isEmpty()) // prevents detaching shared null
return false;
@ -967,12 +974,20 @@ public:
d->erase(it);
return true;
}
public:
template <typename Predicate>
qsizetype removeIf(Predicate pred)
{
return QtPrivate::associative_erase_if(*this, pred);
}
T take(const Key &key)
{
return takeImpl(key);
}
private:
template <typename K> T takeImpl(const K &key)
{
if (isEmpty()) // prevents detaching shared null
return T();
@ -988,6 +1003,7 @@ public:
return value;
}
public:
bool contains(const Key &key) const noexcept
{
if (!d)
@ -1025,7 +1041,7 @@ public:
}
private:
template <typename Fn> T valueImpl(const Key &key, Fn &&defaultValue) const noexcept
template <typename K, typename Fn> T valueImpl(const K &key, Fn &&defaultValue) const noexcept
{
if (d) {
Node *n = d->findNode(key);
@ -1240,11 +1256,7 @@ private:
return std::make_pair(first, second);
}
public:
typedef iterator Iterator;
typedef const_iterator ConstIterator;
inline qsizetype count() const noexcept { return d ? qsizetype(d->size) : 0; }
iterator find(const Key &key)
template <typename K> iterator findImpl(const K &key)
{
if (isEmpty()) // prevents detaching shared null
return end();
@ -1256,7 +1268,7 @@ public:
return end();
return iterator(it.toIterator(d));
}
const_iterator find(const Key &key) const noexcept
template <typename K> const_iterator constFindImpl(const K &key) const noexcept
{
if (isEmpty())
return end();
@ -1265,6 +1277,19 @@ public:
return end();
return const_iterator({d, it.toBucketIndex(d)});
}
public:
typedef iterator Iterator;
typedef const_iterator ConstIterator;
inline qsizetype count() const noexcept { return d ? qsizetype(d->size) : 0; }
iterator find(const Key &key)
{
return findImpl(key);
}
const_iterator find(const Key &key) const noexcept
{
return constFindImpl(key);
}
const_iterator constFind(const Key &key) const noexcept
{
return find(key);
@ -1328,8 +1353,61 @@ private:
result.it.node()->emplaceValue(std::forward<Args>(args)...);
return iterator(result.it);
}
};
public:
#ifdef __cpp_concepts
bool remove(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return removeImpl(key);
}
T take(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return takeImpl(key);
}
bool contains(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return d ? d->findNode(key) != nullptr : false;
}
qsizetype count(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return contains(key) ? 1 : 0;
}
T value(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return valueImpl(key, [] { return T(); });
}
T value(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &defaultValue) const noexcept
{
return valueImpl(key, [&] { return defaultValue; });
}
const T operator[](const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return value(key);
}
std::pair<iterator, iterator>
equal_range(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return equal_range_impl(*this, key);
}
std::pair<const_iterator, const_iterator>
equal_range(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return equal_range_impl(*this, key);
}
iterator find(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return findImpl(key);
}
const_iterator find(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return constFindImpl(key);
}
const_iterator constFind(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return find(key);
}
#endif // __cpp_concepts
};
template <typename Key, typename T>
@ -1506,6 +1584,11 @@ public:
}
qsizetype remove(const Key &key)
{
return removeImpl(key);
}
private:
template <typename K> qsizetype removeImpl(const K &key)
{
if (isEmpty()) // prevents detaching shared null
return 0;
@ -1522,12 +1605,20 @@ public:
d->erase(it);
return n;
}
public:
template <typename Predicate>
qsizetype removeIf(Predicate pred)
{
return QtPrivate::associative_erase_if(*this, pred);
}
T take(const Key &key)
{
return takeImpl(key);
}
private:
template <typename K> T takeImpl(const K &key)
{
if (isEmpty()) // prevents detaching shared null
return T();
@ -1553,6 +1644,7 @@ public:
return t;
}
public:
bool contains(const Key &key) const noexcept
{
if (!d)
@ -1586,7 +1678,7 @@ public:
}
private:
template <typename Fn> T valueImpl(const Key &key, Fn &&defaultValue) const noexcept
template <typename K, typename Fn> T valueImpl(const K &key, Fn &&defaultValue) const noexcept
{
if (d) {
Node *n = d->findNode(key);
@ -1650,8 +1742,14 @@ public:
}
return res;
}
QList<T> values() const { return QList<T>(begin(), end()); }
QList<T> values(const Key &key) const
{
return valuesImpl(key);
}
private:
template <typename K> QList<T> valuesImpl(const K &key) const
{
QList<T> values;
if (d) {
@ -1667,6 +1765,7 @@ public:
return values;
}
public:
class const_iterator;
class iterator
@ -1878,7 +1977,9 @@ public:
typedef iterator Iterator;
typedef const_iterator ConstIterator;
inline qsizetype count() const noexcept { return size(); }
iterator find(const Key &key)
private:
template <typename K> iterator findImpl(const K &key)
{
if (isEmpty())
return end();
@ -1891,11 +1992,7 @@ public:
return end();
return iterator(it.toIterator(d));
}
const_iterator find(const Key &key) const noexcept
{
return constFind(key);
}
const_iterator constFind(const Key &key) const noexcept
template <typename K> const_iterator constFindImpl(const K &key) const noexcept
{
if (isEmpty())
return end();
@ -1904,6 +2001,20 @@ public:
return constEnd();
return const_iterator(it.toIterator(d));
}
public:
iterator find(const Key &key)
{
return findImpl(key);
}
const_iterator constFind(const Key &key) const noexcept
{
return constFindImpl(key);
}
const_iterator find(const Key &key) const noexcept
{
return constFindImpl(key);
}
iterator insert(const Key &key, const T &value)
{
return emplace(key, value);
@ -1968,6 +2079,11 @@ public:
{ QMultiHash result = *this; result += other; return result; }
bool contains(const Key &key, const T &value) const noexcept
{
return containsImpl(key, value);
}
private:
template <typename K> bool containsImpl(const K &key, const T &value) const noexcept
{
if (isEmpty())
return false;
@ -1977,7 +2093,13 @@ public:
return n->value->contains(value);
}
public:
qsizetype remove(const Key &key, const T &value)
{
return removeImpl(key, value);
}
private:
template <typename K> qsizetype removeImpl(const K &key, const T &value)
{
if (isEmpty()) // prevents detaching shared null
return 0;
@ -2007,7 +2129,13 @@ public:
return n;
}
public:
qsizetype count(const Key &key) const noexcept
{
return countImpl(key);
}
private:
template <typename K> qsizetype countImpl(const K &key) const noexcept
{
if (!d)
return 0;
@ -2024,7 +2152,13 @@ public:
return n;
}
public:
qsizetype count(const Key &key, const T &value) const noexcept
{
return countImpl(key, value);
}
private:
template <typename K> qsizetype countImpl(const K &key, const T &value) const noexcept
{
if (!d)
return 0;
@ -2042,7 +2176,7 @@ public:
return n;
}
iterator find(const Key &key, const T &value)
template <typename K> iterator findImpl(const K &key, const T &value)
{
if (isEmpty())
return end();
@ -2051,11 +2185,7 @@ public:
auto it = constFind(key, value);
return iterator(it.i, it.e);
}
const_iterator find(const Key &key, const T &value) const noexcept
{
return constFind(key, value);
}
const_iterator constFind(const Key &key, const T &value) const noexcept
template <typename K> const_iterator constFindImpl(const K &key, const T &value) const noexcept
{
const_iterator i(constFind(key));
const_iterator end(constEnd());
@ -2067,6 +2197,21 @@ public:
return end;
}
public:
iterator find(const Key &key, const T &value)
{
return findImpl(key, value);
}
const_iterator constFind(const Key &key, const T &value) const noexcept
{
return constFindImpl(key, value);
}
const_iterator find(const Key &key, const T &value) const noexcept
{
return constFind(key, value);
}
QMultiHash &unite(const QMultiHash &other)
{
if (isEmpty()) {
@ -2103,6 +2248,11 @@ public:
}
std::pair<iterator, iterator> equal_range(const Key &key)
{
return equal_range_impl(key);
}
private:
template <typename K> std::pair<iterator, iterator> equal_range_impl(const K &key)
{
const auto copy = isDetached() ? QMultiHash() : *this; // keep 'key' alive across the detach
detach();
@ -2110,7 +2260,13 @@ public:
return {iterator(pair.first.i), iterator(pair.second.i)};
}
public:
std::pair<const_iterator, const_iterator> equal_range(const Key &key) const noexcept
{
return equal_range_impl(key);
}
private:
template <typename K> std::pair<const_iterator, const_iterator> equal_range_impl(const K &key) const noexcept
{
if (!d)
return {end(), end()};
@ -2124,7 +2280,6 @@ public:
return {const_iterator(it), const_iterator(end)};
}
private:
void detach_helper()
{
if (!d) {
@ -2161,6 +2316,90 @@ private:
}
return iterator(result.it);
}
public:
#ifdef __cpp_concepts
qsizetype remove(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return removeImpl(key);
}
T take(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return takeImpl(key);
}
bool contains(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
if (!d)
return false;
return d->findNode(key) != nullptr;
}
T value(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return valueImpl(key, [] { return T(); });
}
T value(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &defaultValue) const noexcept
{
return valueImpl(key, [&] { return defaultValue; });
}
const T operator[](const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return value(key);
}
QList<T> values(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return valuesImpl(key);
}
iterator find(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return findImpl(key);
}
const_iterator constFind(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return constFindImpl(key);
}
const_iterator find(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return constFindImpl(key);
}
bool contains(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &value) const noexcept
{
return containsImpl(key, value);
}
qsizetype remove(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &value)
{
return removeImpl(key, value);
}
qsizetype count(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return countImpl(key);
}
qsizetype count(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &value) const noexcept
{
return countImpl(key, value);
}
iterator find(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &value)
{
return findImpl(key, value);
}
const_iterator constFind(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &value) const noexcept
{
return constFindImpl(key, value);
}
const_iterator find(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key, const T &value) const noexcept
{
return constFind(key, value);
}
std::pair<iterator, iterator>
equal_range(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key)
{
return equal_range_impl(key);
}
std::pair<const_iterator, const_iterator>
equal_range(const QHashPrivate::HeterogeneouslySearchableWith<Key> auto &key) const noexcept
{
return equal_range_impl(key);
}
#endif // __cpp_concepts
};
Q_DECLARE_ASSOCIATIVE_FORWARD_ITERATOR(Hash)

View File

@ -38,6 +38,11 @@ private slots:
void qhash();
void take(); // copied from tst_QMap
void operator_eq(); // slightly modified from tst_QMap
void heterogeneousSearch();
void heterogeneousSearchConstKey();
void heterogeneousSearchByteArray();
void heterogeneousSearchString();
void rehash_isnt_quadratic();
void dont_need_default_constructor();
void qmultihash_specific();
@ -45,6 +50,10 @@ private slots:
void qmultihash_qhash_rvalue_ref_unite();
void qmultihashUnite();
void qmultihashSize();
void qmultihashHeterogeneousSearch();
void qmultihashHeterogeneousSearchConstKey();
void qmultihashHeterogeneousSearchByteArray();
void qmultihashHeterogeneousSearchString();
void compare();
void compare2();
@ -1158,6 +1167,187 @@ void tst_QHash::operator_eq()
}
}
#ifdef __cpp_concepts
struct HeterogeneousHashingType
{
inline static int conversionCount = 0;
QString s;
Q_IMPLICIT operator QString() const
{
++conversionCount;
return s;
}
// std::equality_comparable_with requires we be self-comparable too
friend bool operator==(const HeterogeneousHashingType &t1, const HeterogeneousHashingType &t2) = default;
friend bool operator==(const QString &string, const HeterogeneousHashingType &tester)
{ return tester.s == string; }
friend bool operator!=(const QString &string, const HeterogeneousHashingType &tester)
{ return !(tester.s == string); }
friend size_t qHash(const HeterogeneousHashingType &tester, size_t seed)
{ return qHash(tester.s, seed); }
};
QT_BEGIN_NAMESPACE
template <> struct QHashHeterogeneousSearch<QString, HeterogeneousHashingType> : std::true_type {};
template <> struct QHashHeterogeneousSearch<HeterogeneousHashingType, QString> : std::true_type {};
QT_END_NAMESPACE
static_assert(std::is_same_v<QString, std::common_type_t<QString, HeterogeneousHashingType>>);
static_assert(std::equality_comparable_with<QString, HeterogeneousHashingType>);
static_assert(QHashPrivate::HeterogeneouslySearchableWith<QString, HeterogeneousHashingType>);
static_assert(QHashPrivate::HeterogeneouslySearchableWith<HeterogeneousHashingType, QString>);
template <typename T> struct HeterogeneousSearchTestHelper
{
static void resetCounter() {}
static void checkCounter() {}
};
template <> struct HeterogeneousSearchTestHelper<HeterogeneousHashingType>
{
static void resetCounter()
{
HeterogeneousHashingType::conversionCount = 0;
}
static void checkCounter()
{
QTest::setThrowOnFail(true);
auto scopeExit = qScopeGuard([] { QTest::setThrowOnFail(false); });
QCOMPARE(HeterogeneousHashingType::conversionCount, 0);
}
};
#else
using HeterogeneousHashingType = QString;
#endif
template <template <typename, typename> class Hash, typename String, typename View>
static void heterogeneousSearchTest(const QList<std::remove_const_t<String>> &keys)
{
#ifdef __cpp_concepts
using Helper = HeterogeneousSearchTestHelper<View>;
String key = keys.last();
String otherKey = keys.first();
View keyView(key);
View otherKeyView(otherKey);
Hash<String, qsizetype> hash;
static constexpr bool IsMultiHash = !std::is_same_v<decltype(hash.remove(String())), bool>;
hash[key] = keys.size();
Helper::resetCounter();
QVERIFY(hash.contains(keyView));
QCOMPARE_EQ(hash.count(keyView), 1);
QCOMPARE_EQ(hash.value(keyView), keys.size());
QCOMPARE_EQ(hash.value(keyView, -1), keys.size());
QCOMPARE_EQ(std::as_const(hash)[keyView], keys.size());
QCOMPARE_EQ(hash.find(keyView), hash.begin());
QCOMPARE_EQ(std::as_const(hash).find(keyView), hash.constBegin());
QCOMPARE_EQ(hash.constFind(keyView), hash.constBegin());
QCOMPARE_EQ(hash.equal_range(keyView), std::make_pair(hash.begin(), hash.end()));
QCOMPARE_EQ(std::as_const(hash).equal_range(keyView),
std::make_pair(hash.constBegin(), hash.constEnd()));
Helper::checkCounter();
QVERIFY(!hash.contains(otherKeyView));
QCOMPARE_EQ(hash.count(otherKeyView), 0);
QCOMPARE_EQ(hash.value(otherKeyView), 0);
QCOMPARE_EQ(hash.value(otherKeyView, -1), -1);
QCOMPARE_EQ(std::as_const(hash)[otherKeyView], 0);
QCOMPARE_EQ(hash.find(otherKeyView), hash.end());
QCOMPARE_EQ(std::as_const(hash).find(otherKeyView), hash.constEnd());
QCOMPARE_EQ(hash.constFind(otherKeyView), hash.constEnd());
QCOMPARE_EQ(hash.equal_range(otherKeyView), std::make_pair(hash.end(), hash.end()));
QCOMPARE_EQ(std::as_const(hash).equal_range(otherKeyView),
std::make_pair(hash.constEnd(), hash.constEnd()));
Helper::checkCounter();
if constexpr (IsMultiHash) {
hash.insert(key, keys.size());
QCOMPARE_EQ(hash.count(keyView), 2);
// not depending on which of the two the current implementation finds
QCOMPARE_NE(hash.value(keyView), 0);
QCOMPARE_NE(hash.value(keyView, -1000), -1000);
QCOMPARE_NE(std::as_const(hash)[keyView], 0);
QCOMPARE_NE(hash.find(keyView), hash.end());
QCOMPARE_NE(std::as_const(hash).find(keyView), hash.constEnd());
QCOMPARE_NE(hash.constFind(keyView), hash.constEnd());
QCOMPARE_NE(hash.equal_range(keyView), std::make_pair(hash.end(), hash.end()));
QCOMPARE_NE(std::as_const(hash).equal_range(keyView),
std::make_pair(hash.constEnd(), hash.constEnd()));
// QMultiHash-specific functions
QVERIFY(hash.contains(keyView, keys.size()));
QCOMPARE_EQ(hash.count(keyView, 0), 0);
QCOMPARE_EQ(hash.count(keyView, keys.size()), 2);
QCOMPARE_EQ(hash.values(keyView), QList<qsizetype>({ keys.size(), keys.size() }));
hash.insert(key, -keys.size());
QCOMPARE_EQ(hash.count(keyView), 3);
QCOMPARE_EQ(hash.find(keyView, 0), hash.end());
QCOMPARE_NE(hash.find(keyView, keys.size()), hash.end());
QCOMPARE_NE(hash.find(keyView, -keys.size()), hash.end());
QCOMPARE_EQ(std::as_const(hash).find(keyView, 0), hash.constEnd());
QCOMPARE_NE(std::as_const(hash).find(keyView, keys.size()), hash.constEnd());
QCOMPARE_NE(std::as_const(hash).find(keyView, -keys.size()), hash.constEnd());
QCOMPARE_EQ(hash.constFind(keyView, 0), hash.constEnd());
QCOMPARE_NE(hash.constFind(keyView, keys.size()), hash.constEnd());
QCOMPARE_NE(hash.constFind(keyView, -keys.size()), hash.constEnd());
// removals
QCOMPARE_EQ(hash.remove(keyView, -keys.size()), 1);
QCOMPARE_EQ(hash.remove(keyView), 2);
} else {
// removals
QCOMPARE_EQ(hash.remove(keyView), true);
}
QCOMPARE_EQ(hash.take(otherKeyView), 0);
QVERIFY(hash.isEmpty());
Helper::checkCounter();
// repeat with more keys
for (qsizetype i = 0; i < keys.size() - 1; ++i) {
hash.insert(keys[i], -(i + 1));
hash.insert(keys[i], i + 1);
}
QVERIFY(!hash.contains(keyView));
QCOMPARE_EQ(hash.count(keyView), 0);
QCOMPARE_EQ(hash.value(keyView), 0);
QCOMPARE_EQ(hash.value(keyView, -1), -1);
QCOMPARE_EQ(std::as_const(hash)[keyView], 0);
QCOMPARE_EQ(hash.find(keyView), hash.end());
QCOMPARE_EQ(hash.constFind(keyView), hash.constEnd());
Helper::checkCounter();
#else
Q_UNUSED(keys);
QSKIP("This feature requires C++20 (concepts)");
#endif
}
void tst_QHash::heterogeneousSearch()
{
heterogeneousSearchTest<QHash, QString, HeterogeneousHashingType>({ "Hello", {}, "World" });
}
void tst_QHash::heterogeneousSearchConstKey()
{
// QHash<const QString, X> seen in the wild (e.g. Qt Creator)
heterogeneousSearchTest<QHash, const QString, HeterogeneousHashingType>({ "Hello", {}, "World" });
}
void tst_QHash::heterogeneousSearchByteArray()
{
heterogeneousSearchTest<QHash, QByteArray, QByteArrayView>({ "Hello", {}, "World" });
}
void tst_QHash::heterogeneousSearchString()
{
heterogeneousSearchTest<QHash, QString, QStringView>({ "Hello", {}, "World" });
}
void tst_QHash::compare()
{
QHash<int, QString> hash1,hash2;
@ -2127,6 +2317,26 @@ void tst_QHash::qmultihashSize()
}
}
void tst_QHash::qmultihashHeterogeneousSearch()
{
heterogeneousSearchTest<QMultiHash, QString, HeterogeneousHashingType>({ "Hello", {}, "World" });
}
void tst_QHash::qmultihashHeterogeneousSearchConstKey()
{
heterogeneousSearchTest<QMultiHash, const QString, HeterogeneousHashingType>({ "Hello", {}, "World" });
}
void tst_QHash::qmultihashHeterogeneousSearchByteArray()
{
heterogeneousSearchTest<QMultiHash, QByteArray, QByteArrayView>({ "Hello", {}, "World" });
}
void tst_QHash::qmultihashHeterogeneousSearchString()
{
heterogeneousSearchTest<QMultiHash, QString, QStringView>({ "Hello", {}, "World" });
}
void tst_QHash::keys_values_uniqueKeys()
{
QMultiHash<QString, int> hash;