Qt Timers: disallow setting negative intervals

As disccussed in the code review. Replace negative values with 1ms (not
0ms as that would spin up the event-loop too many times).

Move QTimer::start(std::chrono::milliseconds msec) API docs next to the
method definition so as to not forget changing it when the
implementation changes.

Drive-by, qWarning() and co. are marked cold so there is no need to use
Q_UNLIKELY in an if-condition leading to calling these methods.

[ChangeLog][QtCore][Important Behvaior Change] Timers with negative
intervals aren't allowed anymore, that is, if you try to start a timer
(or set the interval) with a negative value, that interval will be set
to 1ms. Previously Qt timers would let you set the interval to a
negative value, but behave in surprising ways (for example stop the
timer if it was running or not start it at all). This change affects
QTimer, QChronoTimer, QBasicTimer and QObject::startTimer().

Fixes: QTBUG-132359
Change-Id: I09309063665db051337f91160971993d9ce7911e
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Ahmad Samir 2024-12-19 14:10:31 +02:00
parent d4c312f68a
commit f1f610bc67
8 changed files with 133 additions and 68 deletions

View File

@ -145,6 +145,8 @@ QT_BEGIN_NAMESPACE
The given \a object will receive timer events. The given \a object will receive timer events.
\include timers-common.qdocinc negative-intervals-not-allowed
//! [start-nanoseconds-note] //! [start-nanoseconds-note]
\note Starting from Qt 6.9 this method takes std::chrono::nanoseconds, \note Starting from Qt 6.9 this method takes std::chrono::nanoseconds,
before that it took std::chrono::milliseconds. This change is before that it took std::chrono::milliseconds. This change is
@ -170,6 +172,8 @@ QT_BEGIN_NAMESPACE
given \a timerType. See Qt::TimerType for information on the different given \a timerType. See Qt::TimerType for information on the different
timer types. timer types.
\include timers-common.qdocinc negative-intervals-not-allowed
\a obj will receive timer events. \a obj will receive timer events.
\include qbasictimer.cpp start-nanoseconds-note \include qbasictimer.cpp start-nanoseconds-note
@ -179,9 +183,10 @@ QT_BEGIN_NAMESPACE
void QBasicTimer::start(Duration duration, Qt::TimerType timerType, QObject *obj) void QBasicTimer::start(Duration duration, Qt::TimerType timerType, QObject *obj)
{ {
QAbstractEventDispatcher *eventDispatcher = QAbstractEventDispatcher::instance(); QAbstractEventDispatcher *eventDispatcher = QAbstractEventDispatcher::instance();
if (Q_UNLIKELY(duration < 0ns)) { if (duration < 0ns) {
qWarning("QBasicTimer::start: Timers cannot have negative timeouts"); qWarning("QBasicTimer::start: negative intervals aren't allowed; the "
return; "interval will be set to 1ms.");
duration = 1ms;
} }
if (Q_UNLIKELY(!eventDispatcher)) { if (Q_UNLIKELY(!eventDispatcher)) {
qWarning("QBasicTimer::start: QBasicTimer can only be used with threads started with QThread"); qWarning("QBasicTimer::start: QBasicTimer can only be used with threads started with QThread");

View File

@ -279,10 +279,18 @@ QBindable<bool> QChronoTimer::bindableSingleShot()
stop() and then start() the timer, and acquire a new id(). stop() and then start() the timer, and acquire a new id().
If the timer is not running, only the interval is changed. If the timer is not running, only the interval is changed.
\include timers-common.qdocinc negative-intervals-not-allowed
\sa singleShot \sa singleShot
*/ */
void QChronoTimer::setInterval(std::chrono::nanoseconds nsec) void QChronoTimer::setInterval(std::chrono::nanoseconds nsec)
{ {
if (nsec < 0ns) {
qWarning("QChronoTimer::setInterval: negative intervals aren't allowed; the "
"interval will be set to 1ms.");
nsec = 1ms;
}
auto *d = d_func(); auto *d = d_func();
d->intervalDuration.removeBindingUnlessInWrapper(); d->intervalDuration.removeBindingUnlessInWrapper();
const bool intervalChanged = nsec != d->intervalDuration.valueBypassingBindings(); const bool intervalChanged = nsec != d->intervalDuration.valueBypassingBindings();

View File

@ -1821,6 +1821,8 @@ void QObjectPrivate::setThreadData_helper(QThreadData *currentData, QThreadData
startTimer(std::chrono::milliseconds{interval}, timerType); startTimer(std::chrono::milliseconds{interval}, timerType);
\endcode \endcode
\include timers-common.qdocinc negative-intervals-not-allowed
\sa timerEvent(), killTimer(), QChronoTimer, QBasicTimer \sa timerEvent(), killTimer(), QChronoTimer, QBasicTimer
*/ */
@ -1843,6 +1845,8 @@ int QObject::startTimer(int interval, Qt::TimerType timerType)
then the timer event occurs once every time there are no more window then the timer event occurs once every time there are no more window
system events to process. system events to process.
\include timers-common.qdocinc negative-intervals-not-allowed
The virtual timerEvent() function is called with the QTimerEvent The virtual timerEvent() function is called with the QTimerEvent
event parameter class when a timer event occurs. Reimplement this event parameter class when a timer event occurs. Reimplement this
function to get timer events. function to get timer events.
@ -1887,9 +1891,10 @@ int QObject::startTimer(std::chrono::nanoseconds interval, Qt::TimerType timerTy
using namespace std::chrono_literals; using namespace std::chrono_literals;
if (Q_UNLIKELY(interval < 0ns)) { if (interval < 0ns) {
qWarning("QObject::startTimer: Timers cannot have negative intervals"); qWarning("QObject::startTimer: negative intervals aren't allowed; the "
return 0; "interval will be set to 1ms.");
interval = 1ms;
} }
auto thisThreadData = d->threadData.loadRelaxed(); auto thisThreadData = d->threadData.loadRelaxed();

View File

@ -233,6 +233,8 @@ void QTimer::start()
timer.start(); timer.start();
\endcode \endcode
\include timers-common.qdocinc negative-intervals-not-allowed
\note Keeping the event loop busy with a zero-timer is bound to \note Keeping the event loop busy with a zero-timer is bound to
cause trouble and highly erratic behavior of the UI. cause trouble and highly erratic behavior of the UI.
*/ */
@ -241,9 +243,40 @@ void QTimer::start(int msec)
start(msec * 1ms); start(msec * 1ms);
} }
static std::chrono::milliseconds
checkInterval(const char *caller, std::chrono::milliseconds interval)
{
if (interval < 0ms) {
qWarning("%s: negative intervals aren't allowed; the interval will be set to 1ms.", caller);
interval = 1ms;
}
return interval;
}
/*!
\since 5.8
\overload
Starts or restarts the timer with a timeout of duration \a interval milliseconds.
\include qtimer.cpp stop-restart-timer
\include qtimer.cpp singleshot-activation
This is equivalent to:
\code
timer.setInterval(interval);
timer.start();
\endcode
\include timers-common.qdocinc negative-intervals-not-allowed
*/
void QTimer::start(std::chrono::milliseconds interval) void QTimer::start(std::chrono::milliseconds interval)
{ {
Q_D(QTimer); Q_D(QTimer);
interval = checkInterval("QTimer::start", interval);
// This could be narrowing as the interval is stored in an `int` QProperty, // This could be narrowing as the interval is stored in an `int` QProperty,
// and the type can't be changed in Qt6. // and the type can't be changed in Qt6.
const int msec = interval.count(); const int msec = interval.count();
@ -370,6 +403,8 @@ void QTimer::singleShotImpl(std::chrono::nanoseconds ns, Qt::TimerType timerType
The \a receiver is the receiving object and the \a member is the The \a receiver is the receiving object and the \a member is the
slot. The time interval is \a msec milliseconds. slot. The time interval is \a msec milliseconds.
\include timers-common.qdocinc negative-intervals-not-allowed
\sa start() \sa start()
*/ */
@ -388,6 +423,8 @@ void QTimer::singleShotImpl(std::chrono::nanoseconds ns, Qt::TimerType timerType
time interval is \a msec milliseconds. The \a timerType affects the time interval is \a msec milliseconds. The \a timerType affects the
accuracy of the timer. accuracy of the timer.
\include timers-common.qdocinc negative-intervals-not-allowed
\sa start() \sa start()
*/ */
@ -395,8 +432,9 @@ void QTimer::singleShot(std::chrono::nanoseconds ns, Qt::TimerType timerType,
const QObject *receiver, const char *member) const QObject *receiver, const char *member)
{ {
if (ns < 0ns) { if (ns < 0ns) {
qWarning("QTimer::singleShot: Timers cannot have negative timeouts"); qWarning("QTimer::singleShot: negative intervals aren't allowed; the "
return; "interval will be set to 1ms.");
ns = 1ms;
} }
if (receiver && member) { if (receiver && member) {
if (ns == 0ns) { if (ns == 0ns) {
@ -440,6 +478,8 @@ void QTimer::singleShot(std::chrono::nanoseconds ns, Qt::TimerType timerType,
The \a interval parameter can be an \c int (interpreted as a millisecond The \a interval parameter can be an \c int (interpreted as a millisecond
count) or a \c std::chrono type that implicitly converts to nanoseconds. count) or a \c std::chrono type that implicitly converts to nanoseconds.
\include timers-common.qdocinc negative-intervals-not-allowed
\note In Qt versions prior to 6.8, the chrono overloads took chrono::milliseconds, \note In Qt versions prior to 6.8, the chrono overloads took chrono::milliseconds,
not chrono::nanoseconds. The compiler will automatically convert for you, not chrono::nanoseconds. The compiler will automatically convert for you,
but the conversion may overflow for extremely large milliseconds counts. but the conversion may overflow for extremely large milliseconds counts.
@ -462,6 +502,8 @@ void QTimer::singleShot(std::chrono::nanoseconds ns, Qt::TimerType timerType,
The \a receiver is the receiving object and the \a member is the slot. The The \a receiver is the receiving object and the \a member is the slot. The
time interval is given in the duration object \a nsec. time interval is given in the duration object \a nsec.
\include timers-common.qdocinc negative-intervals-not-allowed
//! [qtimer-ns-overflow] //! [qtimer-ns-overflow]
\note In Qt versions prior to 6.8, this function took chrono::milliseconds, \note In Qt versions prior to 6.8, this function took chrono::milliseconds,
not chrono::nanoseconds. The compiler will automatically convert for you, not chrono::nanoseconds. The compiler will automatically convert for you,
@ -487,6 +529,9 @@ void QTimer::singleShot(std::chrono::nanoseconds ns, Qt::TimerType timerType,
time interval is given in the duration object \a nsec. The \a timerType affects the time interval is given in the duration object \a nsec. The \a timerType affects the
accuracy of the timer. accuracy of the timer.
\include timers-common.qdocinc negative-intervals-not-allowed
\include qtimer.cpp qtimer-ns-overflow \include qtimer.cpp qtimer-ns-overflow
\sa start() \sa start()
@ -526,24 +571,6 @@ void QTimer::singleShot(std::chrono::nanoseconds ns, Qt::TimerType timerType,
\sa QObject::connect(), timeout() \sa QObject::connect(), timeout()
*/ */
/*!
\fn void QTimer::start(std::chrono::milliseconds msec)
\since 5.8
\overload
Starts or restarts the timer with a timeout of duration \a msec milliseconds.
\include qtimer.cpp stop-restart-timer
\include qtimer.cpp singleshot-activation
This is equivalent to:
\code
timer.setInterval(msec);
timer.start();
\endcode
*/
/*! /*!
\fn std::chrono::milliseconds QTimer::intervalAsDuration() const \fn std::chrono::milliseconds QTimer::intervalAsDuration() const
\since 5.8 \since 5.8
@ -604,6 +631,8 @@ QBindable<bool> QTimer::bindableSingleShot()
stop() and then start() the timer, and acquire a new id(). stop() and then start() the timer, and acquire a new id().
If the timer is not running, only the interval is changed. If the timer is not running, only the interval is changed.
\include timers-common.qdocinc negative-intervals-not-allowed
\sa singleShot \sa singleShot
*/ */
void QTimer::setInterval(int msec) void QTimer::setInterval(int msec)
@ -614,6 +643,9 @@ void QTimer::setInterval(int msec)
void QTimer::setInterval(std::chrono::milliseconds interval) void QTimer::setInterval(std::chrono::milliseconds interval)
{ {
Q_D(QTimer); Q_D(QTimer);
interval = checkInterval("QTimer::setInterval", interval);
// This could be narrowing as the interval is stored in an `int` QProperty, // This could be narrowing as the interval is stored in an `int` QProperty,
// and the type can't be changed in Qt6. // and the type can't be changed in Qt6.
const int msec = interval.count(); const int msec = interval.count();

View File

@ -22,3 +22,11 @@
A disadvantage of using timerEvent() is that some high-level features, A disadvantage of using timerEvent() is that some high-level features,
such as single-shot timers and signals, aren't supported. such as single-shot timers and signals, aren't supported.
//! [q-chrono-timer-alternatives] //! [q-chrono-timer-alternatives]
// \1 is the method argument's name e.g. `\a interval`
//! [negative-intervals-not-allowed]
Starting from Qt 6.10, setting a negative interval will result in a run-time warning
and the value being reset to 1ms. Before Qt 6.10 a Qt Timer would let you set a negative
interval but behave in surprising ways (for example stop the timer if it was running or
not start it at all).
//! [negative-intervals-not-allowed]

View File

@ -1090,14 +1090,15 @@ void tst_QChronoTimer::bindToTimer()
auto ignoreMsg = [] { auto ignoreMsg = [] {
QTest::ignoreMessage(QtWarningMsg, QTest::ignoreMessage(QtWarningMsg,
"QObject::startTimer: Timers cannot have negative intervals"); "QChronoTimer::setInterval: negative intervals aren't allowed; the "
"interval will be set to 1ms.");
}; };
ignoreMsg(); ignoreMsg();
timer.setInterval(-100ms); timer.setInterval(-100ms);
ignoreMsg();
timer.start(); timer.start();
QVERIFY(!active); QVERIFY(active);
QCOMPARE(timer.interval(), 1ms);
timer.setInterval(100ms); timer.setInterval(100ms);
timer.start(); timer.start();
@ -1105,9 +1106,9 @@ void tst_QChronoTimer::bindToTimer()
ignoreMsg(); ignoreMsg();
timer.setInterval(-100ms); timer.setInterval(-100ms);
ignoreMsg();
timer.start(); timer.start();
QVERIFY(!active); QVERIFY(active);
QCOMPARE(timer.interval(), 1ms);
} }
void tst_QChronoTimer::bindTimer() void tst_QChronoTimer::bindTimer()
@ -1196,30 +1197,24 @@ void tst_QChronoTimer::negativeInterval()
auto ignoreMsg = [] { auto ignoreMsg = [] {
QTest::ignoreMessage(QtWarningMsg, QTest::ignoreMessage(QtWarningMsg,
"QObject::startTimer: Timers cannot have negative intervals"); "QChronoTimer::setInterval: negative intervals aren't allowed; the "
"interval will be set to 1ms.");
}; };
ignoreMsg(); ignoreMsg();
// Setting a negative interval does not change the active state.
timer.setInterval(-100ms); timer.setInterval(-100ms);
ignoreMsg();
timer.start(); timer.start();
QVERIFY(!timer.isActive()); QVERIFY(timer.isActive());
QCOMPARE(timer.interval(), 1ms);
// Starting a timer that has a positive interval, the active state is changed
timer.setInterval(100ms); timer.setInterval(100ms);
timer.start(); timer.start();
QVERIFY(timer.isActive()); QVERIFY(timer.isActive());
ignoreMsg(); ignoreMsg();
// Setting a negative interval on an already running timer...
timer.setInterval(-100ms); timer.setInterval(-100ms);
// ... the timer is stopped and the active state is changed QVERIFY(timer.isActive());
QVERIFY(!timer.isActive()); QCOMPARE(timer.interval(), 1ms);
// Calling start on a timer that has a negative interval, does not change the active state
timer.start();
QVERIFY(!timer.isActive());
} }
class OrderHelper : public QObject class OrderHelper : public QObject

View File

@ -39,6 +39,7 @@
#include <math.h> #include <math.h>
using namespace std::chrono_literals;
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
class tst_QObject : public QObject class tst_QObject : public QObject
@ -156,6 +157,7 @@ private slots:
void declarativeData(); void declarativeData();
void asyncCallbackHelper(); void asyncCallbackHelper();
void disconnectQueuedConnection_pendingEventsAreDelivered(); void disconnectQueuedConnection_pendingEventsAreDelivered();
void timerWithNegativeInterval();
}; };
struct QObjectCreatedOnShutdown struct QObjectCreatedOnShutdown
@ -8967,5 +8969,15 @@ void tst_QObject::disconnectQueuedConnection_pendingEventsAreDelivered()
QTRY_COMPARE(receiver.count_slot1, 1); QTRY_COMPARE(receiver.count_slot1, 1);
} }
void tst_QObject::timerWithNegativeInterval()
{
QObject obj;
QTest::ignoreMessage(QtWarningMsg,
"QObject::startTimer: negative intervals aren't allowed; the "
"interval will be set to 1ms.");
int id = obj.startTimer(-100ms);
QCOMPARE_NE(Qt::TimerId{id}, Qt::TimerId::Invalid);
}
QTEST_MAIN(tst_QObject) QTEST_MAIN(tst_QObject)
#include "tst_qobject.moc" #include "tst_qobject.moc"

View File

@ -1307,22 +1307,23 @@ void tst_QTimer::bindToTimer()
timer.stop(); timer.stop();
QVERIFY(!active); QVERIFY(!active);
auto ignoreMsg = [] {
QTest::ignoreMessage(QtWarningMsg,
"QObject::startTimer: Timers cannot have negative intervals");
};
// also test that using negative interval updates the binding correctly // also test that using negative interval updates the binding correctly
timer.start(100); timer.start(100);
QVERIFY(active); QVERIFY(active);
ignoreMsg(); QTest::ignoreMessage(QtWarningMsg,
"QTimer::setInterval: negative intervals aren't allowed; the interval "
"will be set to 1ms.");
timer.setInterval(-100); timer.setInterval(-100);
QVERIFY(!active); QVERIFY(active);
QCOMPARE(timer.intervalAsDuration(), 1ms);
timer.start(100); timer.start(100);
QVERIFY(active); QVERIFY(active);
ignoreMsg(); QTest::ignoreMessage(QtWarningMsg,
"QTimer::start: negative intervals aren't allowed; the interval "
"will be set to 1ms.");
timer.start(-100); timer.start(-100);
QVERIFY(!active); QVERIFY(active);
QCOMPARE(timer.intervalAsDuration(), 1ms);
} }
void tst_QTimer::bindTimer() void tst_QTimer::bindTimer()
@ -1405,33 +1406,32 @@ void tst_QTimer::automatedBindingTests()
void tst_QTimer::negativeInterval() void tst_QTimer::negativeInterval()
{ {
auto ignoreMsg = [] {
QTest::ignoreMessage(QtWarningMsg,
"QObject::startTimer: Timers cannot have negative intervals");
};
QTimer timer; QTimer timer;
// Starting with a negative interval does not change active state. QTest::ignoreMessage(QtWarningMsg,
ignoreMsg(); "QTimer::start: negative intervals aren't allowed; the interval "
"will be set to 1ms.");
timer.start(-100ms); timer.start(-100ms);
QVERIFY(!timer.isActive()); QVERIFY(timer.isActive());
QCOMPARE(timer.intervalAsDuration(), 1ms);
// Updating the interval to a negative value stops the timer and changes
// the active state.
timer.start(100ms); timer.start(100ms);
QVERIFY(timer.isActive()); QVERIFY(timer.isActive());
ignoreMsg(); QTest::ignoreMessage(QtWarningMsg,
"QTimer::setInterval: negative intervals aren't allowed; the interval "
"will be set to 1ms.");
timer.setInterval(-100); timer.setInterval(-100);
QVERIFY(!timer.isActive()); QVERIFY(timer.isActive());
QCOMPARE(timer.intervalAsDuration(), 1ms);
// Starting with a negative interval when already started leads to stop
// and inactive state.
timer.start(100); timer.start(100);
QVERIFY(timer.isActive()); QVERIFY(timer.isActive());
ignoreMsg(); QTest::ignoreMessage(QtWarningMsg,
"QTimer::start: negative intervals aren't allowed; the interval "
"will be set to 1ms.");
timer.start(-100ms); timer.start(-100ms);
QVERIFY(!timer.isActive()); QVERIFY(timer.isActive());
QCOMPARE(timer.intervalAsDuration(), 1ms);
} }
class OrderHelper : public QObject class OrderHelper : public QObject