Change cleanup mechanism for orphaned connections

Put all connections that get disconnected into a singly
linked orphaned list.

Whenever the refcount on the connectionData drops down to
one, this list can safely be cleared, even with the planned
removal of locking in activate().

Use an id integer in the connection to acoid activating newly
added connections.

Fixes: QTBUG-72649
Change-Id: Ide3d116ae7fc9ca497598c1c2b71d43b4339c92d
Reviewed-by: Olivier Goffart (Woboq GmbH) <ogoffart@woboq.com>
This commit is contained in:
Lars Knoll 2019-01-09 19:12:46 +01:00
parent a2fda801cc
commit 6e0b5dadc7
4 changed files with 167 additions and 136 deletions

View File

@ -232,7 +232,7 @@ struct QMetaObjectPrivate
const QMetaObject *smeta, const QMetaObject *smeta,
const QObject *receiver, int method_index, void **slot, const QObject *receiver, int method_index, void **slot,
DisconnectType = DisconnectAll); DisconnectType = DisconnectAll);
static inline bool disconnectHelper(QObjectPrivate::Connection *c, static inline bool disconnectHelper(QObjectPrivate::ConnectionData *connections, int signalIndex,
const QObject *receiver, int method_index, void **slot, const QObject *receiver, int method_index, void **slot,
QBasicMutex *senderMutex, DisconnectType = DisconnectAll); QBasicMutex *senderMutex, DisconnectType = DisconnectAll);
#endif #endif

View File

@ -1,6 +1,6 @@
/**************************************************************************** /****************************************************************************
** **
** Copyright (C) 2016 The Qt Company Ltd. ** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2016 Intel Corporation. ** Copyright (C) 2016 Intel Corporation.
** Copyright (C) 2013 Olivier Goffart <ogoffart@woboq.com> ** Copyright (C) 2013 Olivier Goffart <ogoffart@woboq.com>
** Contact: https://www.qt.io/licensing/ ** Contact: https://www.qt.io/licensing/
@ -332,14 +332,15 @@ void QObjectPrivate::addConnection(int signal, Connection *c)
ConnectionList &connectionList = cd->connectionsForSignal(signal); ConnectionList &connectionList = cd->connectionsForSignal(signal);
if (connectionList.last) { if (connectionList.last) {
Q_ASSERT(connectionList.last->receiver);
connectionList.last->nextConnectionList = c; connectionList.last->nextConnectionList = c;
} else { } else {
connectionList.first = c; connectionList.first = c;
} }
c->id = ++cd->currentConnectionId;
c->prevConnectionList = connectionList.last;
connectionList.last = c; connectionList.last = c;
cleanConnectionLists();
QObjectPrivate *rd = QObjectPrivate::get(c->receiver); QObjectPrivate *rd = QObjectPrivate::get(c->receiver);
rd->ensureConnectionData(); rd->ensureConnectionData();
@ -350,39 +351,27 @@ void QObjectPrivate::addConnection(int signal, Connection *c)
c->next->prev = &c->next; c->next->prev = &c->next;
} }
void QObjectPrivate::cleanConnectionLists() void QObjectPrivate::ConnectionData::cleanOrphanedConnectionsImpl(QObject *sender)
{ {
ConnectionData *cd = connections.load(); Connection *c = nullptr;
if (cd->dirty && cd->ref == 1) { {
// remove broken connections QBasicMutexLocker l(signalSlotLock(sender));
for (int signal = -1; signal < cd->signalVector.count(); ++signal) { if (ref > 1)
ConnectionList &connectionList = cd->connectionsForSignal(signal); return;
// Set to the last entry in the connection list that was *not* // Since ref == 1, no activate() is in process since we locked the mutex. That implies,
// deleted. This is needed to update the list's last pointer // that nothing can reference the orphaned connection objects anymore and they can
// at the end of the cleanup. // be safely deleted
QObjectPrivate::Connection *last = 0; c = orphaned.load();
orphaned.store(nullptr);
QObjectPrivate::Connection **prev = &connectionList.first; }
QObjectPrivate::Connection *c = *prev; while (c) {
while (c) { Q_ASSERT(!c->receiver);
if (c->receiver) { Q_ASSERT(!c->prev);
last = c; QObjectPrivate::Connection *next = c->nextInOrphanList;
prev = &c->nextConnectionList; c->freeSlotObject();
c = *prev; c->deref();
} else { c = next;
QObjectPrivate::Connection *next = c->nextConnectionList;
*prev = next;
c->deref();
c = next;
}
}
// Correct the connection list's last pointer.
// As conectionList.last could equal last, this could be a noop
connectionList.last = last;
}
cd->dirty = false;
} }
} }
@ -905,47 +894,22 @@ QObject::~QObject()
QObjectPrivate::ConnectionList &connectionList = cd->connectionsForSignal(signal); QObjectPrivate::ConnectionList &connectionList = cd->connectionsForSignal(signal);
while (QObjectPrivate::Connection *c = connectionList.first) { while (QObjectPrivate::Connection *c = connectionList.first) {
if (!c->receiver) { Q_ASSERT(c->receiver);
connectionList.first = c->nextConnectionList;
c->deref();
continue;
}
QBasicMutex *m = signalSlotLock(c->receiver); QBasicMutex *m = signalSlotLock(c->receiver);
bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m); bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m);
if (c->receiver) { if (c->receiver) {
*c->prev = c->next; cd->removeConnection(c);
if (c->next) c->next->prev = c->prev; Q_ASSERT(connectionList.first != c);
} }
c->receiver = 0;
if (needToUnlock) if (needToUnlock)
m->unlock(); m->unlock();
connectionList.first = c->nextConnectionList;
// The destroy operation must happen outside the lock
if (c->isSlotObject) {
c->isSlotObject = false;
locker.unlock();
c->slotObj->destroyIfLastRef();
locker.relock();
}
c->deref();
} }
} }
/* Disconnect all senders: /* Disconnect all senders:
* This loop basically just does
* for (node = d->senders; node; node = node->next) { ... }
*
* We need to temporarily unlock the receiver mutex to destroy the functors or to lock the
* sender's mutex. And when the mutex is released, node->next might be destroyed by another
* thread. That's why we set node->prev to &node, that way, if node is destroyed, node will
* be updated.
*/ */
QObjectPrivate::Connection *node = cd->senders; while (QObjectPrivate::Connection *node = cd->senders) {
while (node) {
Q_ASSERT(node->receiver); Q_ASSERT(node->receiver);
QObject *sender = node->sender; QObject *sender = node->sender;
// Send disconnectNotify before removing the connection from sender's connection list. // Send disconnectNotify before removing the connection from sender's connection list.
@ -953,19 +917,17 @@ QObject::~QObject()
// and not finish until we release it. // and not finish until we release it.
sender->disconnectNotify(QMetaObjectPrivate::signal(sender->metaObject(), node->signal_index)); sender->disconnectNotify(QMetaObjectPrivate::signal(sender->metaObject(), node->signal_index));
QBasicMutex *m = signalSlotLock(sender); QBasicMutex *m = signalSlotLock(sender);
node->prev = &node;
bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m); bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m);
//the node has maybe been removed while the mutex was unlocked in relock? //the node has maybe been removed while the mutex was unlocked in relock?
if (!node || node->sender != sender) { if (node != cd->senders) {
// We hold the wrong mutex // We hold the wrong mutex
Q_ASSERT(needToUnlock); Q_ASSERT(needToUnlock);
m->unlock(); m->unlock();
continue; continue;
} }
node->receiver = 0;
QObjectPrivate::ConnectionData *senderData = sender->d_func()->connections.load(); QObjectPrivate::ConnectionData *senderData = sender->d_func()->connections.load();
if (senderData) Q_ASSERT(senderData);
senderData->dirty = true;
QtPrivate::QSlotObjectBase *slotObj = nullptr; QtPrivate::QSlotObjectBase *slotObj = nullptr;
if (node->isSlotObject) { if (node->isSlotObject) {
@ -973,20 +935,20 @@ QObject::~QObject()
node->isSlotObject = false; node->isSlotObject = false;
} }
node = node->next; senderData->removeConnection(node);
if (needToUnlock) if (needToUnlock)
m->unlock(); m->unlock();
if (slotObj) { if (slotObj) {
if (node)
node->prev = &node;
locker.unlock(); locker.unlock();
slotObj->destroyIfLastRef(); slotObj->destroyIfLastRef();
locker.relock(); locker.relock();
} }
} }
cd->objectDeleted = true; // invalidate all connections on the object and make sure
// activate() will skip them
cd->currentConnectionId.store(0);
} }
if (cd && !cd->ref.deref()) if (cd && !cd->ref.deref())
delete cd; delete cd;
@ -3333,16 +3295,19 @@ bool QMetaObject::disconnectOne(const QObject *sender, int signal_index,
\internal \internal
Helper function to remove the connection from the senders list and setting the receivers to 0 Helper function to remove the connection from the senders list and setting the receivers to 0
*/ */
bool QMetaObjectPrivate::disconnectHelper(QObjectPrivate::Connection *c, bool QMetaObjectPrivate::disconnectHelper(QObjectPrivate::ConnectionData *connections, int signalIndex,
const QObject *receiver, int method_index, void **slot, const QObject *receiver, int method_index, void **slot,
QBasicMutex *senderMutex, DisconnectType disconnectType) QBasicMutex *senderMutex, DisconnectType disconnectType)
{ {
bool success = false; bool success = false;
auto &connectionList = connections->connectionsForSignal(signalIndex);
auto *c = connectionList.first;
while (c) { while (c) {
if (c->receiver if (c->receiver
&& (receiver == 0 || (c->receiver == receiver && (receiver == nullptr || (c->receiver == receiver
&& (method_index < 0 || (!c->isSlotObject && c->method() == method_index)) && (method_index < 0 || (!c->isSlotObject && c->method() == method_index))
&& (slot == 0 || (c->isSlotObject && c->slotObj->compare(slot)))))) { && (slot == nullptr || (c->isSlotObject && c->slotObj->compare(slot)))))) {
bool needToUnlock = false; bool needToUnlock = false;
QBasicMutex *receiverMutex = nullptr; QBasicMutex *receiverMutex = nullptr;
if (c->receiver) { if (c->receiver) {
@ -3350,24 +3315,12 @@ bool QMetaObjectPrivate::disconnectHelper(QObjectPrivate::Connection *c,
// need to relock this receiver and sender in the correct order // need to relock this receiver and sender in the correct order
needToUnlock = QOrderedMutexLocker::relock(senderMutex, receiverMutex); needToUnlock = QOrderedMutexLocker::relock(senderMutex, receiverMutex);
} }
if (c->receiver) { if (c->receiver)
*c->prev = c->next; connections->removeConnection(c);
if (c->next)
c->next->prev = c->prev;
}
if (needToUnlock) if (needToUnlock)
receiverMutex->unlock(); receiverMutex->unlock();
c->receiver = 0;
if (c->isSlotObject) {
c->isSlotObject = false;
senderMutex->unlock();
c->slotObj->destroyIfLastRef();
senderMutex->lock();
}
success = true; success = true;
if (disconnectType == DisconnectOne) if (disconnectType == DisconnectOne)
@ -3407,23 +3360,19 @@ bool QMetaObjectPrivate::disconnect(const QObject *sender,
if (signal_index < 0) { if (signal_index < 0) {
// remove from all connection lists // remove from all connection lists
for (int sig_index = -1; sig_index < scd->signalVector.count(); ++sig_index) { for (int sig_index = -1; sig_index < scd->signalVector.count(); ++sig_index) {
QObjectPrivate::Connection *c = scd->connectionsForSignal(sig_index).first; if (disconnectHelper(connections.data(), sig_index, receiver, method_index, slot, senderMutex, disconnectType))
if (disconnectHelper(c, receiver, method_index, slot, senderMutex, disconnectType)) {
success = true; success = true;
scd->dirty = true;
}
} }
} else if (signal_index < scd->signalVector.count()) { } else if (signal_index < scd->signalVector.count()) {
QObjectPrivate::Connection *c = scd->signalVector.at(signal_index).first; if (disconnectHelper(connections.data(), signal_index, receiver, method_index, slot, senderMutex, disconnectType))
if (disconnectHelper(c, receiver, method_index, slot, senderMutex, disconnectType)) {
success = true; success = true;
scd->dirty = true;
}
} }
} }
locker.unlock(); locker.unlock();
if (success) { if (success) {
scd->cleanOrphanedConnections(s);
QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index); QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index);
if (smethod.isValid()) if (smethod.isValid())
s->disconnectNotify(smethod); s->disconnectNotify(smethod);
@ -3630,6 +3579,7 @@ void doActivate(QObject *sender, int signal_index, void **argv)
signal_spy_set->signal_begin_callback(sender, signal_index, argv); signal_spy_set->signal_begin_callback(sender, signal_index, argv);
Q_TRACE(QMetaObject_activate_begin_signal, sender, signal_index); Q_TRACE(QMetaObject_activate_begin_signal, sender, signal_index);
bool senderDeleted = false;
{ {
QBasicMutexLocker locker(signalSlotLock(sender)); QBasicMutexLocker locker(signalSlotLock(sender));
Q_ASSERT(sp->connections); Q_ASSERT(sp->connections);
@ -3643,12 +3593,13 @@ void doActivate(QObject *sender, int signal_index, void **argv)
Qt::HANDLE currentThreadId = QThread::currentThreadId(); Qt::HANDLE currentThreadId = QThread::currentThreadId();
// We need to check against the highest connection id to ensure that signals added
// during the signal emission are not emitted in this emission.
uint highestConnectionId = connections->currentConnectionId.load();
do { do {
QObjectPrivate::Connection *c = list->first; QObjectPrivate::Connection *c = list->first;
if (!c) continue; if (!c)
// We need to check against last here to ensure that signals added continue;
// during the signal emission are not emitted in this emission.
QObjectPrivate::Connection *last = list->last;
do { do {
if (!c->receiver) if (!c->receiver)
@ -3732,18 +3683,17 @@ void doActivate(QObject *sender, int signal_index, void **argv)
locker.relock(); locker.relock();
} }
} while ((c = c->nextConnectionList) != 0 && c->id <= highestConnectionId);
if (connections->objectDeleted)
break;
} while (c != last && (c = c->nextConnectionList) != 0);
if (connections->objectDeleted)
break;
} while (list != &connections->allsignals && } while (list != &connections->allsignals &&
//start over for all signals; //start over for all signals;
((list = &connections->allsignals), true)); ((list = &connections->allsignals), true));
if (connections->currentConnectionId.load() == 0)
senderDeleted = true;
} }
if (!senderDeleted)
sp->connections.load()->cleanOrphanedConnections(sender);
if (callbacks_enabled && signal_spy_set->signal_end_callback != nullptr) if (callbacks_enabled && signal_spy_set->signal_end_callback != nullptr)
signal_spy_set->signal_end_callback(sender, signal_index); signal_spy_set->signal_end_callback(sender, signal_index);
@ -4859,30 +4809,25 @@ bool QObject::disconnect(const QMetaObject::Connection &connection)
{ {
QObjectPrivate::Connection *c = static_cast<QObjectPrivate::Connection *>(connection.d_ptr); QObjectPrivate::Connection *c = static_cast<QObjectPrivate::Connection *>(connection.d_ptr);
if (!c || !c->receiver) if (!c)
return false; return false;
QBasicMutex *senderMutex = signalSlotLock(c->sender); QBasicMutex *senderMutex = signalSlotLock(c->sender);
QBasicMutex *receiverMutex = signalSlotLock(c->receiver); QBasicMutex *receiverMutex = signalSlotLock(c->receiver);
QObjectPrivate::ConnectionData *connections;
{ {
QOrderedMutexLocker locker(senderMutex, receiverMutex); QOrderedMutexLocker locker(senderMutex, receiverMutex);
QObjectPrivate::ConnectionData *connections = QObjectPrivate::get(c->sender)->connections.load(); if (!c->receiver)
return false;
connections = QObjectPrivate::get(c->sender)->connections.load();
Q_ASSERT(connections); Q_ASSERT(connections);
connections->dirty = true; connections->removeConnection(c);
*c->prev = c->next;
if (c->next)
c->next->prev = c->prev;
c->receiver = nullptr;
} }
// destroy the QSlotObject, if possible connections->cleanOrphanedConnections(c->sender);
if (c->isSlotObject) {
c->slotObj->destroyIfLastRef();
c->isSlotObject = false;
}
c->sender->disconnectNotify(QMetaObjectPrivate::signal(c->sender->metaObject(), c->sender->disconnectNotify(QMetaObjectPrivate::signal(c->sender->metaObject(),
c->signal_index)); c->signal_index));

View File

@ -1,6 +1,6 @@
/**************************************************************************** /****************************************************************************
** **
** Copyright (C) 2017 The Qt Company Ltd. ** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2013 Olivier Goffart <ogoffart@woboq.com> ** Copyright (C) 2013 Olivier Goffart <ogoffart@woboq.com>
** Contact: https://www.qt.io/licensing/ ** Contact: https://www.qt.io/licensing/
** **
@ -126,22 +126,29 @@ public:
typedef void (*StaticMetaCallFunction)(QObject *, QMetaObject::Call, int, void **); typedef void (*StaticMetaCallFunction)(QObject *, QMetaObject::Call, int, void **);
struct Connection struct Connection
{ {
union {
// linked list of orphaned connections that need cleaning up
Connection *nextInOrphanList;
// linked list of connections connected to slots in this object
Connection *next;
};
Connection **prev;
// linked list of connections connected to signals in this object
Connection *nextConnectionList;
Connection *prevConnectionList;
QObject *sender; QObject *sender;
QObject *receiver; QObject *receiver;
union { union {
StaticMetaCallFunction callFunction; StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase *slotObj; QtPrivate::QSlotObjectBase *slotObj;
}; };
// The next pointer for the singly-linked ConnectionList
Connection *nextConnectionList;
//senders linked list
Connection *next;
Connection **prev;
QAtomicPointer<const int> argumentTypes; QAtomicPointer<const int> argumentTypes;
QAtomicInt ref_; QAtomicInt ref_;
uint id = 0;
ushort method_offset; ushort method_offset;
ushort method_relative; ushort method_relative;
uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex()) int signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
ushort isSlotObject : 1; ushort isSlotObject : 1;
ushort ownArgumentTypes : 1; ushort ownArgumentTypes : 1;
@ -151,9 +158,17 @@ public:
~Connection(); ~Connection();
int method() const { Q_ASSERT(!isSlotObject); return method_offset + method_relative; } int method() const { Q_ASSERT(!isSlotObject); return method_offset + method_relative; }
void ref() { ref_.ref(); } void ref() { ref_.ref(); }
void freeSlotObject()
{
if (isSlotObject) {
slotObj->destroyIfLastRef();
isSlotObject = false;
}
}
void deref() { void deref() {
if (!ref_.deref()) { if (!ref_.deref()) {
Q_ASSERT(!receiver); Q_ASSERT(!receiver);
Q_ASSERT(!isSlotObject);
delete this; delete this;
} }
} }
@ -210,7 +225,9 @@ public:
linked list. linked list.
*/ */
struct ConnectionData { struct ConnectionData {
bool objectDeleted = false; //the QObject owner of this vector has been destroyed while the vector was inUse // the id below is used to avoid activating new connections. When the object gets
// deleted it's set to 0, so that signal emission stops
QAtomicInteger<uint> currentConnectionId;
struct Ref { struct Ref {
int _ref = 0; int _ref = 0;
void ref() { ++_ref; } void ref() { ++_ref; }
@ -219,11 +236,84 @@ public:
}; };
Ref ref; Ref ref;
bool dirty = false; //some Connection have been disconnected (their receiver is 0) but not removed from the list yet
ConnectionList allsignals; ConnectionList allsignals;
QVector<ConnectionList> signalVector; QVector<ConnectionList> signalVector;
Connection *senders = nullptr; Connection *senders = nullptr;
Sender *currentSender = nullptr; // object currently activating the object Sender *currentSender = nullptr; // object currently activating the object
QAtomicPointer<Connection> orphaned;
~ConnectionData()
{
Connection *c = orphaned.load();
while (c) {
Q_ASSERT(!c->receiver);
QObjectPrivate::Connection *next = c->nextInOrphanList;
c->freeSlotObject();
c->deref();
c = next;
}
}
// must be called on the senders connection data
// assumes the senders and receivers lock are held
void removeConnection(Connection *c)
{
Q_ASSERT(c->receiver);
ConnectionList &connections = connectionsForSignal(c->signal_index);
c->receiver = nullptr;
#ifndef QT_NO_DEBUG
bool found = false;
for (Connection *cc = connections.first; cc; cc = cc->nextConnectionList) {
if (cc == c) {
found = true;
break;
}
}
Q_ASSERT(found);
#endif
// remove from the senders linked list
*c->prev = c->next;
if (c->next)
c->next->prev = c->prev;
c->prev = nullptr;
if (connections.first == c)
connections.first = c->nextConnectionList;
if (connections.last == c)
connections.last = c->prevConnectionList;
// keep c->nextConnectionList intact, as it might still get accessed by activate
if (c->nextConnectionList)
c->nextConnectionList->prevConnectionList = c->prevConnectionList;
if (c->prevConnectionList)
c->prevConnectionList->nextConnectionList = c->nextConnectionList;
c->prevConnectionList = nullptr;
Q_ASSERT(c != orphaned.load());
// add c to orphanedConnections
c->nextInOrphanList = orphaned.load();
orphaned.store(c);
#ifndef QT_NO_DEBUG
found = false;
for (Connection *cc = connections.first; cc; cc = cc->nextConnectionList) {
if (cc == c) {
found = true;
break;
}
}
Q_ASSERT(!found);
#endif
}
void cleanOrphanedConnections(QObject *sender)
{
if (orphaned.load() && ref == 1)
cleanOrphanedConnectionsImpl(sender);
}
void cleanOrphanedConnectionsImpl(QObject *sender);
ConnectionList &connectionsForSignal(int signal) ConnectionList &connectionsForSignal(int signal)
{ {
@ -245,7 +335,6 @@ public:
QObjectList senderList() const; QObjectList senderList() const;
void addConnection(int signal, Connection *c); void addConnection(int signal, Connection *c);
void cleanConnectionLists();
static QObjectPrivate *get(QObject *o) { static QObjectPrivate *get(QObject *o) {
return o->d_func(); return o->d_func();

View File

@ -3411,12 +3411,11 @@ void tst_QObject::disconnectSelfInSlotAndDeleteAfterEmit()
void tst_QObject::dumpObjectInfo() void tst_QObject::dumpObjectInfo()
{ {
QObject a, b; QObject a, b;
QObject::connect(&a, SIGNAL(destroyed(QObject*)), &b, SLOT(deleteLater())); QObject::connect(&a, &QObject::destroyed, &b, &QObject::deleteLater);
a.disconnect(&b);
QTest::ignoreMessage(QtDebugMsg, "OBJECT QObject::unnamed"); QTest::ignoreMessage(QtDebugMsg, "OBJECT QObject::unnamed");
QTest::ignoreMessage(QtDebugMsg, " SIGNALS OUT"); QTest::ignoreMessage(QtDebugMsg, " SIGNALS OUT");
QTest::ignoreMessage(QtDebugMsg, " signal: destroyed(QObject*)"); QTest::ignoreMessage(QtDebugMsg, " signal: destroyed(QObject*)");
QTest::ignoreMessage(QtDebugMsg, " <Disconnected receiver>"); QTest::ignoreMessage(QtDebugMsg, " <functor or function pointer>");
QTest::ignoreMessage(QtDebugMsg, " SIGNALS IN"); QTest::ignoreMessage(QtDebugMsg, " SIGNALS IN");
QTest::ignoreMessage(QtDebugMsg, " <None>"); QTest::ignoreMessage(QtDebugMsg, " <None>");
a.dumpObjectInfo(); // should not crash a.dumpObjectInfo(); // should not crash
@ -7575,8 +7574,6 @@ void tst_QObject::functorReferencesConnection()
// top-level + the one in the 3 others lambdas // top-level + the one in the 3 others lambdas
QCOMPARE(countedStructObjectsCount, 4); QCOMPARE(countedStructObjectsCount, 4);
QObject::disconnect(*c2); QObject::disconnect(*c2);
// the one in the c2's lambda is gone
QCOMPARE(countedStructObjectsCount, 3);
slot1Called++; slot1Called++;
}); });
connect(&obj, &GetSenderObject::aSignal, [] {}); // just a dummy signal to fill the connection list connect(&obj, &GetSenderObject::aSignal, [] {}); // just a dummy signal to fill the connection list