jeudi 28 août 2014

Lambdas et capture d'attributs

J'ai aujourd'hui découvert que la capture d'attributs dans une lambda peut parfois donner des résultats surprenants. Regardez donc l'innocent programme que voilà. L'idée est d'avoir une classe A qui créé une lambda, laquelle capture un attribut de type pointeur partagé. L'on s'assure que A est ensuite détruit, avant d'exécuter la lambda. Que se passe-t-il?

#include <iostream>
#include <functional>
#include <memory>

class C
{
public:
  C():
    _value("abcdef")
  {
    std::cout << "Constructor" << std::endl;
  }

  ~C()
  {
    std::cout << "Destructor" << std::endl;
  }
  
  std::string _value;
};

class A
{
public:
  A():
    _c(std::make_shared<C>())
  {
  }
  
  std::function<void()> makeLambda()
  {
    return [=]()
    {
      std::cout << "Executing the lambda" << std::endl;
      std::cout << _c->_value << std::endl;      
    };
  }
  
  std::shared_ptr<C> _c;
};

int main()
{
  std::function<void()> f;

  {
    A a;
    f = a.makeLambda();
  }
  
  f();
  
  return 0;
}

Je pensais simplement que l'attribut allait être capturé par valeur (puisqu'il y a le "=" dans ma lambda), et que donc le pointeur partagé serait copié. Or, voilà ce qui se passe lorsque l'on exécute le programme:

Constructor
Destructor
Executing the lambda

L'objet C est détruit avant l'exécution de la lambda, c'est à dire au moment de la destruction de A! C'est en plein dans le royaume de l'undefined behaviour, en l'occurence ne rien afficher, mais plus probablement un crash. En cherchant un peu, je me suis rendu compte que les attributs sont en fait capturés via le pointeur this. D'ailleurs, la lambda refuse de compiler si l'on capture explicitement _c: [_c](){...};. En revanche, le compilateur est heureux en capturant this: [this](){...};.

La solution pour faire fonctionner l'exemple est tout simple: il faut copier _c dans une variable locale à la fonction, et capturer cette variable.

  std::function<void()> makeLambda()
  {
    auto localC = _c;
    return [=]()
    {
      std::cout << "Executing the lambda" << std::endl;
      std::cout << localC->_value << std::endl;      
    };
  }

Avec cette version, le résultat est celui attendu:

Constructor
Executing the lambda
abcdef
Destructor

Attention donc à ce qui est réellement capturé!

Aucun commentaire: