4

How can I create a one-shot connection (i.e. connection which disconnects automatically the first time activated) with Qt5.12? I am looking for an elegant solution, without excessive verbosity, which clearly communicates what is meant.

I am currently using

QObject::connect(instance,Class::signal,this,[this](){
    QObject::disconnect(instance,Class::signal,this,0);
    /* ... */
});

which only works when there are no other signals connected.

This post https://forum.qt.io/post/328402 suggests

QMetaObject::Connection * const connection = new QMetaObject::Connection;
*connection = connect(_textFadeOutAnimation, &QPropertyAnimation::finished, [this, text, connection](){
    QObject::disconnect(*connection);
    delete connection;
});

which works even in presence of other connections but again, is not very elegant.

There are several questions at SO with this topic but non of the solutions seem to work for me. For instance, https://stackoverflow.com/a/42989833/761090 uses dummy object:

QObject *dummy=new QObject(this);
QObject::connect(instance,Class::signal,[dummy](){
    dummy->deleteLater();
});

produces run-time warning:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is ClassInstance(0x561c14ce3a60), parent's thread is QThread(0x561c14e1b050), current thread is QThread(0x561c14c2b530)

The templated solution (second part of https://stackoverflow.com/a/26554206/761090) does not compile with c++17.

Any better suggestions?

EDIT: I filed this as a requestion Qt bug tracker: https://bugreports.qt.io/browse/QTBUG-74547 .

eudoxos
  • 18,545
  • 10
  • 61
  • 110

4 Answers4

4

I took the (to me) most promising approach from OPs question

QMetaObject::Connection * const connection = new QMetaObject::Connection;
*connection = connect(_textFadeOutAnimation, &QPropertyAnimation::finished, [this, text, connection](){
    QObject::disconnect(*connection);
    delete connection;
});

and thought about how to wrap it into a function. Actually, it has to be a template function to make it usable for any Qt signal:

template <typename Sender, typename Emitter, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(
  Sender *pSender, void (Emitter::*pSignal)(Args ...args), Slot slot)
{
  QMetaObject::Connection *pConnection = new QMetaObject::Connection;
  *pConnection
    = QObject::connect(pSender, pSignal,
      [pConnection, slot](Args... args)
      {
        QObject::disconnect(*pConnection);
        delete pConnection;
        slot(args...);
      });
  return *pConnection;
}

I tried this in an MCVE:

#include <functional>

#include <QtWidgets>

template <typename Sender, typename Emitter, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(Sender *pSender, void (Emitter::*pSignal)(Args ...args), Slot slot)
{
  QMetaObject::Connection *pConnection = new QMetaObject::Connection;
  *pConnection
    = QObject::connect(pSender, pSignal,
      [pConnection, slot](Args... args)
      {
        QObject::disconnect(*pConnection);
        delete pConnection;
        slot(args...);
      });
  return *pConnection;
}

template <typename Sender, typename Emitter,
  typename Receiver, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(
  Sender *pSender, void (Emitter::*pSignal)(Args ...args),
  Receiver *pRecv, Slot slot)
{
  QMetaObject::Connection *pConnection = new QMetaObject::Connection;
  *pConnection
    = QObject::connect(pSender, pSignal,
      [pConnection, pRecv, slot](Args... args)
      {
        QObject::disconnect(*pConnection);
        delete pConnection;
        (pRecv->*slot)(args...);
      });
  return *pConnection;
}

void onBtnClicked(bool)
{
  static int i = 0;
  qDebug() << "onBtnClicked() called:" << ++i;
}

struct PushButton: public QPushButton {
  int i = 0;
  using QPushButton::QPushButton;
  virtual ~PushButton() = default;
  void onClicked(bool)
  {
    ++i;
    qDebug() << this << "PushButton::onClicked() called:" << i;
    setText(QString("Clicked %1.").arg(i));
  }
};

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup user interface
  QWidget qWinMain;
  QGridLayout qGrid;
  qGrid.addWidget(new QLabel("Multi Shot"), 0, 1);
  qGrid.addWidget(new QLabel("One Shot"), 0, 2);
  auto addRow
    = [](
      QGridLayout &qGrid, const QString &qText,
      QWidget &qWidgetMShot, QWidget &qWidgetOneShot)
    {
      const int i = qGrid.rowCount();
      qGrid.addWidget(new QLabel(qText), i, 0);
      qGrid.addWidget(&qWidgetMShot, i, 1);
      qGrid.addWidget(&qWidgetOneShot, i, 2);
    };
  QPushButton qBtnMShotFunc("Click me!");
  QPushButton qBtnOneShotFunc("Click me!");
  addRow(qGrid, "Function:", qBtnMShotFunc, qBtnOneShotFunc);
  PushButton qBtnMShotMemFunc("Click me!");
  PushButton qBtnOneShotMemFunc("Click me!");
  addRow(qGrid, "Member Function:", qBtnMShotMemFunc, qBtnOneShotMemFunc);
  QPushButton qBtnMShotLambda("Click me!");
  QPushButton qBtnOneShotLambda("Click me!");
  addRow(qGrid, "Lambda:", qBtnMShotLambda, qBtnOneShotLambda);
  QLineEdit qEditMShot("Edit me!");
  QLineEdit qEditOneShot("Edit me!");
  addRow(qGrid, "Lambda:", qEditMShot, qEditOneShot);
  qWinMain.setLayout(&qGrid);
  qWinMain.show();
  // install signal handlers
  QObject::connect(&qBtnMShotFunc, &QPushButton::clicked,
    &onBtnClicked);
  connectOneShot(&qBtnOneShotFunc, &QPushButton::clicked,
    &onBtnClicked);
  QObject::connect(&qBtnMShotMemFunc, &QPushButton::clicked,
    &qBtnMShotMemFunc, &PushButton::onClicked);
  connectOneShot(&qBtnOneShotMemFunc, &QPushButton::clicked,
    &qBtnOneShotMemFunc, &PushButton::onClicked);
  QObject::connect(&qBtnMShotLambda, &QPushButton::clicked,
    [&](bool) {
      qDebug() << "[&](bool) qBtnMShotLambda called.";
      static int i = 0;
      qBtnMShotLambda.setText(QString("Clicked %1.").arg(++i));
    });
  connectOneShot(&qBtnOneShotLambda, &QPushButton::clicked,
    [&](bool) {
      qDebug() << "[&](bool) for qBtnOneShotLambda called.";
      static int i = 0;
      qBtnOneShotLambda.setText(QString("Clicked %1.").arg(++i));
    });
  QObject::connect(&qEditMShot, &QLineEdit::editingFinished,
    [&]() {
      qDebug() << "[&]() for qEditMShot called. Input:" << qEditMShot.text();
      qEditMShot.setText("Well done.");
    });
  connectOneShot(&qEditOneShot, &QLineEdit::editingFinished,
    [&]() {
      qDebug() << "[&]() for qEditOneShot called. Input:" << qEditOneShot.text();
      qEditOneShot.setText("No more input accepted.");
      qEditOneShot.setEnabled(false);
    });
  // run
  return app.exec();
}

The corresponding Qt project:

SOURCES = testQSignalOneShot.cc

QT += widgets

Tested in VS2017 on Windows 10:

Snapshot of testQSignalOneShot in Windows 10

Tested in with g++ in cygwin64 (X11):

$ qmake-qt5 testQSignalOneShot.pro

$ make && ./testQSignalOneShot
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o testQSignalOneShot.o testQSignalOneShot.cc
g++  -o testQSignalOneShot.exe testQSignalOneShot.o   -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread 
Qt Version: 5.9.4
onBtnClicked() called: 1
onBtnClicked() called: 2
onBtnClicked() called: 3
QPushButton(0xffffcb60) PushButton::onClicked() called: 1
QPushButton(0xffffcb60) PushButton::onClicked() called: 2
QPushButton(0xffffcba0) PushButton::onClicked() called: 1
[&](bool) qBtnMShotLambda called.
[&](bool) qBtnMShotLambda called.
[&](bool) for qBtnOneShotLambda called.
[&]() for qEditMShot called. Input: "abc123"
[&]() for qEditMShot called. Input: "def456"
[&]() for qEditMShot called. Input: "Well done."
[&]() for qEditOneShot called. Input: "abc456"

Snapshot of testQSignalOneShot in cygwin64 & X11

Note:

I must admit that this solution has a little flaw. If a one-shot-connection is not "fired" but disconnected (e.g. because sender object is deleted) then the QMetaObject::Connection is not deleted and becomes a memory leak. I pondered a while how to solve this without having a good idea. Finally, I decided to sent this as is. So, please, take this with a grain of salt – it's not yet "production-ready". At least, it shows the idea.


Finally, I solved the issue with the memory-leak.

The crux with the connection is that it has to be passed to the inner "trampoline" lambda.

However, passing the connection by value would ensure proper storage management but at this point it's not yet initialized.

Passing it by reference could solve this issue but this would capture a reference to a local variable (fatal).

Hence, the solution with new QMetaObject::Connection seems the only working as the pointer can be passed by value but the instance can be updated afterwards. Due to allocation with new, the application has control over life-time of QMetaObject::Connection instance.

But what if the signal is not emitted. My solution: Otherwise, the sender object might be responsible to delete it. This can be achieved in Qt by "attaching" a QObject to another (setting the latter as parent of the former).

Based on this, an improved solution, where I stored the QMetaObject::Connection in a wrapper derived from QObject:

#include <functional>

#include <QtWidgets>

struct ConnectionWrapper: QObject {
  ConnectionWrapper(QObject *pQParent): QObject(pQParent) { }
  ConnectionWrapper(const ConnectionWrapper&) = delete;
  ConnectionWrapper& operator=(const ConnectionWrapper&) = delete;
  virtual ~ConnectionWrapper()
  {
    qDebug() << "ConnectionWrapper::~ConnectionWrapper()";
  }
  QMetaObject::Connection connection;
};

template <typename Sender, typename Emitter, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(Sender *pSender, void (Emitter::*pSignal)(Args ...args), Slot slot)
{
  ConnectionWrapper *pConn = new ConnectionWrapper(pSender);
  pConn->connection
    = QObject::connect(pSender, pSignal,
      [pConn, slot](Args... args)
      {
        QObject::disconnect(pConn->connection);
        delete pConn;
        slot(args...);
      });
  return pConn->connection;
}

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup user interface
  QWidget qWinMain;
  QGridLayout qGrid;
  auto addRow
    = [](QGridLayout &qGrid, const QString &qText, QWidget &qWidgetMShot, QWidget &qWidgetOneShot)
    {
      const int i = qGrid.rowCount();
      qGrid.addWidget(new QLabel(qText), i, 0);
      qGrid.addWidget(&qWidgetMShot, i, 1);
      qGrid.addWidget(&qWidgetOneShot, i, 2);
    };
  QPushButton qBtn("One Shot");
  QPushButton qBtnDisconnect("Disconnect");
  addRow(qGrid, "Disconnect Test:", qBtnDisconnect, qBtn);
  qWinMain.setLayout(&qGrid);
  qWinMain.show();
  // install signal handlers
  QMetaObject::Connection connectionBtn
    = connectOneShot(&qBtn, &QPushButton::clicked,
      [&](bool) {
        qDebug() << "[&](bool) for qBtn called.";
        static int i = 0;
        qBtn.setText(QString("Clicked %1.").arg(++i));
      });
  QObject::connect(&qBtnDisconnect, &QPushButton::clicked,
    [&](bool) {
      QObject::disconnect(connectionBtn);
      qDebug() << "qBtn disconnected.";
    });
  // run
  return app.exec();
}

The corresponding Qt project:

SOURCES = testQSignalOneShot2.cc

QT += widgets

I tested again in VS2017 (Windows 10 "native") and on cygwin64 with X11. Below the session in the latter:

$ qmake-qt5 testQSignalOneShot2.pro

$ make && ./testQSignalOneShot2
/usr/bin/qmake-qt5 -o Makefile testQSignalOneShot2.pro
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o testQSignalOneShot2.o testQSignalOneShot2.cc
g++  -o testQSignalOneShot2.exe testQSignalOneShot2.o   -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread 
Qt Version: 5.9.4
ConnectionWrapper::~ConnectionWrapper()
[&](bool) for qBtn called.
qBtn disconnected.

In this case, I first pressed One Shot and second Disconnect. Tested again for the opposite order:

$ make && ./testQSignalOneShot2
make: Nothing to be done for 'first'.
Qt Version: 5.9.4
qBtn disconnected.
ConnectionWrapper::~ConnectionWrapper()

In this case, the output of ConnectionWrapper::~ConnectionWrapper() didn't appear before I exited application. (Makes sense – the sender qBtn was deleted when scope of main() was left.)

Snapshot of testQSignalOneShot2 in cygwin64 & X11

Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56
3

There is an easier way: deleting the receiver to destroy the connection.

auto receiver = new QObject(this);
connect(_textFadeOutAnimation, &QPropertyAnimation::finished, receiver, [this, text, receiver](){
    receiver.deleteLater();
});
Flavio Tordini
  • 535
  • 4
  • 11
1

some time ago I wrote a wrapper for single shot connect. Seems to work fine, maybe it will be useful for somebody.

    template <typename SenderFunc, typename ReceiverFunc>
void singleShotConnect(const typename QtPrivate::FunctionPointer<SenderFunc>::Object* sender, SenderFunc signal, const QObject* context, ReceiverFunc receiverFunc){
    auto connection = std::make_shared<QMetaObject::Connection>();
    connection = QObject::connect(sender, signal, context, [connection, receiverFunc](auto ...args){
        QObject::disconnect(*connection);
        std::invoke(receiverFunc, args...);
    });
};

Example usagee:

auto* senderObj = new SenderObj();
auto* receiverObj = new ReceiverObj();
singleShotConnect(senderObj, &SenderObj:someSignal, receiverObj, &ReceiverObj::someSlot);
Mieszko
  • 69
  • 7
0

The slight issue with the accepted answer (and I think all the other answers) is that it doesn't handle the case where the sender and receiver are in different threads. If the receiver is busy (for example handling another signal) then it's possible for the signal to fire multiple times before it is disconnected. Disconnecting the signal doesn't remove any connections that are already queued.

The solution is to do a slightly variant of it with two connections. The first connects to the intended receiver (and runs on the receiver's thread). The second runs on the sender's thread and does the disconnection(s).

template <typename SenderT, typename Func1T, typename ...Args>
QMetaObject::Connection connectOneShot(SenderT* sender, const Func1T& signal, Args&&...args) {
    auto connections = std::make_shared<std::pair<QMetaObject::Connection, QMetaObject::Connection>>();
    // The order of connections is important here - the target slot must be connected first (so that it's called before the disconnection has happened)
    connections->first = QObject::connect(sender, signal, std::forward<Args>(args)...);
    // This gets run as a direct connection on the sender's thread, thus ensuring that's it's called immediately (before the signal has the change to repeat)
    connections->second = QObject::connect(sender, signal, sender, [connections](){
        QObject::disconnect(connections->first);
        QObject::disconnect(connections->second);
    });
    return connections->first;
}

Another slight change from the original is that I use a std::shared_ptr to ensure that the heap-allocated connections are cleared up - like in @Mieszko's answer.

DavidW
  • 29,336
  • 6
  • 55
  • 86