Подтвердить что ты не робот

Стек сигнала Qt и параметр в качестве ссылки

Могу ли я иметь "оборванную ссылку" со следующим кодом (в конечном слоте, подключенном к 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;
}

В частности, если испускание и слот не выполняются в одном потоке.

4b9b3361

Ответ 1

ОБНОВЛЕНИЕ 20-APR-2015

Первоначально я считал, что передача ссылки на объект, выделенный стеком, будет эквивалентна передаче адреса этого объекта. Следовательно, в отсутствие обертки, в которой будет храниться копия (или общий указатель), подключение к очередному слоту может завершиться с использованием плохих данных.

Но мое внимание привлекло @BenjaminT и @cgmb, что Qt действительно имеет специальную обработку для опорных параметров const. Он вызывается конструктором копирования и убирает скопированный объект для использования для вызовов слотов. Даже если исходный объект, который вы передали, был уничтожен к моменту запуска слота, ссылки, которые получают слоты, будут полностью связаны с различными объектами.

Вы можете прочитать @cgmb 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

Проблема не в корне не связана с сигналами и слотами. В С++ есть всевозможные способы удаления объектов, когда они упоминаются где-то, или даже если некоторые из их кода запущены в стеке вызовов. Вы можете легко справиться с этой проблемой в любом коде, где вы не имеете контроля над кодом и используете правильную синхронизацию. Техники, такие как использование QSharedPointer, могут помочь.

Есть несколько дополнительных полезных вещей, которые Qt предлагает более грациозно обрабатывать сценарии удаления. Если есть объект, который вы хотите уничтожить, но вы знаете, что он может быть использован в данный момент, вы можете использовать метод QObject:: deleteLater():

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

Это пригодится мне пару раз. Еще одна полезная вещь - сигнал QObject:: destroy():

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

Ответ 2

Мне жаль, что я продолжаю тему лет, но она появилась в Google. Я хочу уточнить ответ HostileFork, поскольку он может ввести в заблуждение будущих читателей.

Передача ссылки на сигнал Qt не является опасной благодаря тому, как работают соединения сигнал/слот:

  • Если соединение является прямым, подключенные слоты непосредственно вызываются напрямую, например. когда emit MySignal(my_string) возвращает все непосредственно подключенные слоты.
  • Если соединение поставлено в очередь, Qt создает копию ссылок. Поэтому, когда слот называется, он имеет свою собственную действительную копию переменных, переданных по ссылке. Однако это означает, что параметры должны быть типа, который Qt знает для его копирования.

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

Ответ 3

Нет, вы не столкнетесь с оборванной ссылкой. По крайней мере, если ваш слот не будет делать такие вещи, которые могут вызвать проблемы и в обычных функциях.

Qt:: DirectionConnection

Обычно мы можем согласиться с тем, что это не будет проблемой для прямых подключений, так как эти слоты вызываются немедленно. Блокировка вашего сигнала блокируется до тех пор, пока не будут вызваны все слоты. Как только это произойдет, emit myQtSignal(fooStackObject); вернется как обычная функция. Действительно, myQtSignal(fooStackObject); является регулярной функцией! Исходное ключевое слово целиком для вас - оно ничего не делает. Функция сигнала является просто специальной, поскольку ее код генерируется компилятором 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, наш идентификатор сигнала и указатель на каждый из аргументов, полученный нашим сигналом.

Если мы исследуем 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) для получения идентификатора метатипа типа аргумента из сигнальной сигнатуры. Это означает две вещи:

  • Тип должен иметь идентификатор QMetaType, поэтому он должен быть зарегистрирован qRegisterMetaType.

  • Типы normalized. Это означает, что "const T &" и "T" - к идентификатору QMetaType для T.

Наконец, queued_activate передает типы аргументов сигнала и заданные аргументы сигнала в QMetaType::construct, чтобы копировать новые объекты со временем жизни, которые будут длиться пока слот не был вызван в другом потоке. После того, как событие было поставлено в очередь, сигнал возвращается.

И это в основном история.

Ответ 4

Если область, в которой существует объект, заканчивается и затем используется, она будет ссылаться на уничтоженный объект, который приведет к поведению undefined. Если вы не уверены, закончится ли область действия, лучше выделить объект в свободном хранилище с помощью new и использовать что-то вроде shared_ptr для управления его жизненным циклом.