28

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.

Guillaume Paris
  • 10,303
  • 14
  • 70
  • 145
  • Note that [cgmb](https://stackoverflow.com/a/29742028/7621674)'s answer may be better than the accepted one. – m7913d Apr 18 '20 at 16:41

4 Answers4

34

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

Community
  • 1
  • 1
  • Since Qt makes a copy anyway, wouldn't it be better style to pass arguments by value? Since the actual semantics of what's going on is rather obscure, I think pass-by-argument expresses the fact that copies are going to be made in a more clear way than pass-by-reference. And the intermediate copies problem sounds like something that should be solved by Qt developers rather than its users. – Alexander Revo May 30 '18 at 12:41
  • 1
    The original answer was completely wrong. This should not be the accepted answer. – m7913d Apr 18 '20 at 16:39
30

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

Benjamin T
  • 8,120
  • 20
  • 37
  • The whole point of having StackOverflow be editable wiki-style and let people come up with later answers is for it to get better! So don't worry about that. In fact, I found this Q&A myself now--years later--looking up why the QImage in Mandelbrot is [passed by reference](http://qt-project.org/doc/qt-4.8/threads-mandelbrot-mandelbrotwidget-cpp.html), which seemed wrong. The link you cite does mention copying, but it doesn't explicitly state that it copies the objects pointed to by reference *(which I'd think it should!)* That's a pretty major thing, and seems like a documentation bug to me! – HostileFork says dont trust SE Mar 19 '14 at 03:48
  • HostileFork and Benjamin T seem to have opposing arguments. Is is safe or not to use references everywhere when defining signals and slots? – nullstellensatz Apr 19 '15 at 21:47
  • @nullstellensatz HostileFork was mistaken. It is safe. I would even call it good style. – cgmb Apr 19 '15 at 23:29
  • @cgmb While how things actually work and how one imagines they work can be different things, I wasn't aware that Qt somehow noticed when you passed a reference to something and chose to make a copy *(vs. if you passed a pointer, does it make a copy then?)* It's all fine and well to be proven wrong, and systems are complicated, etc. But I don't see evidence here that suggests Qt has subverted C++ to the level of *"making a copy of the referencees"* as claimed. I'm busy and would like to see proof-positive that's how it works, vs. burdening me and SethCarnegie to disprove it. – HostileFork says dont trust SE Apr 20 '15 at 03:27
  • 1
    @HostileFork Quite the opposite. It doesn't notice the difference between a reference and a value. Either way, it's just making a copy of the argument you gave it. I ran out of room in this comment field, so I've posted more detail on how it works as an answer. – cgmb Apr 20 '15 at 07:50
  • by the way: `Qt` does NOT support `references` as return-type link: https://github.com/qt/qtbase/blob/cf708de03aa96d18c5bc043c90b2795fcf1b0517/src/tools/moc/moc.cpp#L420 – Top-Master Dec 09 '18 at 07:53
19

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.

cgmb
  • 4,284
  • 3
  • 33
  • 60
  • I tried some variations and couldn't manage an incantation to get a `const &` parameter to work. A pointer will compile and call, but is clearly incorrect with the behavior mentioned, [see example](https://gist.github.com/hostilefork/61cd8083bb958272caca). If you can modify that program to call with const reference and get this implicit copy to happen via output, well...that will be interesting. – HostileFork says dont trust SE Apr 20 '15 at 23:38
  • You need to pass a string that matches the type name to `qRegisterMetaType`. Qt complains: `QObject::connect: Cannot queue arguments of type 'Param' (Make sure 'Param' is registered using qRegisterMetaType().)`. See [my fix / port to Qt4](https://gist.github.com/cgmb/a000fdb907995e0d727e) – cgmb Apr 20 '15 at 23:58
  • Strange...I was always under the impression the name was abstract, and for non-reference cases it seems to work fine with any name. Well this is definitely weird...the copy constructor appears to be called once for each slot, which undermines the usefulness vs. a shared pointer. Anyway, thanks; I will update my answer to reflect the finding. – HostileFork says dont trust SE Apr 21 '15 at 00:16
0

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.

Seth Carnegie
  • 73,875
  • 22
  • 181
  • 249