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

发布于 2024-12-20 16:08:04 字数 489 浏览 2 评论 0原文

我可以有一个带有以下代码的“悬空引用”(在连接到 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 不在同一个线程中执行。

May I have a "dangling reference" with the following code (in an eventual slot connected to the 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;
}

Particularly if emit and slot are not executed in the same thread.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

雅心素梦 2024-12-27 16:08:04

更新 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 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

UPDATE 20-APR-2015

Originally I believed that passing a reference to a stack-allocated object would be equivalent to passing the address of that object. Hence in the absence of a wrapper that would store a copy (or a shared pointer), a queued slot connection could wind up using the bad data.

But it was raised to my attention by @BenjaminT and @cgmb that Qt actually does have special handling for const reference parameters. It will call the copy constructor and stow away the copied object to use for the slot calls. Even if the original object you passed has been destroyed by the time the slot runs, the references that the slots get will be to different objects entirely.

You can read @cgmb's answer for the mechanical details. But here's a quick test:

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

...and a 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();
}

Running this demonstrates that for each of the 3 slot connections, a separate copy of the Param is made via the copy constructor:

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

You might wonder what good it does to "pass by reference" if Qt is just going to make copies anyway. However, it doesn't always make the copy...it depends on the connection type. If you change to Qt::DirectConnection, it doesn't make any copies:

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

And if you switched to passing by value, you'd actually get a more intermediate copies, especially in the Qt::QueuedConnection case:

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

But passing by pointer doesn't do any special magic. So it has the problems mentioned in the original answer, which I'll keep below. But it has turned out that reference handling is just a different beast.

ORIGINAL ANSWER

Yes, this can be dangerous if your program is multithreaded. And it's generally poor style even if not. Really you should be passing objects by value over signal and slot connections.

Note that Qt has support for "implicitly shared types", so passing things like a QImage "by value" won't make a copy unless someone writes to the value they receive:

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

The problem isn't fundamentally anything to do with signals and slots. C++ has all kinds of ways that objects might be deleted while they're referenced somewhere, or even if some of their code is running in the call stack. You can get into this trouble easily in any code where you don't have control over the code and use proper synchronization. Techniques like using QSharedPointer can help.

There are a couple of additional helpful things Qt offers to more gracefully handle deletion scenarios. If there's an object you want to destroy but you are aware that it might be in use at the moment, you can use the QObject::deleteLater() method:

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

That's come in handy for me a couple of times. Another useful thing is the QObject::destroyed() signal:

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

嘴硬脾气大 2024-12-27 16:08:04

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

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

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

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

I'm sorry to continue a subject years old but it came up on Google. I want to clarify HostileFork's answer as it may mislead future readers.

Passing a reference to a Qt signal is not dangerous thanks to the way signal/slot connections work:

  • If the connection is direct, connected slots are directly called directly, e.g. when emit MySignal(my_string) returns all directly connected slots have been executed.
  • If the connection is queued, Qt creates a copy of the referencees. So when the slot is called it has its own valid copy of the variables passed by reference. However this means that parameters must be of a type that Qt knows about in order to copy it.

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

演出会有结束 2024-12-27 16:08:04

不,您不会遇到悬空引用。至少,除非你的插槽执行的操作也会导致常规函数出现问题。

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中声明。这是 QObject 工作方式中不可或缺的一部分。在浏览了一些与这个问题无关的内容后,我们发现了排队连接的行为。这次我们调用 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) 来从信号的签名。这意味着两件事:

  1. 该类型必须有一个 QMetaType id,因此它必须已使用 qRegisterMetaType

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

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

故事基本上就是这样。

No, you won't encounter a dangling reference. At least, not unless your slot does the sort of things that would cause problems in regular functions too.

Qt::DirectionConnection

We can generally accept that this won't be a problem for direct connections as those slots are called immediately. Your signal emission blocks until all slots have been called. Once that happens, emit myQtSignal(fooStackObject); will return just like a regular function. In fact, myQtSignal(fooStackObject); is a regular function! The emit keyword is entirely for your benefit--it does nothing. The signal function is just special because its code is generated by Qt's compiler: the moc.

Qt::QueuedConnection

Benjamin T has pointed out in the documentation that arguments are copied, but I think it's enlightening to explore how this works under the hood (at least in Qt 4).

If we start by compiling our project and searching around for our generated moc file, we can find something like this:

// 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);
}

So basically, we pass a number of things to QMetaObject::activate: our QObject, the metaObject for our QObject's type, our signal id, and a pointer to each of the arguments our signal received.

If we investigate QMetaObject::activate, we'll find it's declared in qobject.cpp. This is something integral to how QObjects work. After browsing through some stuff that's irrelevant to this question, we find the behaviour for queued connections. This time we call QMetaObject::queued_activate with our QObject, the signal's index, an object representing the connection from signal to slot, and the arguments again.

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

Having reached queued_activate, we've finally arrived at the real meat of the question.

First, it builds a list of connection types from the signal:

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

The important thing in queuedConnectionTypes is that it uses QMetaType::type(const char* typeName) to get the metatype id of the argument type from the signal's signature. This means two things:

  1. The type must have a QMetaType id, thus it must have been registered with qRegisterMetaType.

  2. Types are normalized. This means "const T&" and "T" map to the QMetaType id for T.

Finally, queued_activate passes the signal argument types and the given signal arguments into QMetaType::construct to copy-construct new objects with lifetimes that will last until the slot has been called in another thread. Once the event has been queued, the signal returns.

And that's basically the story.

雪若未夕 2024-12-27 16:08:04

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

If the scope in which an object exists ends and it is then used, it will refer to a destroyed object which will cause undefined behaviour. If you are not sure whether the scope will end, it is best to allocate the object on the free store via new and use something like shared_ptr to manage its lifetime.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文