Qu’est-ce qu’une expression lambda en C ++ 11?

Qu’est-ce qu’une expression lambda en C ++ 11? Quand en utiliserais-je un? Quelle classe de problèmes sont-ils résolus qui n’étaient pas possibles avant leur introduction?

Quelques exemples et cas d’utilisation seraient utiles.

Le problème

C ++ inclut des fonctions génériques utiles telles que std::for_each et std::transform , ce qui peut être très pratique. Malheureusement, ils peuvent également être assez lourds à utiliser, en particulier si le foncteur que vous souhaitez appliquer est unique pour une fonction particulière.

 #include  #include  namespace { struct f { void operator()(int) { // do something } }; } void func(std::vector& v) { ff; std::for_each(v.begin(), v.end(), f); } 

Si vous n’utilisez qu’une seule fois et à cet endroit précis, il semble exagéré d’écrire une classe entière simplement pour faire quelque chose de sortingvial et de différent.

En C ++ 03, vous pourriez être tenté d’écrire quelque chose comme ce qui suit pour garder le foncteur local:

 void func2(std::vector& v) { struct { void operator()(int) { // do something } } f; std::for_each(v.begin(), v.end(), f); } 

Cependant, cela n’est pas autorisé, f ne peut pas être transmis à une fonction de modèle en C ++ 03.

La nouvelle solution

C ++ 11 introduit lambdas vous permet d’écrire un foncteur anonyme en ligne pour remplacer la struct f . Pour de petits exemples simples, cela peut être plus lisible (tout garde en un seul endroit) et peut-être plus simple à gérer, par exemple sous la forme la plus simple:

 void func3(std::vector& v) { std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ }); } 

Les fonctions Lambda ne sont que du sucre syntaxique pour les foncteurs anonymes.

Types de retour

Dans les cas simples, le type de retour du lambda est déduit pour vous, par exemple:

 void func4(std::vector& v) { std::transform(v.begin(), v.end(), v.begin(), [](double d) { return d < 0.00001 ? 0 : d; } ); } 

cependant, lorsque vous commencez à écrire des lambda plus complexes, vous rencontrerez rapidement des cas où le type de retour ne peut pas être déduit par le compilateur, par exemple:

 void func4(std::vector& v) { std::transform(v.begin(), v.end(), v.begin(), [](double d) { if (d < 0.0001) { return 0; } else { return d; } }); } 

Pour résoudre ce problème, vous pouvez spécifier explicitement un type de retour pour une fonction lambda, en utilisant -> T :

 void func4(std::vector& v) { std::transform(v.begin(), v.end(), v.begin(), [](double d) -> double { if (d < 0.0001) { return 0; } else { return d; } }); } 

"Capturer" les variables

Jusqu'à présent, nous n'avons rien utilisé d'autre que ce qui a été transmis au lambda, mais nous pouvons également utiliser d'autres variables, dans le lambda. Si vous voulez accéder à d'autres variables, vous pouvez utiliser la clause de capture (le [] de l'expression), qui n'a pas encore été utilisée dans ces exemples, par exemple:

 void func5(std::vector& v, const double& epsilon) { std::transform(v.begin(), v.end(), v.begin(), [epsilon](double d) -> double { if (d < epsilon) { return 0; } else { return d; } }); } 

Vous pouvez capturer à la fois par référence et par valeur, que vous pouvez spécifier à l'aide de & et = respectivement:

  • [&epsilon] capture par référence
  • [&] capture toutes les variables utilisées dans le lambda par référence
  • [=] capture toutes les variables utilisées dans le lambda par valeur
  • [&, epsilon] capture des variables comme avec [&], mais epsilon par valeur
  • [=, &epsilon] capture des variables comme avec [=], mais epsilon par référence

L' operator() généré est par défaut, avec pour conséquence que les captures seront const lorsque vous y accédez par défaut. Cela a pour effet que chaque appel avec la même entrée produirait le même résultat, cependant vous pouvez marquer le lambda comme mutable pour demander que l' operator() qui est produit n'est pas const .

Qu’est-ce qu’une fonction lambda?

Le concept C ++ d’une fonction lambda prend sa source dans le calcul lambda et la functional programming. Un lambda est une fonction sans nom qui est utile (dans la programmation réelle, pas la théorie) pour de courts extraits de code qui sont impossibles à réutiliser et qui ne valent pas la peine d’être nommés.

En C ++, une fonction lambda est définie comme ceci

 []() { } // barebone lambda 

ou dans toute sa gloire

 []() mutable -> T { } // T is the return type, still lacking throw() 

[] est la liste de capture, () la liste des arguments et {} le corps de la fonction.

La liste de capture

La liste de capture définit ce qui doit être disponible depuis l’extérieur du lambda dans le corps de la fonction et comment. Cela peut être soit:

  1. une valeur: [x]
  2. une référence [& x]
  3. toute variable actuellement dans le champ d’application par référence [&]
  4. identique à 3, mais par valeur [=]

Vous pouvez mélanger les éléments ci-dessus dans une liste séparée par des virgules [x, &y] .

La liste des arguments

La liste des arguments est la même que dans toute autre fonction C ++.

Le corps de fonction

Le code qui sera exécuté lorsque le lambda est réellement appelé.

Déduction de type de retour

Si un lambda n’a qu’une seule déclaration de retour, le type de retour peut être omis et possède le type implicite de decltype(return_statement) .

Mutable

Si un lambda est marqué comme étant mutable (par exemple, []() mutable { } ), il est permis de muter les valeurs capturées par valeur.

Cas d’utilisation

La bibliothèque définie par la norme ISO bénéficie énormément de lambdas et augmente la facilité d’utilisation de plusieurs barres car les utilisateurs n’ont plus besoin d’encombrer leur code avec de petits foncteurs dans une scope accessible.

C ++ 14

En C ++ 14 lambdas ont été étendus par diverses propositions.

Captures lambda initialisées

Un élément de la liste de capture peut maintenant être initialisé avec = . Cela permet de renommer les variables et de les capturer en les déplaçant. Un exemple tiré de la norme:

 int x = 4; auto y = [&r = x, x = x+1]()->int { r += 2; return x+2; }(); // Updates ::x to 6, and initializes y to 7. 

et un extrait de Wikipedia montrant comment capturer avec std::move :

 auto ptr = std::make_unique(10); // See below for std::make_unique auto lambda = [ptr = std::move(ptr)] {return *ptr;}; 

Lambdas Générique

Lambdas peut maintenant être générique ( auto équivaudrait à T ici si T était un argument de type template quelque part dans la scope environnante):

 auto lambda = [](auto x, auto y) {return x + y;}; 

Déduction de type de retour améliorée

C ++ 14 autorise des types de retour déduits pour chaque fonction et ne le restreint pas aux fonctions de l’ return expression; formulaire return expression; . Ceci est également étendu aux lambdas.

Les expressions lambda sont généralement utilisées pour encapsuler des algorithmes afin de pouvoir les transmettre à une autre fonction. Cependant, il est possible d’exécuter un lambda immédiatement après la définition :

 [&](){ ...your code... }(); // immediately executed lambda expression 

est fonctionnellement équivalent à

 { ...your code... } // simple code block 

Cela fait des expressions lambda un outil puissant pour la refactorisation de fonctions complexes . Vous commencez par encapsuler une section de code dans une fonction lambda, comme indiqué ci-dessus. Le processus de paramétrage explicite peut ensuite être effectué progressivement avec des tests intermédiaires après chaque étape. Une fois le bloc de code entièrement paramétré (comme le montre la suppression du & ), vous pouvez déplacer le code vers un emplacement externe et en faire une fonction normale.

De même, vous pouvez utiliser des expressions lambda pour initialiser des variables en fonction du résultat d’un algorithme

 int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5! 

Pour partitionner la logique de votre programme , vous pourriez même trouver utile de passer une expression lambda comme argument à une autre expression lambda …

 [&]( std::function algorithm ) // wrapper section { ...your wrapper code... algorithm(); ...your wrapper code... } ([&]() // algorithm section { ...your algorithm code... }); 

Les expressions Lambda vous permettent également de créer des fonctions nestedes nommées, ce qui peut être un moyen pratique d’éviter la logique dupliquée. L’utilisation de lambdas nommés a aussi tendance à être un peu plus facile pour les yeux (comparée aux lambdas en ligne anonymes) lors du passage d’une fonction non sortingviale en tant que paramètre à une autre fonction. Note: n’oubliez pas le point-virgule après l’accolade fermante.

 auto algorithm = [&]( double x, double m, double b ) -> double { return m*x+b; }; int a=algorithm(1,2,3), b=algorithm(4,5,6); 

Si le profilage ultérieur révèle une surcharge d’initialisation significative pour l’object fonction, vous pouvez choisir de le réécrire comme une fonction normale.

Réponses

Q: Qu’est-ce qu’une expression lambda en C ++ 11?

A: Sous le capot, c’est l’object d’une classe générée automatiquement avec un opérateur surchargé () const . Un tel object s’appelle la fermeture et est créé par le compilateur. Ce concept de «fermeture» est proche du concept de liaison de C ++ 11. Mais les lambdas génèrent généralement un meilleur code. Et les appels à travers les fermetures permettent une intégration complète.

Q: Quand en utiliserais-je un?

R: Définir la “logique simple et petite” et demander au compilateur d’effectuer la génération à partir de la question précédente. Vous donnez à un compilateur des expressions que vous souhaitez inclure dans operator (). Tout le rest du compilateur vous générera.

Q: Quelle classe de problèmes est-ce qu’ils résolvent qui n’étaient pas possibles avant leur introduction?

R: C’est une sorte de sucre de syntaxe comme les opérateurs qui surchargent au lieu de fonctions pour les opérations d’ ajout, de soustraction et de soustraitement personnalisées. Certains ingénieurs pensent que si le nombre de lignes est plus petit, il y a moins de chance de faire des erreurs (je le pense aussi)

Exemple d’utilisation

 auto x = [=](int arg1){printf("%i", arg1); }; void(*f)(int) = x; f(1); x(1); 

Extras sur les lambdas, non couverts par la question. Ignorer cette section si vous n’êtes pas intéressé

1. Valeurs capturées. Ce que vous pouvez capturer

1.1. Vous pouvez faire référence à une variable avec une durée de stockage statique dans lambdas. Ils sont tous capturés.

1.2. Vous pouvez utiliser lambda pour les valeurs de capture “par valeur”. Dans ce cas, les vars capturés seront copiés dans l’object fonction (fermeture).

 [captureVar1,captureVar2](int arg1){} 

1.3. Vous pouvez capturer être la référence. & – dans ce contexte, signifie référence, pas de pointeur.

  [&captureVar1,&captureVar2](int arg1){} 

1.4. Il existe une notation pour capturer tous les vars non statiques par valeur ou par référence

  [=](int arg1){} // capture all not-static vars by value [&](int arg1){} // capture all not-static vars by reference 

1.5. Il existe une notation pour capturer tous les vars non statiques par valeur, ou par référence et spécifier smth. plus. Exemples: Capturez toutes les vars non statiques par valeur, mais par capture de référence Param2

 [=,&Param2](int arg1){} 

Capturez toutes les vars non statiques par référence, mais par capture de valeur Param2

 [&,Param2](int arg1){} 

2. Déduction du type de retour

2.1. Le type de retour Lambda peut être déduit si lambda est une expression. Ou vous pouvez le spécifier explicitement.

 [=](int arg1)->trailing_return_type{return trailing_return_type();} 

Si lambda a plus d’une expression, alors le type de retour doit être spécifié via le type de retour final. De même, une syntaxe similaire peut être appliquée aux fonctions automatiques et aux fonctions membres.

3. Valeurs capturées. Ce que vous ne pouvez pas capturer

3.1. Vous ne pouvez capturer que les vars locaux, pas les variables membres de l’object.

4. Сonversions

4.1. lambda n’est pas un pointeur de fonction et ce n’est pas une fonction anonyme , mais peut être implicitement converti en un pointeur de fonction.

ps

  1. Pour plus d’informations sur la grammaire lambda, reportez-vous à la section Version préliminaire du langage de programmation C ++ # 337, 2012-01-16, 5.1.2. Expressions lambda, p.88

  2. En C ++ 14, la fonctionnalité supplémentaire nommée “capture d’initialisation” a été ajoutée. Il permet d’effectuer une déclaration arbitraire des membres de données de fermeture:

     auto toFloat = [](int value) { return float(value);}; auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);}; 

Une fonction lambda est une fonction anonyme que vous créez en ligne. Il peut capturer des variables comme certains l’ont expliqué (par exemple, http://www.stroustrup.com/C++11FAQ.html#lambda ), mais il existe certaines limitations. Par exemple, s’il existe une interface de rappel comme celle-ci,

 void apply(void (*f)(int)) { f(10); f(20); f(30); } 

Vous pouvez écrire une fonction sur-le-champ pour l’utiliser comme celle passée ci-dessous:

 int col=0; void output() { apply([](int data) { cout << data << ((++col % 10) ? ' ' : '\n'); }); } 

Mais tu ne peux pas faire ça:

 void output(int n) { int col=0; apply([&col,n](int data) { cout << data << ((++col % 10) ? ' ' : '\n'); }); } 

en raison des limitations du standard C ++ 11. Si vous souhaitez utiliser des captures, vous devez vous fier à la bibliothèque et

 #include  

(ou une autre bibliothèque STL comme un algorithme pour l'obtenir indirectement) et ensuite travailler avec std :: function au lieu de passer des fonctions normales comme des parameters comme ceci:

 #include  void apply(std::function f) { f(10); f(20); f(30); } void output(int width) { int col; apply([width,&col](int data) { cout << data << ((++col % width) ? ' ' : '\n'); }); } 

Une des meilleures explications de l’ lambda expression est donnée par l’auteur de C ++ Bjarne Stroustrup dans son livre ***The C++ Programming Language*** chapitre 11 ( ISBN-13: 978-0321563842 ):

What is a lambda expression?

Une expression lambda , parfois aussi appelée fonction lambda ou (à tort à tort, mais familièrement) en tant que lambda , est une notation simplifiée pour définir et utiliser un object de fonction anonyme . Au lieu de définir une classe nommée avec un opérateur (), puis de créer un object de cette classe et de l’invoquer, nous pouvons utiliser un raccourci.

When would I use one?

Ceci est particulièrement utile lorsque vous voulez passer une opération en argument à un algorithme. Dans le contexte des interfaces utilisateur graphiques (et ailleurs), ces opérations sont souvent appelées des callbacks .

What class of problem do they solve that wasn't possible prior to their introduction?

Ici, je suppose que toutes les actions effectuées avec l’expression lambda peuvent être résolues sans elles, mais avec beaucoup plus de code et une complexité beaucoup plus grande. Expression lambda, c’est le moyen d’optimiser votre code et de le rendre plus attractif. Aussi sortingste par Stroustup:

moyens efficaces d’optimiser

Some examples

via l’expression lambda

 void print_modulo(const vector& v, ostream& os, int m) // output v[i] to os if v[i]%m==0 { for_each(begin(v),end(v), [&os,m](int x) { if (x%m==0) os << x << '\n'; }); } 

ou via la fonction

 class Modulo_print { ostream& os; // members to hold the capture list int m; public: Modulo_print(ostream& s, int mm) :os(s), m(mm) {} void operator()(int x) const { if (x%m==0) os << x << '\n'; } }; 

ou même

 void print_modulo(const vector& v, ostream& os, int m) // output v[i] to os if v[i]%m==0 { class Modulo_print { ostream& os; // members to hold the capture list int m; public: Modulo_print (ostream& s, int mm) :os(s), m(mm) {} void operator()(int x) const { if (x%m==0) os << x << '\n'; } }; for_each(begin(v),end(v),Modulo_print{os,m}); } 

si vous avez besoin de vous pouvez nommer l' lambda expression comme ci-dessous:

 void print_modulo(const vector& v, ostream& os, int m) // output v[i] to os if v[i]%m==0 { auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; }; for_each(begin(v),end(v),Modulo_print); } 

Ou supposons un autre échantillon simple

 void TestFunctions::simpleLambda() { bool sensitive = true; std::vector v = std::vector({1,33,3,4,5,6,7}); sort(v.begin(),v.end(), [sensitive](int x, int y) { printf("\n%i\n", x < y); return sensitive ? x < y : abs(x) < abs(y); }); printf("sorted"); for_each(v.begin(), v.end(), [](int x) { printf("x - %i;", x); } ); } 

va générer le prochain

0

1

0

1

0

1

0

1

0

1

0 sortingésx - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;

[] - ceci est une liste de capture ou un lambda introducer : si les lambda introducer n'ont pas besoin d'accéder à leur environnement local, nous pouvons l'utiliser.

Citation du livre:

Le premier caractère d'une expression lambda est toujours [ . Un introducteur lambda peut prendre diverses formes:

[] : une liste de capture vide. Cela implique qu'aucun nom local du contexte environnant ne peut être utilisé dans le corps lambda. Pour de telles expressions lambda, les données sont obtenues à partir d’arguments ou de variables non locales.

[&] : capture implicite par référence. Tous les noms locaux peuvent être utilisés. Toutes les variables locales sont accessibles par référence.

[=] : capture implicite par valeur. Tous les noms locaux peuvent être utilisés. Tous les noms font référence aux copies des variables locales sockets au point d'appel de l'expression lambda.

[capture-list]: capture explicite; la liste de capture est la liste des noms des variables locales à capturer (c'est-à-dire stockées dans l'object) par référence ou par valeur. Les variables dont les noms sont précédés de & sont capturées par référence. D'autres variables sont capturées par valeur. Une liste de capture peut également contenir ceci et les noms suivis par ... comme éléments.

[&, capture-list] : capture implicite par référence de toutes les variables locales dont les noms ne sont pas mentionnés dans la liste. La liste de capture peut contenir ceci. Les noms listés ne peuvent pas être précédés de &. Les variables nommées dans la liste de capture sont capturées par valeur.

[=, capture-list] : capture implicite par valeur de toutes les variables locales dont les noms ne sont pas mentionnés dans la liste. La liste de capture ne peut pas contenir cela. Les noms listés doivent être précédés de &. Les variables nommées dans la liste de capture sont capturées par référence.

Notez qu'un nom local précédé de & est toujours capturé par référence et un nom local non précédé de & est toujours capturé par valeur. Seule la capture par référence permet de modifier les variables dans l'environnement appelant.

Additional

Format d' Lambda expression

entrer la description de l'image ici

Références supplémentaires:

  • Wiki
  • open-std.org , chapitre 5.1.2

Un problème qu’il résout: Code plus simple que lambda pour un appel au constructeur qui utilise une fonction de paramètre de sortie pour initialiser un membre const

Vous pouvez initialiser un membre const de votre classe, avec un appel à une fonction qui définit sa valeur en restituant sa sortie en tant que paramètre de sortie.

Eh bien, une utilisation pratique que j’ai trouvée consiste à réduire le code de la plaque de la chaudière. Par exemple:

 void process_z_vec(vector& vec) { auto print_2d = [](const vector& board, int bsize) { for(int i = 0; i 

Sans lambda, vous devrez peut-être faire quelque chose pour différents cas de bsize . Bien sûr, vous pouvez créer une fonction mais que faire si vous souhaitez limiter l'utilisation dans le cadre de la fonction utilisateur soul? la nature de lambda répond à cette exigence et je l'utilise pour ce cas.