Qu'est-ce qu'une expression lambda en C++ 11? Quand pourrais-je en utiliser un? Quelle classe de problèmes résolvent-ils qui n'était pas possible avant leur introduction?
Quelques exemples et cas d'utilisation seraient utiles.
C++ inclut des fonctions génériques utiles comme std::for_each
et std::transform
, qui peuvent être très pratiques. 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 <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 que f
une fois et à cet endroit précis, il semble exagéré d'écrire toute une classe juste pour faire quelque chose de trivial et ponctuel.
En C++ 03, vous pourriez être tenté d’écrire quelque chose comme ceci pour garder le foncteur en local:
void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}
cependant, ceci n'est pas autorisé, f
ne peut pas être passé à une fonction template en C++ 03.
C++ 11 introduit lambdas qui vous permet d’écrire un foncteur anonyme en ligne pour remplacer le struct f
. Pour de petits exemples simples, cela peut être plus clair à lire (cela permet de tout garder au même 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; }
);
}
toutefois, lorsque vous commencerez à é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 ce problème, 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 rien utilisé d'autre que ce qui a été transmis au lambda, mais nous pouvons également utiliser d'autres variables, au sein du lambda. Si vous souhaitez accéder à d'autres variables, vous pouvez utiliser la clause de capture (le []
de l'expression), qui n'a jusqu'à présent pas été utilisé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, ce que vous pouvez spécifier en utilisant respectivement &
et =
:
[&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 les variables comme avec [&], mais epsilon par valeur[=, &epsilon]
capture les variables comme avec [=], mais epsilon par référenceLa operator()
générée est const
par défaut. L'implication que les captures capturées sera const
lorsque vous y accéderez 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 la operator()
produite ne soit 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 et non en théorie) pour de courts extraits de code impossibles à réutiliser et ne méritant pas 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 d'arguments et {}
le corps de la fonction.
La liste de capture définit ce qui doit être disponible à l'extérieur du lambda dans le corps de la fonction et comment. Cela peut être soit:
Vous pouvez mélanger n'importe lequel des éléments ci-dessus dans une liste séparée par des virgules [x, &y]
.
La liste des arguments est la même que dans toute autre fonction C++.
Le code qui sera exécuté lorsque le lambda est appelé.
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)
.
Si un lambda est marqué comme étant mutable (par exemple, []() mutable { }
), il est autorisé à muter les valeurs capturées par valeur.
La bibliothèque définie par la norme ISO profite énormément des lambdas et augmente la facilité d'utilisation de plusieurs barres, car les utilisateurs n'ont désormais plus à encombrer leur code avec de petits foncteurs accessibles.
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 extrait 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;};
Les Lambdas peuvent maintenant être génériques (auto
serait équivalent à T
ici si T
était un argument de modèle de type quelque part dans la portée environnante):
auto lambda = [](auto x, auto y) {return x + y;};
C++ 14 autorise les types de retour déduits pour chaque fonction et ne le limite pas aux fonctions de la forme 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 de:
[&](){ ...your code... }(); // immediately executed lambda expression
est fonctionnellement équivalent à
{ ...your code... } // simple code block
Cela fait des expressions lambdaun puissant outil de refactorisation de fonctions complexes. Vous commencez par envelopper 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 de &
), 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 pourinitialiser 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!
Commeun moyen de partitionner la logique de votre programme, vous trouverez peut-être même utile de passer une expression lambda en tant qu'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 fonctions nommées nested , ce qui peut être un moyen pratique d'éviter la duplication de la logique. L'utilisation de lambdas nommés a également tendance à être un peu plus facile pour les yeux (par rapport aux lambdas en ligne anonymes) lors du passage d'une fonction non triviale en tant que paramètre à une autre fonction. Remarque: 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 importante pour l’objet fonction, vous pouvez choisir de la réécrire en tant que fonction normale.
Réponses
Q: Qu'est-ce qu'une expression lambda en C++ 11?
R: Sous le capot, il fait l'objet d'une classe auto-générée avec surcharge operator () 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 intégration complète.
Q: Quand devrais-je en utiliser un?
R: Définir la "logique simple et petite" et demander au compilateur de générer la génération de la question précédente. Vous donnez au compilateur des expressions dans lesquelles vous voulez être à l'intérieur de operator (). Toutes les autres choses que le compilateur va générer pour vous.
Q: Quelle classe de problèmes résolvent-ils qui n’était pas possible avant leur introduction?
R: C'est une sorte de sucre de syntaxe, comme une surcharge d'opérateurs au lieu de fonctions pour des opérations personnalisées add, subrtact ... Mais cela enregistre davantage de lignes de code inutiles pour envelopper 1 à 3 lignes de logique réelle dans certaines classes, etc.! Certains ingénieurs pensent que si le nombre de lignes est plus petit, il y a moins de chance de s'y tromper (je pense aussi)
Exemple d'utilisation
auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);
Extras sur lambdas, pas couverts 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 de fonction (fermeture).
[captureVar1,captureVar2](int arg1){}
1.3. Vous pouvez capturer être une référence. & - dans ce contexte, signifie référence et non 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 tous les vars non statiques par valeur, mais par capture de référence Param2
[=,&Param2](int arg1){}
Capturer tous les vars non statiques par référence, mais par capture de valeur Param2
[&,Param2](int arg1){}
2. Retourner le type déduction
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 final. 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 vars locaux, pas les variables membres de l'objet.
4. Сonversions
4.1 !! Lambda n'est pas un pointeur de fonction et ce n'est pas une fonction anonyme, mais capture-less lambdas peut être converti implicitement en pointeur de fonction.
p.s.
Vous trouverez plus d'informations sur les informations de la grammaire lambda dans Working draft for Programming Language C++ # 337, 2012-01-16, 5.1.2. Expressions lambda, p.88
En C++ 14, la fonctionnalité supplémentaire nommée "capture init" a été ajoutée. Il permet d'effectuer des déclarations de fermeture arbitraires des données membres:
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 place pour l'utiliser comme celle transmise à appliquer 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 du standard C++ 11. Si vous souhaitez utiliser des captures, vous devez compter sur la bibliothèque et
#include <functional>
(ou une autre bibliothèque STL telle qu'un algorithme pour l'obtenir indirectement) et ensuite travailler avec std :: function au lieu de transmettre des fonctions normales en tant que paramètres tels que:
#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é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 (à proprement parler incorrecte, mais familièrement) comme lambda, est une notation simplifiée pour définir et utiliser un objet fonction anonyme . Au lieu de définir une classe nommée avec un opérateur (), puis de créer un objet de cette classe et de l'invoquer, nous pouvons utiliser un raccourci.
When would I use one?
Ceci est particulièrement utile lorsque nous voulons passer une opération comme argument à un algorithme. Dans le contexte des interfaces utilisateur graphiques (et ailleurs), ces opérations sont souvent appelées callbacks.
What class of problem do they solve that wasn't possible prior to their introduction?
Ici, je suppose que chaque action effectuée avec l'expression lambda peut être résolue sans eux, mais avec beaucoup plus de code et une complexité bien plus grande. Expression Lambda: c’est le moyen d’optimiser votre code et de le rendre plus attrayant. Aussi triste de Stroustup:
des moyens efficaces d'optimiser
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 supposons 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
1
1
1
1
1
0 triés x - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;
_[]
_ - Ceci est une liste de capture ou _lambda introducer
_: Si lambdas
ne nécessite aucun accès à son environnement local, nous pouvons l'utiliser.
Citation du livre:
Le premier caractère d'une expression lambda est toujours [. Un introducteur lambda peut prendre différentes 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 à des copies des variables locales prises au moment de l’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'objet) par référence ou par valeur. Les variables avec des noms 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 également contenir cela et des noms suivis de ... en tant qu'éléments.
• [&, capture-list] : capture implicitement par référence toutes les variables locales portant des noms non mentionnés dans la liste. La liste de capture peut contenir cela. 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 implicitement par valeur toutes les variables locales dont le nom n’est pas mentionné 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 qu'un nom local non précédé de & est toujours capturé par valeur. Seule la capture par référence permet la modification de variables dans l'environnement appelant.
Additional
_Lambda expression
_ format
Références supplémentaires:
Eh bien, une utilisation pratique que j'ai découverte consiste à réduire le 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 cas bsize
. Vous pouvez bien sûr créer une fonction, mais que se passe-t-il si vous souhaitez limiter l'utilisation dans le cadre de la fonction utilisateur soul? la nature de lambda remplit cette condition et je l’utilise pour ce cas.
Les lambda en c ++ sont traités comme des "fonctions disponibles". oui sa littéralement sur le pouce, vous le définissez; utilise le; et à la fin de la portée de la fonction parent, la fonction lambda a disparu.
c ++ l'a introduit dans c ++ 11 et tout le monde a commencé à l'utiliser comme à n'importe quel endroit possible. l'exemple et ce qui est lambda peuvent être trouvés ici https://en.cppreference.com/w/cpp/language/lambda
Je décrirai ce qui n'est pas là, mais il est essentiel de le savoir pour chaque programmeur c ++
Lambda n'est pas conçu pour être utilisé partout et chaque fonction ne peut pas être remplacée par lambda. Ce n'est pas non plus le plus rapide comparé à la fonction normale. car il a des frais généraux qui doivent être traités par lambda.
cela aidera sûrement à réduire le nombre de lignes dans certains cas. il peut être essentiellement utilisé pour la section de code, qui est appelée dans la même fonction une ou plusieurs fois et cette partie de code n'est pas utilisée nulle part ailleurs pour vous permettre de créer une fonction autonome.
Voici l'exemple de base de lambda et de ce qui se passe en arrière-plan.
Code utilisateur:
int main()
{
// Lambda & auto
int member=10;
auto endGame = [=](int a, int b){ return a+b+member;};
endGame(4,5);
return 0;
}
Comment compile le développe:
int main()
{
int member = 10;
class __lambda_6_18
{
int member;
public:
inline /*constexpr */ int operator()(int a, int b) const
{
return a + b + member;
}
public: __lambda_6_18(int _member)
: member{_member}
{}
};
__lambda_6_18 endGame = __lambda_6_18{member};
endGame.operator()(4, 5);
return 0;
}
comme vous pouvez le constater, quel type de surcharge cela ajoute-t-il lorsque vous l'utilisez. Ce n'est donc pas une bonne idée de les utiliser partout. il peut être utilisé aux endroits où ils sont applicables.
Un problème qu'il résout: Code plus simple que lambda pour un constructeur appelant qui utilise une fonction de paramètre de sortie pour initialiser un membre const
Vous pouvez initialiser un membre const de votre classe en appelant une fonction qui définit sa valeur en rendant sa sortie en tant que paramètre de sortie.