From 12b41c3332612eb26b7b8598c0a94a4f0d709787 Mon Sep 17 00:00:00 2001 From: Marc Mutz Date: Mon, 21 Oct 2024 14:56:01 +0200 Subject: [PATCH] QDebug: add streaming operators for std::tuple Unlike the existing one for std::pair, make the operator SCARY, using the newly-added StreamTypeErased type-erasure (added in 5cdd1f594d26e1d4f84b00741be1ab7231458512). [ChangeLog][QtCore][QDebug] Can now stream std::tuple. Change-Id: I46670ac32eaee7f0ea5f4543ebcf59e19b394166 Reviewed-by: Ivan Solovev --- src/corelib/io/qdebug.cpp | 35 ++++++++++++++++ src/corelib/io/qdebug.h | 39 +++++++++++++++++ tests/auto/corelib/io/qdebug/tst_qdebug.cpp | 46 +++++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/src/corelib/io/qdebug.cpp b/src/corelib/io/qdebug.cpp index 598355e4296..39eccd889cf 100644 --- a/src/corelib/io/qdebug.cpp +++ b/src/corelib/io/qdebug.cpp @@ -1086,6 +1086,33 @@ QByteArray QDebug::toBytesImpl(StreamTypeErased s, const void *obj) return result; } +/*! + \internal + \since 6.9 + + Outputs a heterogeneous product type (pair, tuple, or anything that + implements the Tuple Protocol). The class name is described by "\a ns + \c{::} \a what", while the addresses of the \a n elements are stored in the + array \a data. The formatters are stored in the array \a ops. + + If \a ns is empty, only \a what is used. +*/ +QDebug &QDebug::putTupleLikeImplImpl(const char *ns, const char *what, + size_t n, StreamTypeErased *ops, const void **data) +{ + const QDebugStateSaver saver(*this); + nospace(); + if (ns && *ns) + *this << ns << "::"; + *this << what << '('; + while (n--) { + (*ops++)(*this, *data++); + if (n) + *this << ", "; + } + return *this << ')'; +} + /*! \fn template QDebug operator<<(QDebug debug, const QList &list) \relates QDebug @@ -1179,6 +1206,14 @@ QByteArray QDebug::toBytesImpl(StreamTypeErased s, const void *obj) \c T need to support streaming into QDebug. */ +/*! + \fn template > QDebug &QDebug::operator<<(const std::tuple &tuple) + \since 6.9 + + Writes the contents of \a tuple to the stream. All \c Ts... need to support + streaming into QDebug. +*/ + /*! \fn template QDebug operator<<(QDebug debug, const std::pair &pair) \relates QDebug diff --git a/src/corelib/io/qdebug.h b/src/corelib/io/qdebug.h index ff31806b4cb..41ada051d72 100644 --- a/src/corelib/io/qdebug.h +++ b/src/corelib/io/qdebug.h @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include @@ -82,6 +84,11 @@ class QT6_ONLY(Q_CORE_EXPORT) QDebug : public QIODeviceBase QT7_ONLY(Q_CORE_EXPORT) void putUInt128(const void *i); QT7_ONLY(Q_CORE_EXPORT) void putQtOrdering(QtOrderingPrivate::QtOrderingTypeFlag flags, Qt::partial_ordering order); + + template + using if_streamable = std::enable_if_t< + std::conjunction_v...> + , bool>; public: explicit QDebug(QIODevice *device) : stream(new Stream(device)) {} explicit QDebug(QString *string) : stream(new Stream(string)) {} @@ -241,6 +248,32 @@ private: using StreamTypeErased = void(*)(QDebug&, const void*); QT7_ONLY(Q_CORE_EXPORT) static QString toStringImpl(StreamTypeErased s, const void *obj); QT7_ONLY(Q_CORE_EXPORT) static QByteArray toBytesImpl(StreamTypeErased s, const void *obj); + QT7_ONLY(Q_CORE_EXPORT) QDebug &putTupleLikeImplImpl(const char *ns, const char *what, size_t n, + StreamTypeErased *ops, const void **data); + + template + QDebug &putTupleLikeImpl(const char *ns, const char *what, const TupleLike &t, + std::index_sequence) + { + if constexpr (sizeof...(Is)) { + StreamTypeErased ops[] = { + &streamTypeErased>>... + }; + const void *data[] = { + std::addressof(std::get(t))... + }; + return putTupleLikeImplImpl(ns, what, sizeof...(Is), ops, data); + } else { + return putTupleLikeImplImpl(ns, what, 0, nullptr, nullptr); + } + } + + template + QDebug &putTupleLike(const char *ns, const char *what, const TupleLike &t) + { + using Indexes = std::make_index_sequence>; + return putTupleLikeImpl(ns, what, t, Indexes{}); + } public: template static QString toString(const T &object) @@ -254,6 +287,12 @@ public: return toBytesImpl(&streamTypeErased, std::addressof(object)); } + template = true> + QDebug &operator<<(const std::tuple &t) + { + return putTupleLike("std", "tuple", t); + } + private: template using if_ordering_type = std::enable_if_t, bool>; diff --git a/tests/auto/corelib/io/qdebug/tst_qdebug.cpp b/tests/auto/corelib/io/qdebug/tst_qdebug.cpp index f54088ffc52..6c6ab7aa87b 100644 --- a/tests/auto/corelib/io/qdebug/tst_qdebug.cpp +++ b/tests/auto/corelib/io/qdebug/tst_qdebug.cpp @@ -25,6 +25,7 @@ namespace pmr = std::pmr; #else namespace pmr = std; #endif +#include using namespace std::chrono; using namespace q20::chrono; @@ -34,6 +35,7 @@ static_assert(QTypeTraits::has_ostream_operator_v); static_assert(QTypeTraits::has_ostream_operator_v); static_assert(QTypeTraits::has_ostream_operator_v>); static_assert(QTypeTraits::has_ostream_operator_v>); +static_assert(QTypeTraits::has_ostream_operator_v>>); struct NonStreamable {}; static_assert(!QTypeTraits::has_ostream_operator_v); static_assert(!QTypeTraits::has_ostream_operator_v>); @@ -81,6 +83,7 @@ private slots: void qDebugQUtf8StringView() const; void qDebugQLatin1String() const; void qDebugStdPair() const; + void qDebugStdTuple() const; void qDebugStdString() const; void qDebugStdStringView() const; void qDebugStdWString() const; @@ -724,6 +727,49 @@ void tst_QDebug::qDebugStdPair() const } } +void tst_QDebug::qDebugStdTuple() const +{ + QByteArray file, function; + int line = 0; + MessageHandlerSetter mhs(myMessageHandler); + { + QDebug d = qDebug(); + d << std::tuple(42, u"foo"_s, 4.2) << std::tuple(42); + d.nospace().noquote() << std::tuple(u"baz"_s); + } +#ifndef QT_NO_MESSAGELOGCONTEXT + file = __FILE__; line = __LINE__ - 5; function = Q_FUNC_INFO; +#endif + QCOMPARE(s_msgType, QtDebugMsg); + QCOMPARE(s_msg, R"(std::tuple(42, "foo", 4.2) std::tuple(42) std::tuple(baz))"_L1); + QCOMPARE(s_file, file); + QCOMPARE(s_line, line); + QCOMPARE(s_function, function); + + /* simpler tests from now on */ +#ifndef Q_OS_QNX // QNX's stdlib doesn't appear to allow nesting tuples (at least not w/CTAD)... + // nested: + qDebug() << std::tuple(std::tuple(std::tuple(4.2, 42), ".42"), u"42"_s); + QCOMPARE(s_msg, R"(std::tuple(std::tuple(std::tuple(4.2, 42), .42), "42"))"_L1); +#endif + // empty: + qDebug() << std::tuple<>(); + QCOMPARE(s_msg, R"(std::tuple())"_L1); + // very long: + qDebug() << std::tuple(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31); + QCOMPARE(s_msg, R"(std::tuple(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31))"_L1); + // with references: + { + auto d = 4.2; auto i = 42; auto s = u"foo"_s; + qDebug() << std::tie(d, i, s); + QCOMPARE(s_msg, R"(std::tuple(4.2, 42, "foo"))"_L1); + s_msg.clear(); // avoid False Positives (next line outputs same as prior) + qDebug() << std::forward_as_tuple(std::move(d), std::move(i), std::as_const(s)); + QCOMPARE(s_msg, R"(std::tuple(4.2, 42, "foo"))"_L1); + } +} + void tst_QDebug::qDebugStdString() const { QString file, function;