Dans la syntaxe des fonctions lambda, à quoi sert une “liste de capture”?

Tiré d’une réponse à cette question , à titre d’exemple, il s’agit d’un code qui calcule la sum des éléments dans un std::vector :

 std::for_each( vector.begin(), vector.end(), [&](int n) { sum_of_elems += n; } ); 

Je comprends que les fonctions lambda ne sont que des fonctions sans nom.

Je comprends la syntaxe des fonctions lambda comme expliqué ici .

Je ne comprends pas pourquoi les fonctions lambda ont besoin de la liste de capture, contrairement aux fonctions normales.

  1. Quelles informations supplémentaires une liste de capture fournit-elle?
  2. Pourquoi les fonctions normales n’ont-elles pas besoin de cette information?
  3. Les fonctions lambda sont-elles plus que des fonctions sans nom?

A partir du lien de syntaxe que vous avez donné, la liste de capture “définit ce qui doit être disponible à l’extérieur du lambda dans le corps de la fonction et comment”

Les fonctions ordinaires peuvent utiliser des données externes de plusieurs manières:

  1. Champs statiques
  2. Champs d’instance
  3. Paramètres (y compris les parameters de référence)
  4. Globales

Lambda ajoute la possibilité d’avoir une fonction non nommée dans une autre. Le lambda peut alors utiliser les valeurs que vous spécifiez. Contrairement aux fonctions ordinaires, cela peut inclure des variables locales d’une fonction externe.

Comme le dit cette réponse, vous pouvez également spécifier comment vous souhaitez capturer. Awoodland donne quelques exemples dans une autre réponse . Par exemple, vous pouvez capturer une variable externe par référence (comme un paramètre de référence) et toutes les autres par valeur:

 [=, &epsilon] 

MODIFIER:

Il est important de faire la distinction entre la signature et ce que le lambda utilise en interne. La signature d’un lambda est la liste ordonnée des types de parameters, ainsi que le type de la valeur renvoyée.

Par exemple, une fonction unaire prend une valeur unique d’un type particulier et renvoie une valeur d’un autre type.

Cependant, en interne, il peut utiliser d’autres valeurs. Comme exemple sortingvial:

 [x, y](int z) -> int { return x + y - z; } 

L’appelant du lambda sait seulement qu’il prend un int et renvoie un int . Cependant, en interne, il arrive d’utiliser deux autres variables par valeur.

Le problème fondamental que nous essayons de résoudre est qu’un algorithme attend une fonction qui ne prend qu’un ensemble d’arguments particulier (un int dans votre exemple). Cependant, nous voulons que la fonction puisse manipuler ou inspecter un autre object, peut-être comme ceci:

 void what_we_want(int n, std::set const & conditions, int & total) { if (conditions.find(n) != conditions.end()) { total += n; } } 

Cependant, tout ce que nous avons le droit de donner à notre algorithme est une fonction comme void f(int) . Alors, où mettons-nous les autres données?

Vous pouvez soit conserver les autres données dans une variable globale, soit suivre l’approche traditionnelle C ++ et écrire un foncteur:

 struct what_we_must_write { what_we_must_write(std::set const & s, int & t) : conditions(s), total(t) { } void operator()(int n) { if (conditions.find(n) != conditions.end()) { total += n; } } private: std::set const & conditions; int & total; }; 

Nous pouvons maintenant appeler l’algorithme avec un foncteur correctement initialisé:

 std::set conditions; int total; for_each(v.begin(), v.end(), what_we_must_write(conditions, total)); 

Enfin, un object de fermeture (qui est décrit par une expression lambda ) n’est en réalité qu’un moyen d’écrire un foncteur. L’équivalent du foncteur ci-dessus est le lambda

 auto what_we_get = [&conditions, &total](int n) -> void { if (condiditons.find(n) != conditions.end()) { total += n; } }; 

Les listes de capture abrégées [=] et [&] capturent simplement “tout” (respectivement par valeur ou par référence), ce qui signifie que le compilateur calcule la liste de capture concrète pour vous (il ne met pas tout dans la object de fermeture, mais seulement les choses dont vous avez besoin).

Donc, en résumé: un object de fermeture sans capture est comme une fonction libre, et une fermeture avec capture est comme un object foncteur avec des objects membres privés correctement définis et initialisés.

Il est peut-être préférable de penser à une expression lambda en tant qu’object ayant l’opérateur () plutôt que simplement une fonction. L ‘”object” lambda peut avoir des champs qui mémorisent (ou “capturent”) les variables hors lambda au moment de la construction de lambda, pour être utilisées ultérieurement au moment de l’exécution de lambda.

La liste de capture est simplement une déclaration de tels champs.

(Vous n’avez même pas besoin de spécifier vous-même la liste de capture – La syntaxe [&] ou [=] indique au compilateur de déterminer automatiquement la liste de capture, en fonction des variables de l’étendue externe utilisées dans le corps lambda.)

Une fonction normale ne peut pas contenir d’état – elle ne peut pas “se souvenir” d’arguments à un moment donné pour être utilisée à un autre moment. Un lambda peut. Une classe conçue manuellement avec un opérateur implémenté par l’utilisateur () (ou “functor”) peut également l’être, mais est beaucoup moins pratique du sharepoint vue syntaxique.

Considère ceci:

 std::function make_generator(int const& i) { return [i] { return i; }; } // could be used like so: int i = 42; auto g0 = make_generator(i); i = 121; auto g1 = make_generator(i); i = 0; assert( g0() == 42 ); assert( g1() == 121 ); 

Notez que dans cette situation, les deux générateurs créés ont chacun leur propre i . Ce n’est pas quelque chose que vous pouvez recréer avec des fonctions ordinaires et, par conséquent, pourquoi celles-ci n’utilisent pas de listes de capture. Les listes de capture résolvent une version du problème de funarg .

Les fonctions lambda sont-elles plus que des fonctions sans nom?

C’est une question très intelligente. Ce que les expressions lambda créent sont en réalité plus puissants que les fonctions habituelles: ce sont des fermetures (et la norme fait en effet référence aux objects créés par les expressions lambda en tant qu ‘«objects de fermeture»). En bref, une fermeture est une fonction combinée à une étendue liée. La syntaxe C ++ a choisi de représenter le bit de fonction sous une forme familière (liste d’arguments avec type de retour retardé avec corps de la fonction, certaines parties étant facultatives), tandis que la liste de capture est la syntaxe qui spécifie la variable locale qui participera à la scope liée (non local les variables sont introduites automatiquement).

Notez que les autres langages avec des fermetures n’ont généralement pas de construction similaire aux listes de capture C ++. C ++ a fait le choix de conception des listes de capture en raison de son modèle de mémoire (la variable locale ne vit que tant que la scope locale) et de sa philosophie consistant à ne pas payer pour ce que vous n’utilisez pas (les variables locales ont désormais une durée de vie plus longue si elles sont automatiquement utilisées) sont capturés peut ne pas être souhaitable dans chaque cas).

Les fonctions lambda sont-elles plus que des fonctions sans nom?

OUI! En plus d’être sans nom, ils peuvent faire référence aux variables de la scope délimitée lexicalement. Dans votre cas, un exemple serait la variable sum_of_elems (ce n’est ni un paramètre ni une variable globale, je suppose). Les fonctions normales en C ++ ne peuvent pas faire cela.

Quelles informations supplémentaires une liste de capture fournit-elle?

La liste de capture fournit les

  1. liste des variables à capturer avec
  2. informations comment doivent-elles être saisies (par référence / par valeur)

Dans d’autres langages (par exemple fonctionnels), cela n’est pas nécessaire, car ils font toujours référence aux valeurs d’une manière (par exemple, si les valeurs sont immuables, la capture sera par valeur; une autre possibilité est que tout est une variable du tas de sorte que tout est capturé par référence et que vous ne devez pas vous inquiéter de sa durée de vie, etc.). En C ++, vous devez le spécifier pour choisir entre référence (peut changer la variable à l’extérieur, mais explose lorsque le lambda survit à la variable) et valeur (tous les changements isolés à l’intérieur du lambda, mais vivra aussi longtemps que le lambda – fondamentalement , ce sera un champ dans une structure représentant le lambda).

Vous pouvez faire en sorte que le compilateur capture toutes les variables nécessaires en utilisant le symbole capture-default , qui spécifie simplement le mode de capture par défaut (dans votre cas: & => référence; = serait valeur). Dans ce cas, toutes les variables référencées dans le lambda à partir de l’étendue externe sont capturées.