From 35431062bd7bf0d27b9bf785ab3cbc7fac5a69da Mon Sep 17 00:00:00 2001 From: Marc Mutz Date: Mon, 3 Jun 2019 11:43:24 +0200 Subject: [PATCH] QString: towards QStringView::arg() pt.3: Long live QStringView/QLatin1String::arg() This version of arg(), unlike its QString counterpart, transparently accepts views without conversion to QString, and is also extensible to further argument types, say a future QFormattedNumber. [ChangeLog][QtCore][QStringView/QLatin1String] Added arg(), taking arbitrarily many strings. Change-Id: If40ef3c445f63383e32573f3f515fdda84c7fe3a Reviewed-by: Thiago Macieira --- src/corelib/tools/qstring.cpp | 108 ++++++++++++++---- src/corelib/tools/qstring.h | 55 +++++++++ src/corelib/tools/qstringview.cpp | 18 +++ src/corelib/tools/qstringview.h | 3 + .../tools/qlatin1string/tst_qlatin1string.cpp | 42 +++++++ .../tools/qstringview/tst_qstringview.cpp | 43 +++++++ 6 files changed, 246 insertions(+), 23 deletions(-) diff --git a/src/corelib/tools/qstring.cpp b/src/corelib/tools/qstring.cpp index 53513d4abbe..89ab8667035 100644 --- a/src/corelib/tools/qstring.cpp +++ b/src/corelib/tools/qstring.cpp @@ -8754,19 +8754,23 @@ QString QString::arg(double a, int fieldWidth, char fmt, int prec, QChar fillCha return replaceArgEscapes(*this, d, fieldWidth, arg, locale_arg, fillChar); } -static int getEscape(const QChar *uc, qsizetype *pos, qsizetype len, int maxNumber = 999) +static inline ushort to_unicode(const QChar c) { return c.unicode(); } +static inline ushort to_unicode(const char c) { return QLatin1Char{c}.unicode(); } + +template +static int getEscape(const Char *uc, qsizetype *pos, qsizetype len, int maxNumber = 999) { int i = *pos; ++i; if (i < len && uc[i] == QLatin1Char('L')) ++i; if (i < len) { - int escape = uc[i].unicode() - '0'; + int escape = to_unicode(uc[i]) - '0'; if (uint(escape) >= 10U) return -1; ++i; while (i < len) { - int digit = uc[i].unicode() - '0'; + int digit = to_unicode(uc[i]) - '0'; if (uint(digit) >= 10U) break; escape = (escape * 10) + digit; @@ -8817,18 +8821,23 @@ static int getEscape(const QChar *uc, qsizetype *pos, qsizetype len, int maxNumb namespace { struct Part { - Q_DECL_CONSTEXPR Part() : string{}, number{0} {} + Part() = default; // for QVarLengthArray; do not use Q_DECL_CONSTEXPR Part(QStringView s, int num = -1) - : string{s}, number{num} {} + : tag{QtPrivate::ArgBase::U16}, number{num}, data{s.utf16()}, size{s.size()} {} + Q_DECL_CONSTEXPR Part(QLatin1String s, int num = -1) + : tag{QtPrivate::ArgBase::L1}, number{num}, data{s.data()}, size{s.size()} {} - QStringView string; + void reset(QStringView s) noexcept { *this = {s, number}; } + void reset(QLatin1String s) noexcept { *this = {s, number}; } + + QtPrivate::ArgBase::Tag tag; int number; + const void *data; + qsizetype size; }; } // unnamed namespace -template <> -class QTypeInfo : public QTypeInfoMerger {}; // Q_DECLARE_METATYPE - +Q_DECLARE_TYPEINFO(Part, Q_PRIMITIVE_TYPE); namespace { @@ -8837,7 +8846,8 @@ enum { ExpectedParts = 32 }; typedef QVarLengthArray ParseResult; typedef QVarLengthArray ArgIndexToPlaceholderMap; -static ParseResult parseMultiArgFormatString(QStringView s) +template +static ParseResult parseMultiArgFormatString(StringView s) { ParseResult result; @@ -8884,16 +8894,29 @@ static ArgIndexToPlaceholderMap makeArgIndexToPlaceholderMap(const ParseResult & return result; } -static qsizetype resolveStringRefsAndReturnTotalSize(ParseResult &parts, const ArgIndexToPlaceholderMap &argIndexToPlaceholderMap, const QString *args[]) +static qsizetype resolveStringRefsAndReturnTotalSize(ParseResult &parts, const ArgIndexToPlaceholderMap &argIndexToPlaceholderMap, const QtPrivate::ArgBase *args[]) { + using namespace QtPrivate; qsizetype totalSize = 0; for (Part &part : parts) { if (part.number != -1) { const auto it = std::find(argIndexToPlaceholderMap.begin(), argIndexToPlaceholderMap.end(), part.number); - if (it != argIndexToPlaceholderMap.end()) - part.string = *args[it - argIndexToPlaceholderMap.begin()]; + if (it != argIndexToPlaceholderMap.end()) { + const auto &arg = *args[it - argIndexToPlaceholderMap.begin()]; + switch (arg.tag) { + case ArgBase::L1: + part.reset(static_cast(arg).string); + break; + case ArgBase::U8: + Q_UNREACHABLE(); // waiting for QUtf8String... + break; + case ArgBase::U16: + part.reset(static_cast(arg).string); + break; + } + } } - totalSize += part.string.size(); + totalSize += part.size; } return totalSize; } @@ -8901,18 +8924,35 @@ static qsizetype resolveStringRefsAndReturnTotalSize(ParseResult &parts, const A } // unnamed namespace QString QString::multiArg(int numArgs, const QString **args) const +{ + QVarLengthArray sva; + sva.reserve(numArgs); + QVarLengthArray pointers; + pointers.reserve(numArgs); + for (int i = 0; i < numArgs; ++i) { + sva.push_back(QtPrivate::qStringLikeToArg(*args[i])); + pointers.push_back(&sva.back()); + } + return QtPrivate::argToQString(qToStringViewIgnoringNull(*this), static_cast(numArgs), pointers.data()); +} + +Q_ALWAYS_INLINE QString to_string(QLatin1String s) noexcept { return s; } +Q_ALWAYS_INLINE QString to_string(QStringView s) noexcept { return s.toString(); } + +template +static QString argToQStringImpl(StringView pattern, size_t numArgs, const QtPrivate::ArgBase **args) { // Step 1-2 above - ParseResult parts = parseMultiArgFormatString(qToStringViewIgnoringNull(*this)); + ParseResult parts = parseMultiArgFormatString(pattern); // 3-4 ArgIndexToPlaceholderMap argIndexToPlaceholderMap = makeArgIndexToPlaceholderMap(parts); - if (argIndexToPlaceholderMap.size() > numArgs) // 3a - argIndexToPlaceholderMap.resize(numArgs); - else if (argIndexToPlaceholderMap.size() < numArgs) // 3b - qWarning("QString::arg: %d argument(s) missing in %s", - numArgs - argIndexToPlaceholderMap.size(), toLocal8Bit().data()); + if (static_cast(argIndexToPlaceholderMap.size()) > numArgs) // 3a + argIndexToPlaceholderMap.resize(int(numArgs)); + else if (Q_UNLIKELY(static_cast(argIndexToPlaceholderMap.size()) < numArgs)) // 3b + qWarning("QString::arg: %d argument(s) missing in %ls", + int(numArgs - argIndexToPlaceholderMap.size()), qUtf16Printable(to_string(pattern))); // 5 const qsizetype totalSize = resolveStringRefsAndReturnTotalSize(parts, argIndexToPlaceholderMap, args); @@ -8922,15 +8962,37 @@ QString QString::multiArg(int numArgs, const QString **args) const auto out = const_cast(result.constData()); for (Part part : parts) { - if (const qsizetype sz = part.string.size()) { - memcpy(out, part.string.data(), sz * sizeof(QChar)); - out += sz; + switch (part.tag) { + case QtPrivate::ArgBase::L1: + if (part.size) { + qt_from_latin1(reinterpret_cast(out), + reinterpret_cast(part.data), part.size); + } + break; + case QtPrivate::ArgBase::U8: + Q_UNREACHABLE(); // waiting for QUtf8String + break; + case QtPrivate::ArgBase::U16: + if (part.size) + memcpy(out, part.data, part.size * sizeof(QChar)); + break; } + out += part.size; } return result; } +QString QtPrivate::argToQString(QStringView pattern, size_t n, const ArgBase **args) +{ + return argToQStringImpl(pattern, n, args); +} + +QString QtPrivate::argToQString(QLatin1String pattern, size_t n, const ArgBase **args) +{ + return argToQStringImpl(pattern, n, args); +} + /*! \fn bool QString::isSimpleText() const \internal diff --git a/src/corelib/tools/qstring.h b/src/corelib/tools/qstring.h index 89b25821eed..6820440ca27 100644 --- a/src/corelib/tools/qstring.h +++ b/src/corelib/tools/qstring.h @@ -106,6 +106,9 @@ public: Q_DECL_CONSTEXPR bool isNull() const noexcept { return !data(); } Q_DECL_CONSTEXPR bool isEmpty() const noexcept { return !size(); } + template + Q_REQUIRED_RESULT inline QString arg(Args &&...args) const; + Q_DECL_CONSTEXPR QLatin1Char at(int i) const { return Q_ASSERT(i >= 0), Q_ASSERT(i < size()), QLatin1Char(m_data[i]); } Q_DECL_CONSTEXPR QLatin1Char operator[](int i) const { return at(i); } @@ -1979,6 +1982,58 @@ inline const QString &asString(const QString &s) { return s; } inline QString &&asString(QString &&s) { return std::move(s); } } +// +// QStringView::arg() implementation +// + +namespace QtPrivate { + +struct ArgBase { + enum Tag : uchar { L1, U8, U16 } tag; +}; + +struct QStringViewArg : ArgBase { + QStringView string; + QStringViewArg() = default; + Q_DECL_CONSTEXPR explicit QStringViewArg(QStringView v) noexcept : ArgBase{U16}, string{v} {} +}; + +struct QLatin1StringArg : ArgBase { + QLatin1String string; + QLatin1StringArg() = default; + Q_DECL_CONSTEXPR explicit QLatin1StringArg(QLatin1String v) noexcept : ArgBase{L1}, string{v} {} +}; + +Q_REQUIRED_RESULT Q_CORE_EXPORT QString argToQString(QStringView pattern, size_t n, const ArgBase **args); +Q_REQUIRED_RESULT Q_CORE_EXPORT QString argToQString(QLatin1String pattern, size_t n, const ArgBase **args); + +template +Q_REQUIRED_RESULT Q_ALWAYS_INLINE QString argToQStringDispatch(StringView pattern, const Args &...args) +{ + const ArgBase *argBases[] = {&args..., /* avoid zero-sized array */ nullptr}; + return QtPrivate::argToQString(pattern, sizeof...(Args), argBases); +} + +Q_DECL_CONSTEXPR inline QStringViewArg qStringLikeToArg(QStringView s) noexcept { return QStringViewArg{s}; } + inline QStringViewArg qStringLikeToArg(const QChar &c) noexcept { return QStringViewArg{QStringView{&c, 1}}; } +Q_DECL_CONSTEXPR inline QLatin1StringArg qStringLikeToArg(QLatin1String s) noexcept { return QLatin1StringArg{s}; } + +} // namespace QtPrivate + +template +Q_ALWAYS_INLINE +QString QStringView::arg(Args &&...args) const +{ + return QtPrivate::argToQStringDispatch(*this, QtPrivate::qStringLikeToArg(args)...); +} + +template +Q_ALWAYS_INLINE +QString QLatin1String::arg(Args &&...args) const +{ + return QtPrivate::argToQStringDispatch(*this, QtPrivate::qStringLikeToArg(args)...); +} + QT_END_NAMESPACE #if defined(QT_USE_FAST_OPERATOR_PLUS) || defined(QT_USE_QSTRINGBUILDER) diff --git a/src/corelib/tools/qstringview.cpp b/src/corelib/tools/qstringview.cpp index d94ed4110e1..cc852dd042f 100644 --- a/src/corelib/tools/qstringview.cpp +++ b/src/corelib/tools/qstringview.cpp @@ -529,6 +529,24 @@ QT_BEGIN_NAMESPACE \sa operator[](), front(), back() */ +/*! + \fn QString QStringView::arg(Args &&...args) const + \fn QString QLatin1String::arg(Args &&...args) const + \since 5.14 + + Replaces occurrences of \c{%N} in this string with the corresponding + argument from \a args. The arguments are not positional: the first of + the \a args replaces the \c{%N} with the lowest \c{N} (all of them), the + second of the \a args the \c{%N} with the next-lowest \c{N} etc. + + \c Args can consist of anything that implicitly converts to QStringView + or QLatin1String. + + In addition, the following types are also supported: QChar, QLatin1Char. + + \sa QString::arg() +*/ + /*! \fn QChar QStringView::front() const diff --git a/src/corelib/tools/qstringview.h b/src/corelib/tools/qstringview.h index 67f5d2203c9..b84b2995b9c 100644 --- a/src/corelib/tools/qstringview.h +++ b/src/corelib/tools/qstringview.h @@ -226,6 +226,9 @@ public: // QString API // + template + Q_REQUIRED_RESULT inline QString arg(Args &&...args) const; // defined in qstring.h + Q_REQUIRED_RESULT QByteArray toLatin1() const { return QtPrivate::convertToLatin1(*this); } Q_REQUIRED_RESULT QByteArray toUtf8() const { return QtPrivate::convertToUtf8(*this); } Q_REQUIRED_RESULT QByteArray toLocal8Bit() const { return QtPrivate::convertToLocal8Bit(*this); } diff --git a/tests/auto/corelib/tools/qlatin1string/tst_qlatin1string.cpp b/tests/auto/corelib/tools/qlatin1string/tst_qlatin1string.cpp index dcfb0aa042f..cf46159251b 100644 --- a/tests/auto/corelib/tools/qlatin1string/tst_qlatin1string.cpp +++ b/tests/auto/corelib/tools/qlatin1string/tst_qlatin1string.cpp @@ -45,6 +45,7 @@ class tst_QLatin1String : public QObject private Q_SLOTS: void at(); + void arg() const; void midLeftRight(); void nullString(); void emptyString(); @@ -63,6 +64,47 @@ void tst_QLatin1String::at() QCOMPARE(l1[l1.size() - 1], QLatin1Char('d')); } +void tst_QLatin1String::arg() const +{ +#define CHECK1(pattern, arg1, expected) \ + do { \ + auto p = QLatin1String(pattern); \ + QCOMPARE(p.arg(QLatin1String(arg1)), expected); \ + QCOMPARE(p.arg(QStringViewLiteral(arg1)), expected); \ + QCOMPARE(p.arg(QStringLiteral(arg1)), expected); \ + QCOMPARE(p.arg(QString(QLatin1String(arg1))), expected); \ + } while (false) \ + /*end*/ +#define CHECK2(pattern, arg1, arg2, expected) \ + do { \ + auto p = QLatin1String(pattern); \ + QCOMPARE(p.arg(QLatin1String(arg1), QLatin1String(arg2)), expected); \ + QCOMPARE(p.arg(QStringViewLiteral(arg1), QLatin1String(arg2)), expected); \ + QCOMPARE(p.arg(QLatin1String(arg1), QStringViewLiteral(arg2)), expected); \ + QCOMPARE(p.arg(QStringViewLiteral(arg1), QStringViewLiteral(arg2)), expected); \ + } while (false) \ + /*end*/ + + CHECK1("", "World", ""); + CHECK1("%1", "World", "World"); + CHECK1("!%1?", "World", "!World?"); + CHECK1("%1%1", "World", "WorldWorld"); + CHECK1("%1%2", "World", "World%2"); + CHECK1("%2%1", "World", "%2World"); + + CHECK2("", "Hello", "World", ""); + CHECK2("%1", "Hello", "World", "Hello"); + CHECK2("!%1, %2?", "Hello", "World", "!Hello, World?"); + CHECK2("%1%1", "Hello", "World", "HelloHello"); + CHECK2("%1%2", "Hello", "World", "HelloWorld"); + CHECK2("%2%1", "Hello", "World", "WorldHello"); + +#undef CHECK2 +#undef CHECK1 + + QCOMPARE(QLatin1String(" %2 %2 %1 %3 ").arg(QLatin1Char('c'), QChar::CarriageReturn, u'C'), " \r \r c C "); +} + void tst_QLatin1String::midLeftRight() { const QLatin1String l1("Hello World"); diff --git a/tests/auto/corelib/tools/qstringview/tst_qstringview.cpp b/tests/auto/corelib/tools/qstringview/tst_qstringview.cpp index e800a0d794a..794f39708a4 100644 --- a/tests/auto/corelib/tools/qstringview/tst_qstringview.cpp +++ b/tests/auto/corelib/tools/qstringview/tst_qstringview.cpp @@ -134,6 +134,8 @@ private Q_SLOTS: void literals() const; void at() const; + void arg() const; + void fromQString() const; void fromQStringRef() const; @@ -425,6 +427,47 @@ void tst_QStringView::at() const QCOMPARE(sv.at(4), QChar('o')); QCOMPARE(sv[4], QChar('o')); } +void tst_QStringView::arg() const +{ +#define CHECK1(pattern, arg1, expected) \ + do { \ + auto p = QStringViewLiteral(pattern); \ + QCOMPARE(p.arg(QLatin1String(arg1)), expected); \ + QCOMPARE(p.arg(QStringViewLiteral(arg1)), expected); \ + QCOMPARE(p.arg(QStringLiteral(arg1)), expected); \ + QCOMPARE(p.arg(QString(QLatin1String(arg1))), expected); \ + } while (false) \ + /*end*/ +#define CHECK2(pattern, arg1, arg2, expected) \ + do { \ + auto p = QStringViewLiteral(pattern); \ + QCOMPARE(p.arg(QLatin1String(arg1), QLatin1String(arg2)), expected); \ + QCOMPARE(p.arg(QStringViewLiteral(arg1), QLatin1String(arg2)), expected); \ + QCOMPARE(p.arg(QLatin1String(arg1), QStringViewLiteral(arg2)), expected); \ + QCOMPARE(p.arg(QStringViewLiteral(arg1), QStringViewLiteral(arg2)), expected); \ + } while (false) \ + /*end*/ + + CHECK1("", "World", ""); + CHECK1("%1", "World", "World"); + CHECK1("!%1?", "World", "!World?"); + CHECK1("%1%1", "World", "WorldWorld"); + CHECK1("%1%2", "World", "World%2"); + CHECK1("%2%1", "World", "%2World"); + + CHECK2("", "Hello", "World", ""); + CHECK2("%1", "Hello", "World", "Hello"); + CHECK2("!%1, %2?", "Hello", "World", "!Hello, World?"); + CHECK2("%1%1", "Hello", "World", "HelloHello"); + CHECK2("%1%2", "Hello", "World", "HelloWorld"); + CHECK2("%2%1", "Hello", "World", "WorldHello"); + +#undef CHECK2 +#undef CHECK1 + + QCOMPARE(QStringViewLiteral(" %2 %2 %1 %3 ").arg(QLatin1Char('c'), QChar::CarriageReturn, u'C'), " \r \r c C "); +} + void tst_QStringView::fromQString() const { QString null;