Add QHash::tryEmplace/try_emplace

Like QHash::emplace, but since it returns a bool (inserted/not inserted)
we finally have a way to avoid the
size(), emplace()/insert()/op[], size() pattern.

We also provide try_emplace (also with the hint argument overloads even
though we completely ignore the hint), for general compatibility with
the rest of the standard library. And for that reason it must also use
key_value_iterator instead of our usual iterator.

[ChangeLog][QtCore][QHash] Added tryEmplace().

[ChangeLog][QtCore][QHash] Added try_emplace() for compatibility with
the standard library.

Fixes: QTBUG-130258
Change-Id: Ie14786591ad6e9805e120e11fa01f69349ed4528
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
Reviewed-by: Marc Mutz <marc.mutz@qt.io>
This commit is contained in:
Mårten Nordheim 2024-01-31 14:26:27 +01:00
parent 7a473ca322
commit d9ad2251d9
4 changed files with 346 additions and 15 deletions

View File

@ -2297,6 +2297,114 @@ size_t qHash(long double key, size_t seed) noexcept
\include qhash.cpp qhash-iterator-invalidation-func-desc
*/
/*!
\class QHash::TryEmplaceResult
\inmodule QtCore
\since 6.9
\ingroup tools
\brief The TryEmplaceResult class is used to represent the result of a tryEmplace operation.
The \c{TryEmplaceResult} class is used in QHash to represent the result
of a tryEmplace operation. It holds an \l{iterator} to the newly
created item, or to the pre-existing item that prevented the insertion, and
a boolean, \l{inserted}, denoting whether the insertion took place.
\sa QHash, QHash::tryEmplace()
*/
/*!
\variable QHash::TryEmplaceResult::iterator
Holds the iterator to the newly inserted element, or the element that
prevented the insertion.
*/
/*!
\variable QHash::TryEmplaceResult::inserted
This value is \c{false} if there was already an entry with the same key.
*/
/*!
\fn template <class Key, class T> template <typename... Args> QHash<Key, T>::TryEmplaceResult QHash<Key, T>::tryEmplace(const Key &key, Args &&...args)
\fn template <class Key, class T> template <typename... Args> QHash<Key, T>::TryEmplaceResult QHash<Key, T>::tryEmplace(Key &&key, Args &&...args)
\fn template <class Key, class T> template <typename K, typename... Args, if_heterogeneously_searchable<K> = true, if_key_constructible_from<K> = true> QHash<Key, T>::TryEmplaceResult QHash<Key, T>::tryEmplace(K &&key, Args &&...args)
\since 6.9
Inserts a new item with the \a key and a value constructed from \a args.
If an item with \a key already exists, no insertion takes place.
Returns an instance of \l{TryEmplaceResult}, a structure that holds an
\l{QHash::TryEmplaceResult::}{iterator} to the newly created item, or
to the pre-existing item that prevented the insertion, and a boolean,
\l{QHash::TryEmplaceResult::}{inserted}, denoting whether the insertion
took place.
For example, this can be used to avoid the pattern of comparing old and
new size or double-lookups. Where you might previously have written code like:
\code
QHash<int, MyType> hash;
// [...]
int myKey = getKey();
qsizetype oldSize = hash.size();
MyType &elem = hash[myKey];
if (oldSize != hash.size()) // Size changed: new element!
initialize(elem);
// [use elem...]
\endcode
You can instead write:
\code
QHash<int, MyType> hash;
// [...]
int myKey = getKey();
auto result = hash.tryEmplace(myKey);
if (result.inserted) // New element!
initialize(*result.iterator);
// [use result.iterator...]
\endcode
\sa emplace()
*/
/*!
\fn template <class Key, class T> template <typename K, typename... Args, if_heterogeneously_searchable<K> = true, if_key_constructible_from<K> = true> QHash<Key, T>::try_emplace(const_iterator hint, K &&key, Args &&...args)
\fn template <class Key, class T> template <typename... Args> iterator QHash<Key, T>::try_emplace(const_iterator hint, const Key &key, Args &&...args)
\fn template <class Key, class T> template <typename... Args> iterator QHash<Key, T>::try_emplace(const_iterator hint, Key &&key, Args &&...args)
\since 6.9
Inserts a new item with the \a key and a value constructed from \a args.
If an item with \a key already exists, no insertion takes place.
Returns the iterator of the inserted item, or to the item that prevented the
insertion.
\a hint is ignored.
These functions are provided for compatibility with the standard library.
\sa emplace(), tryEmplace()
*/
/*!
\fn template <class Key, class T> template <typename... Args> std::pair<iterator, bool> QHash<Key, T>::try_emplace(const Key &key, Args &&...args)
\fn template <class Key, class T> template <typename... Args> std::pair<iterator, bool> QHash<Key, T>::try_emplace(Key &&key, Args &&...args)
\fn template <class Key, class T> template <typename K, typename... Args, if_heterogeneously_searchable<K> = true, if_key_constructible_from<K> = true> QHash<Key, T>::try_emplace(K &&key, Args &&...args)
\since 6.9
Inserts a new item with the \a key and a value constructed from \a args.
If an item with \a key already exists, no insertion takes place.
Returns a pair consisting of an iterator to the inserted item (or to the
item that prevented the insertion), and a bool denoting whether the
insertion took place.
These functions are provided for compatibility with the standard library.
\sa emplace(), tryEmplace()
*/
/*! \fn template <class Key, class T> void QHash<Key, T>::insert(const QHash &other)
\since 5.15

View File

@ -15,6 +15,7 @@
#include <initializer_list>
#include <functional> // for std::hash
#include <QtCore/q20type_traits.h>
class tst_QHash; // for befriending
@ -812,7 +813,12 @@ struct iterator {
{ return !(*this == other); }
};
template <typename HashKey, typename KeyArgument>
using HeterogenousConstructProxy = std::conditional_t<
std::is_same_v<HashKey, q20::remove_cvref_t<KeyArgument>>,
KeyArgument, // HashKey == KeyArg w/ potential modifiers, so we keep modifiers
HashKey
>;
} // namespace QHashPrivate
@ -1090,21 +1096,9 @@ public:
T &operator[](const Key &key)
{
return operatorIndexImpl(key);
}
private:
template <typename K> T &operatorIndexImpl(const K &key)
{
const auto copy = isDetached() ? QHash() : *this; // keep 'key' alive across the detach
detach();
auto result = d->findOrInsert(key);
Q_ASSERT(!result.it.atEnd());
if (!result.initialized)
Node::createInPlace(result.it.node(), Key(key), T());
return result.it.node()->value;
return *tryEmplace(key).iterator;
}
public:
const T operator[](const Key &key) const noexcept
{
return value(key);
@ -1255,6 +1249,12 @@ public:
auto asKeyValueRange() && { return QtPrivate::QKeyValueRange(std::move(*this)); }
auto asKeyValueRange() const && { return QtPrivate::QKeyValueRange(std::move(*this)); }
struct TryEmplaceResult
{
QHash::iterator iterator;
bool inserted;
};
iterator erase(const_iterator it)
{
Q_ASSERT(it != constEnd());
@ -1366,6 +1366,77 @@ public:
return emplace_helper(std::move(key), std::forward<Args>(args)...);
}
template <typename... Args>
TryEmplaceResult tryEmplace(const Key &key, Args &&...args)
{
return tryEmplace_impl(key, std::forward<Args>(args)...);
}
template <typename... Args>
TryEmplaceResult tryEmplace(Key &&key, Args &&...args)
{
return tryEmplace_impl(std::move(key), std::forward<Args>(args)...);
}
template <typename... Args>
std::pair<key_value_iterator, bool> try_emplace(const Key &key, Args &&...args)
{
auto r = tryEmplace_impl(key, std::forward<Args>(args)...);
return {key_value_iterator(r.iterator), r.inserted};
}
template <typename... Args>
std::pair<key_value_iterator, bool> try_emplace(Key &&key, Args &&...args)
{
auto r = tryEmplace_impl(std::move(key), std::forward<Args>(args)...);
return {key_value_iterator(r.iterator), r.inserted};
}
template <typename... Args>
key_value_iterator try_emplace(const_iterator /*hint*/, const Key &key, Args &&...args)
{
auto r = tryEmplace_impl(key, std::forward<Args>(args)...);
return key_value_iterator(r.iterator);
}
template <typename... Args>
key_value_iterator try_emplace(const_iterator /*hint*/, Key &&key, Args &&...args)
{
auto r = tryEmplace_impl(std::move(key), std::forward<Args>(args)...);
return key_value_iterator(r.iterator);
}
private:
template <typename K, typename... Args>
TryEmplaceResult tryEmplace_impl(K &&key, Args &&...args)
{
if (!d)
detach();
QHash detachGuard;
typename Data::Bucket bucket = d->findBucket(key);
const bool shouldInsert = bucket.isUnused();
// Even if we don't insert we may have to detach because we are
// returning a non-const iterator:
if (!isDetached() || (shouldInsert && d->shouldGrow())) {
detachGuard = *this;
const bool resized = shouldInsert && d->shouldGrow();
const size_t bucketIndex = bucket.toBucketIndex(d);
// Like reserve(), but unconditionally detaching if no need to grow:
if (isDetached())
d->rehash(d->size + 1);
else
d = Data::detached(d, d->size + (shouldInsert ? 1 : 0));
bucket = resized ? d->findBucket(key) : typename Data::Bucket(d, bucketIndex);
}
if (shouldInsert) {
Node *n = bucket.insert();
using ConstructProxy = typename QHashPrivate::HeterogenousConstructProxy<Key, K>;
Node::createInPlace(n, ConstructProxy(std::forward<K>(key)),
std::forward<Args>(args)...);
++d->size;
}
return {iterator(bucket.toIterator(d)), shouldInsert};
}
public:
float load_factor() const noexcept { return d ? d->loadFactor() : 0; }
static float max_load_factor() noexcept { return 0.5; }
size_t bucket_count() const noexcept { return d ? d->numBuckets : 0; }
@ -1431,7 +1502,7 @@ public:
template <typename K, if_heterogeneously_searchable<K> = true, if_key_constructible_from<K> = true>
T &operator[](const K &key)
{
return operatorIndexImpl(key);
return *tryEmplace(key).iterator;
}
template <typename K, if_heterogeneously_searchable<K> = true>
const T operator[](const K &key) const noexcept
@ -1465,6 +1536,23 @@ public:
{
return find(key);
}
template <typename K, typename... Args, if_heterogeneously_searchable<K> = true, if_key_constructible_from<K> = true>
TryEmplaceResult tryEmplace(K &&key, Args &&...args)
{
return tryEmplace_impl(std::forward<K>(key), std::forward<Args>(args)...);
}
template <typename K, typename... Args, if_heterogeneously_searchable<K> = true, if_key_constructible_from<K> = true>
std::pair<key_value_iterator, bool> try_emplace(K &&key, Args &&...args)
{
auto r = tryEmplace_impl(std::forward<K>(key), std::forward<Args>(args)...);
return { key_value_iterator(r.iterator), r.inserted };
}
template <typename K, typename... Args, if_heterogeneously_searchable<K> = true, if_key_constructible_from<K> = true>
key_value_iterator try_emplace(const_iterator /*hint*/, K &&key, Args &&...args)
{
auto r = tryEmplace_impl(std::forward<K>(key), std::forward<Args>(args)...);
return key_value_iterator(r.iterator);
}
};

View File

@ -447,6 +447,14 @@ private Q_SLOTS:
void opEqNaN_QSet_Float() { opEqNaN_impl<QSet<float>>(); }
void opEqNaN_QSet_Float16() { opEqNaN_impl<QSet<qfloat16>>(); }
void opEqNaN_QSet_Double() { opEqNaN_impl<QSet<double>>(); }
private:
template <typename Container>
void try_emplace_impl() const;
private Q_SLOTS:
void try_emplace_QHash() { try_emplace_impl<QHash<int, int>>(); }
void try_emplace_unordered_map() { try_emplace_impl<std::unordered_map<int, int>>(); }
};
void tst_ContainerApiSymmetry::init()
@ -1306,5 +1314,32 @@ void tst_ContainerApiSymmetry::opEqNaN_impl() const
QCOMPARE_NE(lhs, rhs);
}
template <typename Container>
void tst_ContainerApiSymmetry::try_emplace_impl() const
{
using K = typename Container::key_type;
using V = typename Container::mapped_type;
Container c;
auto p = c.try_emplace(K(), V());
QVERIFY(p.second);
QCOMPARE(p.first->first, K());
QCOMPARE(p.first->second, V());
auto it = c.try_emplace(c.begin(), K(), V());
QCOMPARE(it->first, K());
QCOMPARE(it->second, V());
K k{};
V v{};
p = c.try_emplace(k, v);
QVERIFY(!p.second);
QCOMPARE(p.first->first, K());
QCOMPARE(p.first->second, V());
it = c.try_emplace(c.begin(), k, v);
QCOMPARE(it->first, K());
QCOMPARE(it->second, V());
}
QTEST_APPLESS_MAIN(tst_ContainerApiSymmetry)
#include "tst_containerapisymmetry.moc"

View File

@ -13,6 +13,7 @@
#include <vector>
#include <unordered_set>
#include <string>
#include <tuple>
#include <qsemaphore.h>
@ -81,6 +82,7 @@ private slots:
void multiHashStoresInReverseInsertionOrder();
void emplace();
void tryEmplace();
void badHashFunction();
void hashOfHash();
@ -2829,6 +2831,104 @@ void tst_QHash::emplace()
}
}
void tst_QHash::tryEmplace()
{
{
QHash<int, int> hash;
QHash<int, int>::TryEmplaceResult r = hash.tryEmplace(0, 0);
QCOMPARE_NE(r.iterator, hash.end());
QCOMPARE(hash.size(), 1);
QCOMPARE(hash[0], 0);
QVERIFY(r.inserted);
QCOMPARE(*r.iterator, 0);
int cref = 0;
r = hash.tryEmplace(cref, 1);
QCOMPARE_NE(r.iterator, hash.end());
QCOMPARE(hash.size(), 1);
QCOMPARE(hash[0], 0);
QVERIFY(!r.inserted);
QCOMPARE(*r.iterator, 0);
auto [iterator, inserted] = hash.tryEmplace(1, 1);
QCOMPARE_NE(iterator, hash.end());
QCOMPARE(hash.size(), 2);
QCOMPARE(hash[1], 1);
QVERIFY(inserted);
QCOMPARE(*iterator, 1);
auto [it, inserted2] = hash.try_emplace(cref, 1);
QCOMPARE_NE(it, hash.keyValueEnd());
QCOMPARE(hash.size(), 2);
QCOMPARE(hash[0], 0);
QVERIFY(!inserted2);
QCOMPARE(it->second, 0);
}
{
// Make sure any kind of resize is properly handled
QHash<int, int> hash;
hash.reserve(1);
const qsizetype end = hash.capacity() + 2;
auto [it, inserted] = hash.try_emplace(0, 0);
for (int i = 1; i < end; ++i) {
QHash<int, int> copy = hash;
// Test pure detach, no insert, works on any capacity:
std::tie(it, inserted) = hash.try_emplace(i - 1, i);
QCOMPARE_NE(it, hash.keyValueEnd());
QVERIFY(!inserted);
QCOMPARE(it->second, i - 1);
QVERIFY(!hash.isSharedWith(copy));
copy = hash;
// And when an insertion _does_ happen:
std::tie(it, inserted) = hash.try_emplace(i, i);
QVERIFY(inserted);
QCOMPARE(it->second, i);
}
QCOMPARE(hash.size(), end);
QCOMPARE_GT(hash.capacity(), end);
}
{
// Heterogeneous key
QHash<QString, int> hash;
auto [it, inserted] = hash.try_emplace(HeterogeneousHashingType{u"Hello World"_s}, 242);
QCOMPARE_NE(it, hash.keyValueEnd());
QVERIFY(inserted);
QCOMPARE(it->second, 242);
QCOMPARE(hash.size(), 1);
QCOMPARE(it->first, u"Hello World"_s);
auto r = hash.tryEmplace(HeterogeneousHashingType{u"Uniquely"_s}, 1);
QCOMPARE_NE(r.iterator, hash.end());
QVERIFY(r.inserted);
QCOMPARE(*r.iterator, 1);
QCOMPARE(hash.size(), 2);
QCOMPARE(r.iterator.key(), u"Uniquely"_s);
}
// Overloads taking hint:
{
QHash<QString, int> hash;
auto it = hash.try_emplace(hash.end(), HeterogeneousHashingType{u"Hello World"_s}, 242);
QCOMPARE_NE(it, hash.keyValueEnd());
QCOMPARE(it->second, 242);
QCOMPARE(hash.size(), 1);
QCOMPARE(it->first, u"Hello World"_s);
it = hash.try_emplace(hash.begin(), u"Uniquely"_s, 1);
QCOMPARE_NE(it, hash.keyValueEnd());
QCOMPARE(it->second, 1);
QCOMPARE(hash.size(), 2);
QCOMPARE(it->first, u"Uniquely"_s);
QString cref = u"Uniquely"_s;
it = hash.try_emplace(hash.end(), cref, 16);
QCOMPARE_NE(it, hash.keyValueEnd());
QCOMPARE(it->second, 1);
QCOMPARE(hash.size(), 2);
QCOMPARE(it->first, u"Uniquely"_s);
}
}
struct BadKey {
int k;
BadKey(int i) : k(i) {}