【问题标题】:stack object Qt signal and parameter as reference堆栈对象 Qt 信号和参数作为参考
【发布时间】:2012-01-17 08:37:12
【问题描述】:

我可以使用以下代码(在连接到 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 不在同一个线程中执行。

【问题讨论】:

  • 请注意cgmb的回答可能比接受的要好。

标签: c++ qt signals-slots


【解决方案1】:

2015 年 4 月 20 日更新

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

但@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

【讨论】:

  • 既然 Qt 无论如何都会进行复制,那么按值传递参数不是更好的风格吗?由于正在发生的事情的实际语义相当模糊,我认为 pass-by-argument 表达了这样一个事实,即副本将以比 pass-by-reference 更清晰的方式制作。中间副本问题听起来应该由 Qt 开发人员而不是其用户来解决。
  • 原来的答案是完全错误的。这不应该是公认的答案。
【解决方案2】:

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

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

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

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

【讨论】:

  • 让 StackOverflow 成为可编辑的 wiki 风格并让人们想出以后的答案的全部意义在于让它变得更好!所以不用担心。事实上,我自己现在 - 几年后 - 发现了这个问答 - 查找为什么 Mandelbrot 中的 QImage 是passed by reference,这似乎是错误的。您引用的链接确实提到了复制,但它没有明确说明它复制了引用 指向的对象(我认为它应该这样做!) 这是一件非常重要的事情,看起来像对我来说是一个文档错误!
  • HostileFork 和 Benjamin T 似乎有相反的论点。在定义信号和槽时,到处使用引用是否安全?
  • @nullstellensatz HostileFork 搞错了。这是安全的。我什至会称之为好风格。
  • @cgmb 虽然事情的实际工作方式和人们想象它们的工作方式可能是不同的事情,但我不知道当你传递对某事物的引用并选择复制时 Qt 不知何故注意到了 (与如果你传递了一个指针,那么它会复制吗?) 证明是错误的一切都很好,而且系统很复杂,等等。但我在这里没有看到表明 Qt 的证据已将 C++ 颠覆到声称的“复制引用” 的水平。我很忙,希望看到证明是肯定的,这就是它的工作原理,而不是让我和塞思卡内基加重反驳它的负担。
  • @HostileFork 恰恰相反。它没有注意到引用和值之间的区别。无论哪种方式,它只是复制你给出的论点。我在此评论字段中的空间不足,因此我发布了有关它如何作为答案的更多详细信息。
【解决方案3】:

不,您不会遇到悬空引用。至少,除非你的 slot 做的事情也会导致常规函数出现问题。

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 以及指向我们的信号接收到的每个参数的指针。

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

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。这意味着“const T&”和“T”映射到 T 的 QMetaType id。

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

这基本上就是故事。

【讨论】:

  • 我尝试了一些变体,但无法管理咒语以使 const &amp; 参数起作用。指针将编译和调用,但显然与提到的行为不正确,see example。如果您可以修改该程序以使用 const 引用调用并通过输出获得此隐式副本,那么……那将很有趣。
  • 您需要将与类型名称匹配的字符串传递给qRegisterMetaType。 Qt 投诉:QObject::connect: Cannot queue arguments of type 'Param' (Make sure 'Param' is registered using qRegisterMetaType().)。见my fix / port to Qt4
  • 奇怪...我一直觉得这个名字是抽象的,对于非参考案例,它似乎适用于任何名称。好吧,这绝对很奇怪......复制构造函数似乎为每个插槽调用一次,这破坏了与共享指针相比的有用性。不管怎样,谢谢;我将更新我的答案以反映调查结果。
【解决方案4】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-11-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-25
    • 2020-10-25
    • 1970-01-01
    相关资源
    最近更新 更多