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.
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.
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.
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.
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;
}
});
}
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érenceLe 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
.
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 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:
Vous pouvez mélanger l'un des éléments ci-dessus dans une liste séparée par des virgules [x, &y]
.
La liste d'arguments est la même que dans n'importe quelle autre fonction C ++.
Le code qui sera exécuté quand le lambda est réellement appelé.
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)
.
Si un lambda est marqué mutable (par ex. []() mutable { }
) il est permis de muter les valeurs qui ont été capturées par valeur.
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.
En C ++ 14, les lambdas ont été étendus par diverses propositions.
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;};
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;};
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.
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.
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.
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
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);};
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');
});
}
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
Références supplémentaires:
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.
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.