Long live QDebug::operator<<(q(u)int128)!

Replace the ad-hoc implementation of QTest::toString() in
tst_qglobal.cpp with a QDebug stream operator, so the
QTest::toString() fall-back to QDebug::toString() kicks in.

Since the ABI issues revolving around the new int128 types are not
known, yet, avoid baking the types into the ABI by a) making the
operators constrained templates¹ and b) passing though void* to the
exported helpers. These functions return an error message if Qt was
compiled without support for int128.

Use the Thiago Trick™ (leaving obviouly dead code around for the
compiler to remove without warning) to expose more code to more
compilers. This appears to work elsewhere in Qt, so I hope it does
here, too.

This completes the minimum qint128 support so we're able to debug code
and write tests that use these types.

¹ Templates, unlike inline member functions of wholly-exported
  classes, never² become part of the ABI.

² <insert here the convoluted scenario under which this is false>

Fixes: QTBUG-117011
Change-Id: Ia4e56d26c6ffd18b7d69a7ceaed65b2211d258b2
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Marc Mutz 2023-09-14 18:22:12 +02:00
parent f9f1272e7c
commit ab910e09c7
4 changed files with 173 additions and 40 deletions

View File

@ -15,6 +15,7 @@
#include <private/qtextstream_p.h>
#include <private/qtools_p.h>
#include <array>
#include <q20chrono.h>
QT_BEGIN_NAMESPACE
@ -436,6 +437,87 @@ void QDebug::putTimeUnit(qint64 num, qint64 den)
stream->ts << timeUnit(num, den); // ### optimize
}
namespace {
#ifdef QT_SUPPORTS_INT128
constexpr char Q_INT128_MIN_STR[] = "-170141183460469231731687303715884105728";
constexpr int Int128BufferSize = sizeof(Q_INT128_MIN_STR);
using Int128Buffer = std::array<char, Int128BufferSize>;
// numeric_limits<qint128>::digits10 may not exist
static char *i128ToStringHelper(Int128Buffer &buffer, quint128 n)
{
auto dst = buffer.data() + buffer.size();
*--dst = '\0'; // NUL-terminate
if (n == 0) {
*--dst = '0'; // and done
} else {
while (n != 0) {
*--dst = "0123456789"[n % 10];
n /= 10;
}
}
return dst;
}
#endif // QT_SUPPORTS_INT128
[[maybe_unused]]
static const char *int128Warning()
{
const char *msg = "Qt was not compiled with int128 support.";
qWarning("%s", msg);
return msg;
}
} // unnamed namespace
/*!
\since 6.7
\internal
Helper to the qint128 debug streaming output.
*/
void QDebug::putInt128([[maybe_unused]] const void *p)
{
#ifdef QT_SUPPORTS_INT128
Q_ASSERT(p);
qint128 i;
memcpy(&i, p, sizeof(i)); // alignment paranoia
if (i == Q_INT128_MIN) {
// -i is not representable, hardcode the result:
stream->ts << Q_INT128_MIN_STR;
} else {
Int128Buffer buffer;
auto dst = i128ToStringHelper(buffer, i < 0 ? -i : i);
if (i < 0)
*--dst = '-';
stream->ts << dst;
}
return;
#endif // QT_SUPPORTS_INT128
stream->ts << int128Warning();
}
/*!
\since 6.7
\internal
Helper to the quint128 debug streaming output.
*/
void QDebug::putUInt128([[maybe_unused]] const void *p)
{
#ifdef QT_SUPPORTS_INT128
Q_ASSERT(p);
quint128 i;
memcpy(&i, p, sizeof(i)); // alignment paranoia
Int128Buffer buffer;
stream->ts << i128ToStringHelper(buffer, i);
return;
#endif // QT_SUPPORTS_INT128
stream->ts << int128Warning();
}
/*!
\fn QDebug::swap(QDebug &other)
\since 5.0
@ -902,6 +984,23 @@ QDebug &QDebug::resetFormat()
The unit is not localized.
*/
/*!
\fn template <typename T, if_qint128<T>> QDebug::operator<<(T i)
\fn template <typename T, if_quint128<T>> QDebug::operator<<(T i)
\since 6.7
Prints the textual representation of the 128-bit integer \a i.
\note This operator is only available if Qt supports 128-bit integer types.
If 128-bit integer types are available in your build, but the Qt libraries
were compiled without, the operator will print a warning instead.
\note Because the operator is a function template, no implicit conversions
are performed on its argument. It must be exactly qint128/quint128.
\sa QT_SUPPORTS_INT128
*/
/*!
\fn template <class T> QString QDebug::toString(T &&object)
\since 6.0

View File

@ -11,6 +11,7 @@
#include <QtCore/qcontainerfwd.h>
#include <QtCore/qtextstream.h>
#include <QtCore/qtypes.h>
#include <QtCore/qstring.h>
#include <QtCore/qcontiguouscache.h>
#include <QtCore/qsharedpointer.h>
@ -69,6 +70,8 @@ class QT6_ONLY(Q_CORE_EXPORT) QDebug : public QIODeviceBase
QT7_ONLY(Q_CORE_EXPORT) void putString(const QChar *begin, size_t length);
QT7_ONLY(Q_CORE_EXPORT) void putByteArray(const char *begin, size_t length, Latin1Content content);
QT7_ONLY(Q_CORE_EXPORT) void putTimeUnit(qint64 num, qint64 den);
QT7_ONLY(Q_CORE_EXPORT) void putInt128(const void *i);
QT7_ONLY(Q_CORE_EXPORT) void putUInt128(const void *i);
public:
explicit QDebug(QIODevice *device) : stream(new Stream(device)) {}
explicit QDebug(QString *string) : stream(new Stream(string)) {}
@ -203,6 +206,21 @@ public:
return maybeSpace();
}
#ifdef QT_SUPPORTS_INT128
private:
// Constrained templates so they only match q(u)int128 without conversions.
// Also keeps these operators out of the ABI.
template <typename T>
using if_qint128 = std::enable_if_t<std::is_same_v<T, qint128>, bool>;
template <typename T>
using if_quint128 = std::enable_if_t<std::is_same_v<T, quint128>, bool>;
public:
template <typename T, if_qint128<T> = true>
QDebug &operator<<(T i128) { putInt128(&i128); return maybeSpace(); }
template <typename T, if_quint128<T> = true>
QDebug &operator<<(T u128) { putUInt128(&u128); return maybeSpace(); }
#endif // QT_SUPPORTS_INT128
template <typename T>
static QString toString(T &&object)
{

View File

@ -15,46 +15,6 @@
#include <array>
#include <cmath>
QT_BEGIN_NAMESPACE
namespace QTest {
#ifdef QT_SUPPORTS_INT128
namespace detail {
char *i128ToStringHelper(std::array<char, 64> &buffer, quint128 n)
{
auto dst = buffer.data() + buffer.size();
*--dst = '\0'; // NUL-terminate
if (n == 0) {
*--dst = '0'; // and done
} else {
while (n != 0) {
*--dst = "0123456789"[n % 10];
n /= 10;
}
}
return dst;
}
}
template <>
char *toString(const qint128 &i)
{
if (i == Q_INT128_MIN) // -i is not representable, hardcode:
return qstrdup("-170141183460469231731687303715884105728");
std::array<char, 64> buffer;
auto dst = detail::i128ToStringHelper(buffer, i < 0 ? -i : i);
if (i < 0)
*--dst = '-';
return qstrdup(dst);
}
template <>
char *toString(const quint128 &i)
{
std::array<char, 64> buffer;
return qstrdup(detail::i128ToStringHelper(buffer, i));
}
#endif // QT_SUPPORTS_INT128
} // namespace QTest
QT_END_NAMESPACE
class tst_QGlobal: public QObject
{
Q_OBJECT

View File

@ -2,6 +2,9 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>
#include <QtCore/qtypes.h>
#include <memory>
#include <q20chrono.h>
@ -14,6 +17,8 @@ private:
void addColumns();
void testRows();
private slots:
void int128();
void chrono_duration_data();
void chrono_duration() { testRows(); }
};
@ -57,6 +62,57 @@ template <typename T> void addRow(QByteArrayView name, T &&value, QByteArrayView
#define ADD_ROW(name, expr, expected) \
::addRow(name, expr, #expr, expected, __FILE__, __LINE__)
void tst_toString::int128()
{
#ifndef QT_SUPPORTS_INT128
QSKIP("This test requires int128 support enabled in the compiler.");
#else
// ### port to data-driven once QVariant has support for qint128/quint128
std::unique_ptr<char[]> s;
{
// build Q_INT128_MIN without using Q_INT128_ macros,
// because we use Q_INT128_MIN in the impl
qint128 accu = 1701411834604692317LL;
accu *= 1000000000000000000LL;
accu += 316873037158841057LL;
accu *= -100;
accu -= 28;
QCOMPARE_EQ(accu, Q_INT128_MIN);
s.reset(QTest::toString(accu));
QCOMPARE(s.get(), "-170141183460469231731687303715884105728");
}
// now test with the macro, too:
s.reset(QTest::toString(Q_INT128_MIN));
QCOMPARE(s.get(), "-170141183460469231731687303715884105728");
s.reset(QTest::toString(Q_INT128_MIN + 1));
QCOMPARE(s.get(), "-170141183460469231731687303715884105727");
s.reset(QTest::toString(Q_INT128_MAX));
QCOMPARE(s.get(), "170141183460469231731687303715884105727");
s.reset(QTest::toString(Q_INT128_MAX - 1));
QCOMPARE(s.get(), "170141183460469231731687303715884105726");
s.reset(QTest::toString(Q_UINT128_MAX));
QCOMPARE(s.get(), "340282366920938463463374607431768211455");
s.reset(QTest::toString(Q_UINT128_MAX - 1));
QCOMPARE(s.get(), "340282366920938463463374607431768211454");
s.reset(QTest::toString(quint128{0}));
QCOMPARE(s.get(), "0");
s.reset(QTest::toString(qint128{0}));
QCOMPARE(s.get(), "0");
s.reset(QTest::toString(qint128{-1}));
QCOMPARE(s.get(), "-1");
#endif // QT_SUPPORTS_INT128
}
void tst_toString::chrono_duration_data()
{
addColumns();