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

Gui*_*e07 25 c++ qt signals-slots

我可以使用以下代码(在连接到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;
}
Run Code Online (Sandbox Code Playgroud)

特别是如果在同一个线程中没有执行emit和slot.

Hos*_*ork 30

更新20-APR-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 Constructor\n";
    }
};

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 parameter\n";
        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 parameter\n";
    }
};
Run Code Online (Sandbox Code Playgroud)

......和一个主要的:

#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();
}
Run Code Online (Sandbox Code Playgroud)

运行此演示表明,对于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
Run Code Online (Sandbox Code Playgroud)

你可能想知道如果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
Run Code Online (Sandbox Code Playgroud)

如果你转换为通过值传递,你实际上会获得更多的中间副本,特别是在这种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
Run Code Online (Sandbox Code Playgroud)

但是通过指针传递没有任何特殊的魔力.所以它有原始答案中提到的问题,我将在下面提到.但事实证明,参考处理只是一种不同的野兽.

原始答案

是的,如果您的程序是多线程的,这可能很危险.即使没有,它通常也很糟糕.你应该通过信号和插槽连接按值传递对象.

请注意,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#destroyed


Ben*_*n T 23

对不起,我想继续学习几年,但它出现在Google上.我想澄清HostileFork的答案,因为它可能会误导未来的读者.

由于信号/插槽连接的工作方式,传递对Qt信号的引用并不危险:

  • 如果连接是直接连接,则直接直接调用连接的插槽,例如,当emit MySignal(my_string)返回所有直接连接的插槽时.
  • 如果连接排队,Qt会创建引用的副本.因此,当调用插槽时,它有自己的有效副本,通过引用传递.但是,这意味着参数必须是Qt知道的类型才能复制它.

http://qt-project.org/doc/qt-5.1/qtcore/qt.html#ConnectionType-enum


cgm*_*gmb 15

不,你不会遇到悬挂的参考.至少,除非你的插槽做了那些会导致常规功能出现问题的东西.

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);
}
Run Code Online (Sandbox Code Playgroud)

所以基本上,我们传递了许多东西QMetaObject::activate:我们的QObject,我们的QObject类型的元对象,我们的信号id,以及指向我们信号接收的每个参数的指针.

如果我们调查QMetaObject::activate,我们会发现它在qobject.cpp中声明.这是QObjects如何工作不可或缺的一部分.在浏览了与此问题无关的一些内容之后,我们找到了排队连接的行为.这次我们调用QMetaObject::queued_activateQObject,信号的索引,一个表示从信号到插槽的连接的对象,以及参数.

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

到达queued_activate后,我们终于找到了问题的真正含义.

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

QMetaMethod m = sender->metaObject()->method(signal);
int *tmp = queuedConnectionTypes(m.parameterTypes());
Run Code Online (Sandbox Code Playgroud)

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

  1. 该类型必须具有QMetaType标识,因此必须已注册qRegisterMetaType.

  2. 类型是标准化的.这意味着"const T&"和"T"映射到T的QMetaType id.

最后,queued_activate将信号参数类型和给定的信号参数QMetaType::construct传递给copy-construct new对象,其生命周期将持续到另一个线程中调用了slot.一旦事件排队,信号就会返回.

这基本上就是故事.