3 votes

Quelle est la durée de vie des variables allouées à la pile capturée dans les fonctions lambda utilisées comme slots ?

J'ai besoin d'aide pour comprendre le fonctionnement des fonctions lambda afin d'éviter les fuites de mémoire lors de leur utilisation. Plus précisément, j'aimerais savoir quand foo sera détruit dans le cas suivant :

void MainWindow::onButtonClicked()
{
    QTimer *t(new QTimer(this));
    bool foo = false;

    t->setSingleShot(true);
    t->setInterval(1000);
    t->start();

    connect(t, &QTimer::timeout, [=](){
        delete t;
        qDebug() << foo;
    });
}

Qu'en est-il du cas où [&] est utilisé ?

2voto

Kuba Ober Points 18926

Une expression lambda évaluée est une instance du foncteur. Un foncteur est un objet avec operator() . Les variables capturées sont des membres de cet objet foncteur. Leur durée de vie ne change pas en fonction de leur type. Ainsi, que vous capturiez des références ou des valeurs, leur durée de vie est la même. C'est votre travail de vous assurer que les références sont valides. - c'est-à-dire que les objets qu'ils référencent n'ont pas été détruits.

La durée de vie du foncteur est la même que celle de la connexion. La connexion, et donc le foncteur, durera jusqu'à ce que l'un ou l'autre :

  1. QObject::disconnect() est invoqué sur la valeur de retour de QObject::connect() ou

  2. En QObject Sa vie se termine et son destructeur est invoqué.

Compte tenu de ce qui précède, la seule utilisation valable de la capture par référence de variables locales est celle où les variables locales survivent à la connexion. Voici quelques exemples valables :

void test1() {
  int a = 5;
  QObject b;
  QObject:connect(&b, &QObject::destroyed, [&a]{ qDebug() << a; });
  // a outlives the connection - per C++ semantics `b` is destroyed before `a` 
}

Ou :

int main(int argc, char **argv) {
  QObject * focusObject = {};
  QApplication app(argc, argv);
  QObject * connect(&app, &QGuiApplication::focusObjectChanged,
                    [&](QObject * obj){ focusObject = obj; });
  //...
  return app.exec();  // focusObject outlives the connection too
}

Le code dans votre question est inutilement complexe. Il n'est pas nécessaire de gérer ces minuteries manuellement :

void MainWindow::onButtonClicked() {
  bool foo = {};
  QTimer::singleShot(1000, this, [this, foo]{ qDebug() << foo; });
}

La partie importante ici est de fournir le contexte de l'objet ( this ) comme 2ème argument de singleShot . Cela garantit que this doit survivre au foncteur. Inversement, le foncteur sera détruit avant que this est détruit.

En supposant que vous vouliez vraiment instancier un nouveau timer transitoire, c'est un comportement non défini que de supprimer l'objet source du signal dans un slot connecté à un tel signal. Vous devez plutôt reporter la suppression à la boucle d'événement :

void MainWindow::onButtonClicked()
{
  auto t = new QTimer(this);
  bool foo = {};

  t->setSingleShot(true);
  t->setInterval(1000);
  t->start();

  connect(t, &QTimer::timeout, [=](){
    qDebug() << foo;
    t->deleteLater();
  });
}

Les deux sites t y foo sont copiés dans l'objet foncteur. L'expression lambda est un raccourci notational - vous pourriez l'écrire explicitement vous-même :

class $OpaqueType {
  QTimer * const t;
  bool const foo;
public:
  $OpaqueType(QTimer * t, bool foo) :
    t(t), foo(foo) {}
  void operator()() {
    qDebug() << foo;
    t->deleteLater();
  }
};

void MainWindow::onButtonClicked() {
  //...
  connect(t, &QTimer::timeout, $OpaqueType(t, foo));
}

Puisqu'une instance lambda n'est qu'un objet, vous pouvez certainement l'assigner à une variable et vous débarrasser de la duplication de code si plusieurs signaux doivent être connectés à la même lambda :

auto f = [&]{ /* code */ };
connect(o, &Class::signal1, this, f);
connect(p, &Class::signal2, this, f);

Le type du lambda est un type unique, indicible, également appelé opaque. Vous ne pouvez pas le mentionner littéralement - il n'y a aucun mécanisme dans le langage pour le faire. Vous pouvez y faire référence via decltype seulement. Ici decltype es el il en celui qui ne doit pas être nommé . Les C++ ont juste fait une blague sur Harry Potter, qu'ils le veuillent ou non. Je ne serai pas convaincu du contraire.

1voto

Peter Points 4026

Un lambda qui capture des variables est essentiellement une structure de données sans nom, et les variables capturées deviennent des membres de cette structure. Cette structure de données est considérée comme appelable parce qu'elle fournit une méthode d'appel. operator() qui accepte les arguments spécifiés.

Si les variables sont capturées par une valeur, les membres de la lambda contiennent ces valeurs. Ces valeurs existent tant que le lambda existe, comme dans votre cas, en tant que copies des variables capturées - indépendamment du fait que les variables capturées à l'origine continuent d'exister.

C'est un peu différent pour les variables capturées par référence. Dans ce cas, la lambda contient une référence à une variable (par exemple une variable de pile), et cela devient une référence pendante si la variable référencée cesse d'exister. De même, si la valeur capturée est un pointeur, et que ce qu'il pointe cesse d'exister. Dans ces cas, l'utilisation de la référence capturée ou le déréférencement du pointeur donnent un comportement non défini.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X