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

时间:2011-12-10 10:34:48

标签: 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;
}

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

4 个答案:

答案 0 :(得分:29)

更新20-APR-2015

最初我认为传递对堆栈分配对象的引用相当于传递该对象的地址。因此,如果没有存储副本(或共享指针)的包装器,排队的插槽连接可能会使用坏数据结束。

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

您可以阅读@cgmb's answer了解机械细节。但这是一个快速测试:

#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";
    }
};

......还有一个主要的:

#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#destroyed

答案 1 :(得分:23)

我很抱歉继续使用这个主题,但它出现在Google上。我想澄清HostileFork的答案,因为它可能会误导未来的读者。

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

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

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

答案 2 :(得分:14)

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

<强> 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类型的元对象,我们的信号id,以及指向我们信号接收的每个参数的指针。 / p>

如果我们调查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. 类型为normalized。这意味着&#34; const T&amp;&#34;和&#34; T&#34;映射到T的QMetaType id。

  3. 最后,queued_activate将信号参数类型和给定的信号参数传递给QMetaType::construct,以复制构造新对象,其生命周期将持续到另一个线程中已调用该槽。一旦事件排队,信号就会返回。

    这基本上就是故事。

答案 3 :(得分:0)

如果对象存在的作用域结束然后使用它,它将引用一个被破坏的对象,这将导致未定义的行为。如果您不确定范围是否结束,最好通过new在免费商店中分配对象,并使用类似shared_ptr的内容来管理其生命周期。