QJsonObject::iterator: add keyView()

Unlike CBOR maps, JSON objects can only have string keys, but, like
CBOR, they are internally stored as either US-ASCII (ie. Latin-1),
UTF-8, or UTF-16, so in order to return a view on the internal
storage, QAnyStringView is the perfect fit.

Add (const_)iterator::keyView() and add a benchmark, prepared for
testing a similar change to the value side of things.

Results (fastest each of ten runs) on my machine suggest a 40%
speedup:

  PASS   : BenchmarkQtJson::iteratorKey()
  RESULT : BenchmarkQtJson::iteratorKey():
       0.071 msecs per iteration (total: 73, iterations: 1024)
  PASS   : BenchmarkQtJson::iteratorKeyView()
  RESULT : BenchmarkQtJson::iteratorKeyView():
       0.042 msecs per iteration (total: 87, iterations: 2048)

[ChangeLog][QtCore][QJsonObject] Added keyView() methods to iterator
and const_iterator, allowing zero-copy inspection of the key().

Task-number: QTBUG-133688
Change-Id: I0ccedaf8a4fa41125b12bdbab5bea3bd2468d9a5
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Marc Mutz 2025-02-10 18:55:38 +01:00
parent 6aaa874d79
commit bbdbcb2eba
8 changed files with 135 additions and 7 deletions

View File

@ -286,6 +286,18 @@ public:
return data->asLatin1();
return data->toUtf8String();
}
QAnyStringView anyStringViewAt(qsizetype idx) const
{
const auto &e = elements.at(idx);
const auto data = byteData(e);
if (!data)
return nullptr;
if (e.flags & QtCbor::Element::StringIsUtf16)
return data->asStringView();
if (e.flags & QtCbor::Element::StringIsAscii)
return data->asLatin1();
return data->asUtf8StringView();
}
static void resetValue(QCborValue &v)
{

View File

@ -914,7 +914,24 @@ QJsonObject::const_iterator QJsonObject::constFindImpl(T key) const
iterator, although it can be done by calling QJsonObject::erase()
followed by QJsonObject::insert().
\sa value()
\sa value(), keyView()
*/
/*!
\fn QAnyStringView QJsonObject::iterator::keyView() const
\since 6.10
Returns the current item's key as a QAnyStringView. This function does not
allocate memory.
Since QJsonObject stores keys in US-ASCII, UTF-8 or UTF-16, the returned
QAnyStringView may be in any of these encodings.
There is no direct way of changing an item's key through an
iterator, although it can be done by calling QJsonObject::erase()
followed by QJsonObject::insert().
\sa key(), value()
*/
/*! \fn QJsonValueRef QJsonObject::iterator::value() const
@ -930,7 +947,7 @@ QJsonObject::const_iterator QJsonObject::constFindImpl(T key) const
the assignment will apply to the element in the QJsonArray or QJsonObject
from which you got the reference.
\sa key(), operator*()
\sa key(), keyView(), operator*()
*/
/*! \fn QJsonValueRef QJsonObject::iterator::operator*() const
@ -945,7 +962,7 @@ QJsonObject::const_iterator QJsonObject::constFindImpl(T key) const
the assignment will apply to the element in the QJsonArray or QJsonObject
from which you got the reference.
\sa key()
\sa key(), keyView()
*/
/*! \fn QJsonValueRef *QJsonObject::iterator::operator->()
@ -1189,14 +1206,27 @@ QJsonObject::const_iterator QJsonObject::constFindImpl(T key) const
Returns the current item's key.
\sa value()
\sa value(), keyView()
*/
/*!
\fn QAnyStringView QJsonObject::const_iterator::keyView() const
\since 6.10
Returns the current item's key as a QAnyStringView. This function does not
allocate.
Since QJsonObject stores keys in US-ASCII, UTF-8 or UTF-16, the returned
QAnyStringView may be in any of these encodings.
\sa value(), key()
*/
/*! \fn QJsonValueConstRef QJsonObject::const_iterator::value() const
Returns the current item's value.
\sa key(), operator*()
\sa key(), keyView(), operator*()
*/
/*! \fn const QJsonValueConstRef QJsonObject::const_iterator::operator*() const
@ -1205,7 +1235,7 @@ QJsonObject::const_iterator QJsonObject::constFindImpl(T key) const
Same as value().
\sa key()
\sa key(), keyView()
*/
/*! \fn const QJsonValueConstRef *QJsonObject::const_iterator::operator->() const

View File

@ -102,6 +102,7 @@ public:
}
inline QString key() const { return item.objectKey(); }
QAnyStringView keyView() const { return item.objectKeyView(); }
inline QJsonValueRef value() const { return item; }
inline QJsonValueRef operator*() const { return item; }
inline const QJsonValueConstRef *operator->() const { return &item; }
@ -215,6 +216,7 @@ public:
}
inline QString key() const { return item.objectKey(); }
QAnyStringView keyView() const { return item.objectKeyView(); }
inline QJsonValueConstRef value() const { return item; }
inline const QJsonValueConstRef operator*() const { return item; }
inline const QJsonValueConstRef *operator->() const { return &item; }

View File

@ -1073,6 +1073,17 @@ QJsonValue QJsonValueConstRef::concrete(QJsonValueConstRef self) noexcept
return QJsonPrivate::Value::fromTrustedCbor(d->valueAt(index));
}
QAnyStringView QJsonValueConstRef::objectKeyView(QJsonValueConstRef self)
{
Q_ASSERT(self.is_object);
const QCborContainerPrivate *d = QJsonPrivate::Value::container(self);
const qsizetype index = QJsonPrivate::Value::indexHelper(self);
Q_ASSERT(d);
Q_ASSERT(index < d->elements.size());
return d->anyStringViewAt(index - 1);
}
QString QJsonValueConstRef::objectKey(QJsonValueConstRef self)
{
Q_ASSERT(self.is_object);

View File

@ -201,6 +201,9 @@ protected:
Q_CORE_EXPORT static QString objectKey(QJsonValueConstRef self);
QString objectKey() const { return objectKey(*this); }
Q_CORE_EXPORT static QAnyStringView objectKeyView(QJsonValueConstRef self);
QAnyStringView objectKeyView() const { return objectKeyView(*this); }
#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) && !defined(QT_BOOTSTRAPPED)
QJsonValueConstRef(QJsonArray *array, qsizetype idx)
: a(array), is_object(false), index(static_cast<quint64>(idx)) {}

View File

@ -1135,6 +1135,7 @@ void tst_QtJson::testObjectIteration()
for (QJsonObject::iterator it = object.begin(); it != object.end(); ++it) {
QJsonValue value = it.value();
QCOMPARE(it.keyView(), QString::number(it.value().toInteger()));
QCOMPARE((double)it.key().toInt(), value.toDouble());
QT_TEST_EQUALITY_OPS(it, QJsonObject::iterator(), false);
}
@ -1600,8 +1601,10 @@ void tst_QtJson::keySorting()
QCOMPARE(o.keys(), sortedKeys);
QJsonObject::const_iterator it = o.constBegin();
QStringList::const_iterator it2 = sortedKeys.constBegin();
for ( ; it != o.constEnd(); ++it, ++it2)
for ( ; it != o.constEnd(); ++it, ++it2) {
QCOMPARE(it.key(), *it2);
QCOMPARE(it.keyView(), *it2);
}
}
void tst_QtJson::undefinedValues()

View File

@ -306,6 +306,7 @@ void tst_QCborValue_Json::nonStringKeysInMaps()
QJsonObject o = m.toJsonObject();
auto it = o.begin();
QVERIFY(it != o.end());
QCOMPARE(it.keyView(), converted);
QCOMPARE(it.key(), converted);
QCOMPARE(it.value(), 0);
QCOMPARE(++it, o.end());

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
#include <QtCore/qjsonarray.h>
#include <QVariantMap>
#include <qjsondocument.h>
#include <qjsonobject.h>
@ -24,6 +26,21 @@ private Q_SLOTS:
void jsonObjectInsert();
void variantMapInsert();
void iteratorKey()
{
iteratorKeyImpl([](auto it) { return it.key(); },
[](auto it) { return it->toString(); });
}
void iteratorKeyView()
{
iteratorKeyImpl([](auto it) { return it.keyView(); },
[](auto it) { return it->toString(); });
}
private:
template <typename KeyFunc, typename ValueFunc>
void iteratorKeyImpl(KeyFunc key, ValueFunc val);
};
BenchmarkQtJson::BenchmarkQtJson(QObject *parent) : QObject(parent)
@ -117,6 +134,55 @@ void BenchmarkQtJson::variantMapInsert()
}
}
namespace Iteration {
template <typename KeyFunc, typename ValueFunc>
void visitObject(const QJsonObject &o, KeyFunc key, ValueFunc val);
template <typename KeyFunc, typename ValueFunc>
void visitArray(const QJsonArray &a, KeyFunc key, ValueFunc val) {
for (const auto &value : a) {
if (value.isObject())
visitObject(value.toObject(), key, val);
else if (value.isArray())
visitArray(value.toArray(), key, val);
}
}
template <typename KeyFunc, typename ValueFunc>
void visitObject(const QJsonObject &o, KeyFunc key, ValueFunc val) {
for (auto it = o.begin(), end = o.end(); it != end; ++it) {
[[maybe_unused]] const auto k = key(it);
[[maybe_unused]] const auto v = val(it);
if (it->isObject())
visitObject(it->toObject(), key, val);
else if (it->isArray())
visitArray(it->toArray(), key, val);
}
}
} // namespace Iteration
template <typename KeyFunc, typename ValueFunc>
void BenchmarkQtJson::iteratorKeyImpl(KeyFunc key, ValueFunc val)
{
const QString testFile = QFINDTESTDATA("test.json");
QVERIFY2(!testFile.isEmpty(), "cannot find test file test.json!");
QFile file(testFile);
QVERIFY2(file.open(QFile::ReadOnly), qPrintable(file.errorString()));
const QByteArray content = file.readAll();
QVERIFY(!content.isEmpty());
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(content, &error);
QVERIFY2(!error.error, qPrintable(QString::asprintf("at offset %d: %ls",
error.offset,
qUtf16Printable(error.errorString()))));
QVERIFY(!doc.isNull());
QVERIFY(doc.isArray());
const auto array = doc.array();
QCOMPARE_GT(array.size(), 0);
QBENCHMARK {
Iteration::visitArray(array, key, val);
}
}
QTEST_MAIN(BenchmarkQtJson)
#include "tst_bench_qtjson.moc"