Qt5:与 lambda 的一次性连接

Posted

技术标签:

【中文标题】Qt5:与 lambda 的一次性连接【英文标题】:Qt5: one-shot connection to lambda 【发布时间】:2019-03-05 10:24:05 【问题描述】:

如何使用 Qt5.12 创建一次性连接(即第一次激活时自动断开的连接)?我正在寻找一个优雅的解决方案,没有过多的冗长,可以清楚地传达含义。

我正在使用

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

只有在没有连接其他信号时才有效。

这篇帖子https://forum.qt.io/post/328402建议

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

即使在存在其他连接的情况下也可以工作,但同样不是很优雅。

SO 有几个关于这个主题的问题,但似乎没有一个解决方案对我有用。例如,https://***.com/a/42989833/761090 使用虚拟对象:

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

产生运行时警告:

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)

模板化解决方案(https://***.com/a/26554206/761090 的第二部分)不能用 c++17 编译。

有更好的建议吗?

编辑:我将此作为请求 Qt 错误跟踪器提交:https://bugreports.qt.io/browse/QTBUG-74547。

【问题讨论】:

【参考方案1】:

我从 OPs 问题中采用了(对我来说)最有希望的方法

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

并考虑如何将其包装成一个函数。实际上,它必须是一个模板函数才能使其可用于任何 Qt 信号:

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;

我在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();

对应的Qt项目:

SOURCES = testQSignalOneShot.cc

QT += widgets

在 Windows 10 上的 VS2017 中测试:

在cygwin64 (X11) 中使用 g++ 测试:

$ 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"

注意:

我必须承认这个解决方案有一点缺陷。如果一次性连接没有“触发”而是断开连接(例如,因为发送者对象被删除),那么QMetaObject::Connection 不会被删除并成为内存泄漏。我思考了一会儿如何在没有一个好主意的情况下解决这个问题。最后,我决定按原样发送。所以,请对它持保留态度——它还没有“准备好生产”。至少,它显示了这个想法。


最后,我解决了内存泄漏的问题。

连接的关键在于它必须传递给内部的“蹦床”lambda。

但是,按值传递连接将确保正确的存储管理,但此时它尚未初始化。

通过引用传递它可以解决这个问题,但这会捕获对局部变量的引用(致命)。

因此,new QMetaObject::Connection 的解决方案似乎是唯一可行的方法,因为指针可以按值传递,但之后可以更新实例。由于使用new 进行分配,应用程序可以控制QMetaObject::Connection 实例的生命周期。

但是如果没有发出信号怎么办。我的解决方案:否则,发件人对象可能负责删除它。这可以在 Qt 中通过将QObject“附加”到另一个来实现(将后者设置为前者的父级)。

基于此,一个改进的解决方案,我将QMetaObject::Connection 存储在派生自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();

对应的Qt项目:

SOURCES = testQSignalOneShot2.cc

QT += widgets

我在 VS2017(Windows 10“本机”)和带有 X11 的 cygwin64 上再次进行了测试。在后者的会话下方:

$ 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.

在这种情况下,我首先按下 One Shot,然后按下 Disconnect。再次测试相反的顺序:

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

在这种情况下,ConnectionWrapper::~ConnectionWrapper() 的输出在我退出应用程序之前没有出现。 (有道理——当main() 的范围离开时,发件人qBtn 被删除。)

【讨论】:

@eudoxos 刚刚添加了答案的更新 - 我找到了解决潜在内存泄漏的方法。【参考方案2】:

有一个更简单的方法:删除接收者来破坏连接。

auto receiver = new QObject(this);
connect(_textFadeOutAnimation, &QPropertyAnimation::finished, receiver, [this, text, receiver]()
    receiver.deleteLater();
);

【讨论】:

【参考方案3】:

前段时间我为单次连接写了一个包装器。似乎工作正常,也许对某人有用。

    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...);
    );
;

示例用法:

auto* senderObj = new SenderObj();
auto* receiverObj = new ReceiverObj();
singleShotConnect(senderObj, &SenderObj:someSignal, receiverObj, &ReceiverObj::someSlot);

【讨论】:

我相信您在函数声明中缺少signal【参考方案4】:

the accepted answer 的小问题(我认为所有其他答案)是它无法处理发送者和接收者在不同线程中的情况。如果接收器很忙(例如处理另一个信号),那么信号可能会在断开之前多次触发。断开信号并不会删除任何已排队的连接。

解决方案是使用两个连接对其进行稍微变体。第一个连接到预期的接收器(并在接收器的线程上运行)。第二个在发送者的线程上运行并断开连接。

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;

与原始版本的另一个细微变化是我使用std::shared_ptr 来确保清除堆分配的连接 - 就像在@Mieszko's answer 中一样。

【讨论】:

以上是关于Qt5:与 lambda 的一次性连接的主要内容,如果未能解决你的问题,请参考以下文章

无服务器函数中的数据库连接缓存代码仅执行一次

模板参数推导与 QT lambda 不匹配

在 qt5 中连接动态创建的按钮

使用 lambda 函数了解 QProcess 信号的行为

在 Qt5 中断开 lambda 函数

Web-Socket