堆栈对象Qt信号和参数作为参考

stack object Qt signal and parameter as reference

本文关键字:参考 参数 对象 Qt 信号 堆栈      更新时间:2023-10-16

我可以有一个"悬垂引用"与以下代码(在一个最终的插槽连接到myQtSignal)?

class Test : public QObject
{
    Q_OBJECT
signals:
    void myQtSignal(const FooObject& obj);
public:
    void sendSignal(const FooObject& fooStackObject)
    {
        emit  myQtSignal(fooStackObject);
    }
};
void f()
{
    FooObject fooStackObject;
    Test t;
    t.sendSignal(fooStackObject);
}
int main()
{
    f();
    std::cin.ignore();
    return 0;
}

特别是当emit和slot不在同一个线程中执行时。

更新20 - 4月- 2015

最初我认为传递对堆栈分配对象的引用等同于传递该对象的地址。因此,在没有包装器来存储拷贝(或共享指针)时,排队槽连接可能会使用坏数据。

但是@BenjaminT和@cgmb引起了我的注意,Qt实际上对const引用参数有特殊处理。它将调用复制构造函数并存储复制的对象以用于槽调用。即使您传递的原始对象在插槽运行时已经被销毁,插槽获得的引用也将完全是对不同对象的引用。

你可以阅读@cgmb关于机械细节的回答。但这里有一个快速测试:

#include <iostream>
#include <QCoreApplication>
#include <QDebug>
#include <QTimer>
class Param {
public:
    Param () {}
    Param (Param const &) {
        std::cout << "Calling Copy Constructorn";
    }
};
class Test : public QObject {
    Q_OBJECT
public:
    Test () {
        for (int index = 0; index < 3; index++)
            connect(this, &Test::transmit, this, &Test::receive,
                Qt::QueuedConnection);
    }
    void run() {
        Param p;
        std::cout << "transmitting with " << &p << " as parametern";
        emit transmit(p);
        QTimer::singleShot(200, qApp, &QCoreApplication::quit);
    }
signals:
    void transmit(Param const & p);
public slots:
    void receive(Param const & p) {
        std::cout << "receive called with " << &p << " as parametern";
    }
};

…和main:

#include <QCoreApplication>
#include <QTimer>
#include "param.h"
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    // name "Param" must match type name for references to work (?)
    qRegisterMetaType<Param>("Param"); 
    Test t;
    QTimer::singleShot(200, qApp, QCoreApplication::quit);
    return a.exec();
}

运行这段代码表明,对于3个槽连接中的每一个,Param的一个单独副本都是通过复制构造函数生成的:

Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
receive called with 0x1bbf7c0 as parameter
receive called with 0x1bbf8a0 as parameter
receive called with 0x1bbfa00 as parameter

你可能会想,如果Qt只是要做拷贝,那么"通过引用传递"有什么用。然而,它并不总是使副本……这取决于连接类型。如果您更改为Qt::DirectConnection,它不会生成任何副本:

transmitting with 0x7ffebf241147 as parameter
receive called with 0x7ffebf241147 as parameter
receive called with 0x7ffebf241147 as parameter
receive called with 0x7ffebf241147 as parameter

如果你切换到通过值传递,你实际上会得到更多的中间副本,特别是在Qt::QueuedConnection的情况下:

Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
receive called with 0x7fff15146ecf as parameter
Calling Copy Constructor
receive called with 0x7fff15146ecf as parameter
Calling Copy Constructor
receive called with 0x7fff15146ecf as parameter

但是通过指针传递并没有任何特殊的魔力。所以它有原始答案中提到的问题,我会保留在下面。但事实证明,引用处理是另一回事。

原始回答

是的,如果你的程序是多线程的,这可能是危险的。即使不是,它的风格也很差。实际上,你应该在信号和槽连接上按值传递对象。

注意Qt支持"隐式共享类型",所以传递像QImage这样的东西"按值"不会复制,除非有人写入他们收到的值:

http://qt-project.org/doc/qt-5/implicit-sharing.html

这个问题根本与信号和插槽无关。c++有各种方法可以在对象被引用时删除它们,或者即使它们的一些代码在调用堆栈中运行。在任何无法控制代码并使用适当同步的代码中,都很容易陷入这种麻烦。像使用QSharedPointer这样的技巧可以提供帮助。

Qt提供了一些额外的有用的东西来更优雅地处理删除场景。如果你想销毁一个对象,但你知道它可能正在被使用,你可以使用QObject::deleteLater()方法:

http://qt-project.org/doc/qt-5/qobject.html deleteLater

这对我来说有几次会派上用场。另一个有用的东西是QObject::destroyed()信号:

http://qt-project.org/doc/qt-5/qobject.html摧毁

我很抱歉继续一个多年前的话题,但它出现在谷歌。我想澄清一下HostileFork的答案,因为它可能会误导未来的读者。

传递一个Qt信号的引用是不危险的,这要归功于信号/槽连接的工作方式:

  • 如果连接为直连,则直接调用已连接的槽位,例如当emit MySignal(my_string)返回时,所有已连接的槽位都已执行。
  • 如果连接是排队的,Qt创建一个引用的副本。因此,当调用slot时,它拥有通过引用传递的变量的有效副本。然而,这意味着参数必须是Qt知道的类型才能复制它。

https://doc.qt.io/qt-5/qt.html ConnectionType-enum

不,您不会遇到悬空引用。至少,除非你的槽做了一些在常规函数中也会导致问题的事情。

Qt: DirectionConnection

我们通常可以接受这对于直接连接来说不会是一个问题,因为这些插槽被立即调用。你的信号发射阻塞,直到所有的槽被调用。一旦发生这种情况,emit myQtSignal(fooStackObject);就会像常规函数一样返回。实际上,myQtSignal(fooStackObject);是一个正则函数!emit关键字完全是为您服务的——它什么也不做。信号函数的特殊之处在于它的代码是由Qt的编译器生成的:moc.

Qt: QueuedConnection

Benjamin T在文档中指出参数是被复制的,但我认为探索这在底层是如何工作的是有启发的(至少在Qt 4中)。

如果我们开始编译我们的项目并搜索我们生成的moc文件,我们可以找到这样的东西:

// SIGNAL 0
void Test::myQtSignal(const FooObject & _t1)
{
    void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

所以基本上,我们传递了一些东西给QMetaObject::activate:我们的QObject,我们QObject的类型的metaObject,我们的信号id,和一个指向信号接收到的每个参数的指针。

如果我们调查QMetaObject::activate,我们会发现它在qobject.cpp中声明。这是QObjects工作的一部分。在浏览了一些与这个问题无关的内容之后,我们发现了排队连接的行为。这一次,我们用QObject(信号的索引,一个表示从信号到槽的连接的对象)和参数调用QMetaObject::queued_activate

if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
    || (c->connectionType == Qt::QueuedConnection)) {
    queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);
    continue;

到达queued_activate后,我们终于到达了问题的实质。

首先,它根据信号构建一个连接类型列表:

QMetaMethod m = sender->metaObject()->method(signal);
int *tmp = queuedConnectionTypes(m.parameterTypes());

queuedConnectionTypes中重要的一点是,它使用QMetaType::type(const char* typeName)从信号的签名中获取参数类型的元类型id。这意味着两件事:

  1. 该类型必须具有QMetaType id,因此它必须已在qRegisterMetaType中注册。

  2. 类型归一化。这意味着"const T&"answers"T"映射到T的QMetaType id

最后,queued_activate将信号参数类型和给定的信号参数传递给QMetaType::construct,以复制构造新的对象,这些对象的生存期将持续到另一个线程调用该插槽为止。一旦事件被排队,信号返回。

这就是故事的基本内容。

如果对象存在的作用域结束,然后使用它,它将引用一个被销毁的对象,这将导致未定义的行为。如果您不确定作用域是否会结束,那么最好通过new在free store上分配对象,并使用shared_ptr之类的东西来管理其生命周期。