web-dev-qa-db-fra.com

Emplacements Qt et lambda C ++ 11

J'ai un élément QAction que j'initialise comme suit:

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

Et puis onSomeAction ressemble à quelque chose comme:

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

    // do some stuff with caller
}

Cela fonctionne très bien, je récupère l'objet caller et je peux l'utiliser comme prévu. Ensuite, j'essaie la façon C++ 11 de connecter l'objet comme tel:

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

    // do some stuff with caller
});

Mais caller est toujours nul et donc le Q_ASSERT déclenche. Comment puis-je utiliser des lambdas pour obtenir l'expéditeur?

48
Addy

La réponse est simple: vous ne pouvez pas. Ou plutôt, vous ne voulez pas (ou n'avez pas besoin!) D'utiliser sender(). Capturez et utilisez simplement action.

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

La spécification de this comme contexte d'objet pour le foncteur garantit que le foncteur ne sera pas appelé si l'action ou this (a QObject) cesse d'exister. Sinon, le foncteur essaierait de référencer des pointeurs pendants.

En général, ce qui suit doit être respecté lors de la capture de variables de contexte pour un foncteur passé à connect, afin d'éviter l'utilisation de pointeurs/références pendantes:

  1. Les pointeurs vers les objets source et cible de connect peuvent être capturés par valeur, comme ci-dessus. Il est garanti que si le foncteur est invoqué, les deux extrémités de la connexion existent.

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

    Les scénarios où a et b sont dans des threads différents nécessitent une attention particulière. Il ne peut pas être garanti qu'une fois le foncteur entré, certains threads ne supprimeront aucun des deux objets.

    Il est idiomatique qu'un objet ne soit détruit que dans sa thread(), ou dans n'importe quel thread si thread() == nullptr. Étant donné que la boucle d'événements d'un thread appelle le foncteur, le thread nul n'est jamais un problème pour b - sans thread, le foncteur ne sera pas invoqué. Hélas, il n'y a aucune garantie sur la durée de vie du a dans le thread de b. Il est donc plus sûr de capturer l'état nécessaire de l'action par valeur à la place, de sorte que la durée de vie de a ne soit pas un problème.

    // 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(); });
    
  2. Les pointeurs bruts vers d'autres objets peuvent être capturés par valeur si vous êtes absolument sûr que la durée de vie des objets qu'ils pointent chevauche la durée de vie de la connexion.

    static C c;
    auto p = &c;
    connect(..., [p]{});
    
  3. Idem pour les références aux objets:

    static D d;
    connect(..., [&d]{});
    
  4. Les objets non copiables qui ne dérivent pas de QObject doivent être capturés via leurs pointeurs partagés par valeur.

    std::shared_ptr<E> e { new E };
    QSharedPointer<F> f { new F; }
    connect(..., [e,f]{});
    
  5. QObjects vivant dans le même thread peuvent être capturés par un QPointer; sa valeur doit être vérifiée avant utilisation dans le foncteur.

    QPointer<QObject> g { this->parent(); }
    connect(..., [g]{ if (g) ... });
    
  6. QObjects vivant dans d'autres threads doivent être capturés par un pointeur partagé ou un pointeur faible. Leur parent doit être désarmé avant leur destruction, sinon vous aurez deux suppressions:

    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) { ... }
    });
    
73
Kuba Ober

L'utilisation de lambdas comme emplacements est simple (par exemple pour un événement à partir d'une QSpinbox):

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

Mais cela ne fonctionne que si le signal n'est pas surchargé (cela signifie qu'il y a plusieurs signaux avec le même nom mais des arguments différents).

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

donne une erreur de compilation, car il existe deux signaux surchargés: valueChanged (int) et valueChanged (const QString &) Il est donc nécessaire de qualifier la version à utiliser:

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

Un peu plus court (ou mieux lisible) est l'utilisation de QOverload :

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