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

Слоты Qt и С++ 11 лямбда

У меня есть элемент QAction, который я инициализирую следующим образом:

QAction* action = foo->addAction(tr("Some Action"));
connect(action, SIGNAL(triggered()), this, SLOT(onSomeAction()));

И тогда onSomeAction выглядит примерно так:

void MyClass::onSomeAction()
{
    QAction* caller = qobject_cast<QAction*>(sender());
    Q_ASSERT(caller != nullptr);

    // do some stuff with caller
}

Это отлично работает, я возвращаю объект caller, и я могу использовать его, как ожидалось. Затем я пытаюсь использовать способ С++ 11 для подключения объекта:

connect(action, &QAction::triggered, [this]()
{
    QAction* caller = qobject_cast<QAction*>(sender());
    Q_ASSERT(caller != nullptr);

    // do some stuff with caller
});

Но caller всегда имеет значение null и, следовательно, триггеры Q_ASSERT. Как я могу использовать lambdas для получения отправителя?

4b9b3361

Ответ 1

Простой ответ: вы не можете. Или, скорее, вы не хотите (или нуждаетесь!) Использовать sender(). Просто запишите и используйте action.

//                                Important!
//                                   vvvv
connect(action, &QAction::triggered, this, [action, this]() {
    // use action as you wish
    ...
});

Спецификация this как контекста объекта для функтора гарантирует, что функтор не будет вызван, если действие или this (a QObject) перестают существовать. В противном случае функтор попытается ссылаться на оборванные указатели.

В общем случае при захвате переменных контекста для функтора, переданного в connect, должно выполняться следующее: во избежание использования оборванных указателей/ссылок:

  • Указатели исходного и целевого объектов connect могут быть записаны по значению, как указано выше. Гарантируется, что при вызове функтора оба конца соединения существуют.

    connect(a, &A::foo, b, [a, b]{});
    

    Сценарии, в которых a и b находятся в разных потоках, требуют особого внимания. Невозможно гарантировать, что после ввода функции какой-либо поток не удалит ни один объект.

    Идиоматично, что объект уничтожается только в thread() или в любом потоке, если thread() == nullptr. Поскольку цикл событий потока вызывает функтор, нулевой поток никогда не является проблемой для b - без потока функтор не будет вызываться. Увы, нет гарантии о времени жизни a в b потоке. Таким образом, безопаснее фиксировать необходимое состояние действия по значению вместо этого, так что a время жизни не вызывает беспокойства.

    // SAFE
    auto aName = a->objectName();       
    connect(a, &A::foo, b, [aName, b]{ qDebug() << aName; });
    // UNSAFE
    connect(a, &A::foo, b, [a,b]{ qDebug() << a->objectName(); });
    
  • Необработанные указатели на другие объекты могут быть записаны по значению, если вы абсолютно уверены, что время жизни объектов, на которые они указывают, перекрывает время жизни соединения.

    static C c;
    auto p = &c;
    connect(..., [p]{});
    
  • Кроме того, для ссылок на объекты:

    static D d;
    connect(..., [&d]{});
    
  • Не скопируемые объекты, которые не выводятся из QObject, должны быть записаны через их общие указатели по значению.

    std::shared_ptr<E> e { new E };
    QSharedPointer<F> f { new F; }
    connect(..., [e,f]{});
    
  • QObject живущие в одной и той же нити могут быть захвачены с помощью QPointer; его значение должно быть проверено перед использованием в функторе.

    QPointer<QObject> g { this->parent(); }
    connect(..., [g]{ if (g) ... });
    
  • QObject, живущие в других потоках, должны быть захвачены общим указателем или слабым указателем. Их родитель должен быть удален до их уничтожения, иначе у вас будут двойные удаления:

    class I : public QObject {
      ...
      ~I() { setParent(nullptr); }
    };
    
    std::shared_ptr<I> i { new I };
    connect(..., [i]{ ... });
    
    std::weak_ptr<I> j { i };
    connect(..., [j]{ 
      auto jp = j.lock();
      if (jp) { ... }
    });
    

Ответ 2

Использовать лямбды в качестве слотов просто (например, для события из QSpinbox):

connect(spinboxObject, &QSpinBox::editingFinished, this, [this]() {<do something>});

Но это работает, только если сигнал не перегружен (это означает, что есть несколько сигналов с одинаковым именем, но разными аргументами).

connect(spinboxObject, &QSpinBox::valueChange, this, [this]() {<do something>});

выдает ошибку компиляции, поскольку существуют два перегруженных сигнала: valueChanged (int) и valueChanged (const QString &). Поэтому необходимо определить, какую версию следует использовать:

connect(spinboxObject, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this](int newValue){ });

Немного короче (или лучше читаемо) использование QOverload:

connect(spinboxObject, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int newValue) { });

Ответ 3

Без "этого" контекста, например из main():

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QLabel lbl{"Hello World!"};
    QPushButton btn;
    btn.show();
    lbl.show();

    QObject::connect(&btn, &QPushButton::clicked, [&lbl](){lbl.setText("Button clicked");});

    return a.exec();
}