Question Qu'est-ce qu'une expression lambda dans C ++ 11?


Qu'est-ce qu'une expression lambda dans C ++ 11? Quand devrais-je en utiliser un? Quelle classe de problème résolvent-ils qui n'était pas possible avant leur introduction?

Quelques exemples, et des cas d'utilisation seraient utiles.


1195
2017-10-02 14:58


origine


Réponses:


Le problème

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

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Si vous n'utilisez f qu'une seule fois et à cet endroit spécifique, il semble exagéré d'écrire une classe entière juste pour faire quelque chose de trivial et un hors.

En C ++ 03 vous pourriez être tenté d'écrire quelque chose comme ceci, pour garder le foncteur local:

void func2(std::vector<int>& 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 passé à une fonction de modèle dans C ++ 03.

La nouvelle solution

C ++ 11 introduit lambdas vous permet d'écrire un foncteur inline, anonyme pour remplacer le struct f. Pour les petits exemples simples, cela peut être plus propre à lire (il garde tout en un seul endroit) et potentiellement plus simple à maintenir, par exemple sous la forme la plus simple:

void func3(std::vector<int>& 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<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

Cependant, lorsque vous commencez à écrire des lambdas 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<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Pour résoudre ceci, vous êtes autorisé à spécifier explicitement un type de retour pour une fonction lambda, en utilisant -> T:

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

"Capturer" des variables

Jusqu'à présent, nous n'avons pas utilisé autre chose 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 capture [] de l'expression), qui a été jusqu'ici inutilisée dans ces exemples, par exemple:

void func5(std::vector<double>& 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 en utilisant & 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

Le généré operator() est const par défaut, avec l'implication 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 produira le même résultat, mais vous pouvez marquer le lambda comme mutable pour demander que le operator() qui est produit n'est pas const.


1206
2017-10-02 15:21



Qu'est-ce qu'une fonction lambda?

Le concept C ++ d'une fonction lambda trouve son origine dans le calcul lambda et la programmation fonctionnelle. Un lambda est une fonction non nommée qui est utile (en programmation réelle, pas en théorie) pour de courts fragments de code qui ne peuvent être réutilisés 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 à l'intérieur du corps de la fonction et comment. Cela peut être:

  1. une valeur: [x]
  2. une référence [& x]
  3. toute variable actuellement dans la portée par référence [&]
  4. Identique à 3, mais en valeur [=]

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

La liste des arguments

La liste d'arguments est la même que dans n'importe quelle autre fonction C ++.

Le corps de la fonction

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

Déduction du type de retour

Si un lambda a seulement une déclaration de retour, le type de retour peut être omis et a le type implicite de decltype(return_statement).

Mutable

Si un lambda est marqué mutable (par ex. []() mutable { }) il est permis de muter les valeurs qui ont été capturées par valeur.

Cas d'utilisation

La bibliothèque définie par la norme ISO profite largement de lambdas et augmente la convivialité de plusieurs barres, car les utilisateurs n'ont plus à encombrer leur code de petits foncteurs dans une portée accessible.

C ++ 14

En C ++ 14, les 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 des 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 pris de Wikipedia montrant comment capturer avec std::move:

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

Générique Lambdas

Lambdas peut maintenant être générique (auto serait équivalent à T ici si T étaient un argument de type template quelque part dans la portée environnante):

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

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

C ++ 14 permet de déduire les types de retour pour chaque fonction et ne le limite pas aux fonctions de la forme return expression;. Ceci est également étendu à lambdas.


718
2017-10-02 15:43



Les expressions lambda sont généralement utilisées pour encapsuler des algorithmes afin qu'ils puissent être transmis à 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 rend les expressions lambda un outil puissant pour refactoriser des 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 que vous avez le bloc de code entièrement paramétré (comme démontré par 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 les 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!

Comme un moyen de partitionner votre logique de programme, vous pourriez même trouver utile de passer une expression lambda comme argument à une autre expression lambda ...

[&]( std::function<void()> 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 noms fonctions imbriquées, ce qui peut être un moyen pratique d'éviter la logique en double. L'utilisation de lambdas nommés a aussi tendance à être un peu plus facile pour les yeux (comparé aux lambdas inline anonymes) lors du passage d'une fonction non triviale en tant que paramètre à une autre fonction. Remarque: ne pas oublier le point-virgule après l'accolade de fermeture.

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 profil suivant révèle un surdébit d'initialisation significatif pour l'objet fonction, vous pouvez choisir de le réécrire comme une fonction normale.


151
2018-03-01 08:08



Réponses

Q: Qu'est-ce qu'une expression lambda dans C ++ 11?

A: Sous le capot, il fait l'objet d'une classe autogénérée avec surcharge opérateur () const. Cet objet est appelé fermeture et 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 inline complète.

Q: Quand en utiliserais-je un?

R: Pour définir "logique simple et petite" et demander au compilateur d'effectuer la génération de la question précédente. Vous donnez au compilateur des expressions que vous voulez voir dans l'opérateur (). Tous les autres compilateurs seront générés pour vous.

Q: Quelle classe de problème résolvent-ils qui n'était pas possible avant leur introduction?

A: Il est une sorte de syntaxe sucre comme les opérateurs de surcharge au lieu de fonctions pour la coutume ajouter, subrtact opérations ... Mais cela économise plus de lignes de code inutiles pour envelopper 1-3 lignes de logique réelle à certaines classes, et ainsi de suite! Certains ingénieurs pensent que si le nombre de lignes est plus petit, il y a moins de chance de faire des erreurs (je pense aussi)

Exemple d'utilisation

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

Extras au sujet de lambdas, non couvert par la question. Ignorez 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 variables capturées seront copiées dans l'objet fonction (fermeture).

[captureVar1,captureVar2](int arg1){}

1.3. Vous pouvez capturer être référence. & - dans ce contexte signifie référence, pas de pointeurs.

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

1.4. Il existe une notation pour capturer toutes les variables 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 toutes les variables non-statiques par valeur, ou par référence et spécifier smth. plus. Exemples: Capture toutes les variables non-statiques par valeur, mais par capture de référence Param2

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

Capture toutes les variables non-statiques par référence, mais par capture de valeur Param2

[&,Param2](int arg1){} 

2. Déduction de 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, le type de retour doit être spécifié via le type de retour.   En outre, 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 pouvez capturer uniquement les variables locales, et non la variable membre de l'objet.

4. Contreversions

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.

p.s. 

  1. Plus d'informations sur les informations de grammaire lambda peuvent être trouvés dans le brouillon de travail pour le 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 "init capture" 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);};
    

30
2018-06-03 16:40



Une fonction lambda est une fonction anonyme que vous créez en ligne. Il peut capturer des variables comme certains l'ont expliqué (par ex. http://www.stroustrup.com/C++11FAQ.html#lambda) mais il y a quelques limites. 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 place pour l'utiliser comme celle passée pour postuler ci-dessous:

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

Mais vous ne pouvez pas faire ceci:

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

en raison des limitations de la norme C ++ 11. Si vous voulez utiliser des captures, vous devez compter sur la bibliothèque et

#include <functional> 

(ou un autre algorithme similaire à la bibliothèque STL pour l'obtenir indirectement) et ensuite travailler avec std :: function au lieu de passer les fonctions normales comme des paramètres comme ceci:

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

12
2018-03-10 22:36



Une des meilleures explications de lambda expression est donné 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é lambda   fonction ou (à proprement parler incorrectement, mais familièrement) en tant que    lambda, est une notation simplifiée pour définir et utiliser un objet de fonction anonyme. Au lieu de définir une classe nommée avec un opérateur (), faire plus tard un objet de cette classe, et enfin   en l'invoquant, nous pouvons utiliser un raccourci.

When would I use one?

Ceci est particulièrement utile lorsque nous voulons passer une opération en tant que   argument à un algorithme. Dans le contexte des interfaces utilisateur graphiques   (et ailleurs), de telles opérations sont souvent appelées rappels.

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

Ici je suppose que chaque action faite avec l'expression lambda peut être résolue sans eux, mais avec beaucoup plus de code et beaucoup plus de complexité. Lambda expression c'est le moyen d'optimisation pour votre code et un moyen de le rendre plus attractif. Aussi triste que Stroustup:

des moyens efficaces d'optimisation

Some examples

via l'expression lambda

void print_modulo(const vector<int>& 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<int>& 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, vous pouvez nommer lambda expression comme ci-dessous:

void print_modulo(const vector<int>& 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 supposez un autre échantillon simple

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({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 la prochaine

0

1

0

1

0

1

0

1

0

1

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

[] - Ceci est la liste de capture ou lambda introducer: si lambdas ne nécessitent pas d'accès à leur environnement local, nous pouvons l'utiliser.

Citation du livre:

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

[]: une liste de capture vide. Ce   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   arguments ou à partir de variables non locales.

[&]: capturer implicitement par   référence. Tous les noms locaux peuvent être utilisés. Toutes les variables locales sont   consulté par référence.

[=]: capture implicitement par valeur. Tous les locaux   les noms peuvent être utilisés. Tous les noms se réfèrent à des copies des variables locales   pris au point d'appel de l'expression lambda.

[capture-list]:  capture explicite; la liste de capture est la liste des noms de variables locales à capturer (c'est-à-dire stockées dans l'objet) 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. Les autres variables sont capturées par valeur. Une liste de capture peut   contient aussi ceci et les noms suivis par ... comme éléments.

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

[=, capture-list]: capture implicitement par valeur toutes les variables locales avec des noms non 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é par & est toujours capturé par   valeur. Seule la capture par référence permet de modifier les variables   l'environnement d'appel.

Additional

Lambda expression format

enter image description here

Références supplémentaires:


6
2017-11-09 11:02



Un problème qu'il résout: Code plus simple que lambda pour un appel dans le 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 donnant sa sortie en tant que paramètre de sortie.


1
2018-06-27 00:38



Eh bien, une utilisation pratique que j'ai découverte est la réduction du code de plaque de chaudière. Par exemple:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

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


1
2017-11-23 09:16