Qu'est-ce qu'une expression lambda en C ++ 11? Quand devrais-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.
Qu'est-ce qu'une expression lambda en C ++ 11? Quand devrais-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.
Réponses:
C ++ inclut des fonctions génériques utiles comme std::for_each
et std::transform
, qui peuvent être très utiles. Malheureusement, ils peuvent également être assez lourds à utiliser, en particulier si le foncteur que vous souhaitez appliquer est unique pour 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 ne l'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 unique.
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é et f
ne peut pas être transmis à une fonction de modèle en C ++ 03.
C ++ 11 introduit lambdas vous permet d'écrire un foncteur anonyme en ligne pour remplacer le struct f
. Pour les petits exemples simples, cela peut être plus propre à lire (il garde tout 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; }
);
}
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 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 utilisé rien d'autre que ce qui a été transmis au lambda en son sein, 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 (celle []
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, 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éderez par défaut. Cela a pour effet que chaque appel avec la même entrée produirait le même résultat, mais vous pouvez marquer le lambdamutable
pour demander que ce operator()
qui est produit ne le soit pas const
.
const
toujours ...
()
- il est passé comme un lambda à zéro argument, mais parce () const
qu'il ne correspond pas au lambda, il recherche une conversion de type qui le permet, qui inclut le cast implicite -to-function-pointer, puis appelle ça! Sournois!
std::function<double(int, bool)> f = [](int a, bool b) -> double { ... };
Mais généralement, nous laissons le compilateur déduire le type: auto f = [](int a, bool b) -> double { ... };
(et n'oubliez pas de #include <functional>
)
return d < 0.00001 ? 0 : d;
est garanti de retourner le double, lorsque l'un des opérandes est une constante entière (c'est à cause d'une règle de promotion implicite de l'opérateur?: Où les 2e et 3e opérandes sont équilibrés les uns par rapport aux autres via l'arithmétique habituelle conversions, quel que soit celui qui est sélectionné). Le fait de changer en 0.0 : d
rendrait peut-être l'exemple plus facile à comprendre.
Le concept C ++ d'une fonction lambda trouve son origine dans le calcul lambda et la programmation fonctionnelle. Un lambda est une fonction sans nom qui est utile (en programmation réelle, pas en théorie) pour de courts extraits de code qui sont impossibles à réutiliser et 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 splendeur
[]() 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 de l'extérieur du lambda à l'intérieur du corps de la fonction et comment. Cela peut être soit:
Vous pouvez mélanger tout ce qui précède dans une liste séparée par des virgules [x, &y]
.
La liste d'arguments est la même que dans toute autre fonction C ++.
Le code qui sera exécuté lorsque le lambda est réellement appelé.
Si un lambda n'a qu'une seule instruction de retour, le type de retour peut être omis et a le type implicite de decltype(return_statement)
.
Si un lambda est marqué comme mutable (par exemple []() mutable { }
), il est autorisé de muter les valeurs qui ont été capturées par valeur.
La bibliothèque définie par la norme ISO bénéficie largement des lambdas et augmente la convivialité de plusieurs barres, car les utilisateurs n'ont plus à encombrer leur code avec 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 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<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
seraient équivalents à T
ici s'il y
T
avait 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 la limite pas aux fonctions du formulaire return expression;
. Ceci est également étendu aux lambdas.
r = &x; r += 2;
mais cela arrive à la valeur d'origine de 4.
Les expressions lambda sont généralement utilisées pour encapsuler des algorithmes afin de 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 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 le bloc de code est entièrement paramétré (comme le dé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<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 imbriquées nommées , ce qui peut être un moyen pratique d'éviter la logique en double. 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 suivant révèle une surcharge d'initialisation importante pour l'objet fonction, vous pouvez choisir de le réécrire en tant que fonction normale.
if
déclarations :, en if ([i]{ for (char j : i) if (!isspace(j)) return false ; return true ; }()) // i is all whitespace
supposant que i
c'est unstd::string
[](){}();
.
(lambda: None)()
syntaxe de Python est tellement plus lisible.
main() {{{{((([](){{}}())));}}}}
Réponses
Q: Qu'est-ce qu'une expression lambda en C ++ 11?
R: Sous le capot, c'est l'objet d'une classe autogénérée avec surcharge opérateur () const . Un tel 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 via les fermetures permettent une intégration complète.
Q: Quand devrais-je en utiliser un?
R: Pour définir une "logique simple et petite" et demander au compilateur d'effectuer la génération à partir de la question précédente. Vous donnez au compilateur quelques expressions que vous voulez voir dans operator (). Tous les autres compilateurs de trucs vont vous générer.
Q: Quelle classe de problèmes résolvent-ils qui n'était pas possible avant leur introduction?
R: Il s'agit d'une sorte de sucre de syntaxe comme la surcharge des opérateurs au lieu des fonctions personnalisées opérations d' ajout, de sous-traitance ... Mais cela enregistre plus de lignes de code inutile pour encapsuler 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 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 sur les lambdas, non 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 capturer des valeurs "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 comme référence. & - dans ce contexte signifie référence, pas 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 tous les vars non statiques par valeur ou par référence et spécifier smth. plus. Exemples: capture tous les vars non statiques par valeur, mais par référence capture Param2
[=,&Param2](int arg1){}
Capture tous les vars non statiques par référence, mais par capture de valeur Param2
[&,Param2](int arg1){}
2. Déduction du type de déclaration
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 de fin. 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 ne pouvez capturer que des variables locales, pas des 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 les lambdas sans capture peuvent être implicitement convertis en pointeur de fonction.
ps
Plus d'informations sur la grammaire lambda peuvent être trouvées dans Working draft for Programming Language C ++ # 337, 2012-01-16, 5.1.2. Expressions Lambda, p.88
Dans C ++ 14, la fonctionnalité supplémentaire nommée "capture d'initialisation" a été ajoutée. Il permet d'effectuer arbitrairement la déclaration des données de fermeture des membres:
auto toFloat = [](int value) { return float(value);};
auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
[&,=Param2](int arg1){}
ne semble pas être une syntaxe valide. La forme correcte serait[&,Param2](int arg1){}
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 y a quelques 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 passée à appliquer ci-dessous:
int col=0;
void output() {
apply([](int data) {
cout << data << ((++col % 10) ? ' ' : '\n');
});
}
Mais vous ne pouvez pas faire ça:
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 souhaitez utiliser des captures, vous devez vous fier à la bibliothèque et
#include <functional>
(ou une autre bibliothèque STL comme un algorithme pour l'obtenir indirectement), puis travaillez avec std :: function au lieu de passer des fonctions normales comme 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');
});
}
apply
c'était un modèle qui acceptait un foncteur, cela fonctionnerait
L'une des meilleures explications 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, 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 (), de créer plus tard un objet de cette classe, et enfin 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 rappels .
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 un moyen de le rendre plus attractif. 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 en 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);
}
);
}
générera ensuite
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;
[]
- c'est la liste de capture ou lambda introducer
: silambdas
n'avez 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 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 implicitement par référence. Tous les noms locaux peuvent être utilisés. Toutes les variables locales sont accessibles par référence.
• [=] : capture implicitement par valeur. Tous les noms locaux peuvent être utilisés. Tous les noms font référence à des copies des variables locales prises au point d'appel de l'expression lambda.
• [capture-list]: capture explicite; la capture-list 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 dont le nom est précédé 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 de ... comme éléments.
• [&, capture-list] : capture implicitement par référence toutes les variables locales dont les noms ne sont pas mentionnés dans la liste. La liste de capture peut contenir cela. Les noms répertorié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 avec des noms non mentionnés dans la liste. La liste de capture ne peut pas contenir cela. Les noms répertorié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 de modifier des variables dans l'environnement appelant.
Additional
Lambda expression
format
Références supplémentaires:
for (int x : v) { if (x % m == 0) os << x << '\n';}
Eh bien, une utilisation pratique que j'ai découverte consiste à réduire le code de plaque de la 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 souhaitez limiter l'utilisation dans le cadre de la fonction utilisateur âme? la nature de lambda remplit cette condition et je l'utilise pour ce cas.
Les lambda en c ++ sont traités comme "fonction disponible à la volée". oui c'est littéralement sur la route, vous le définissez; utilise le; et lorsque la portée de la fonction parent se termine, la fonction lambda a disparu.
c ++ l'a introduit dans c ++ 11 et tout le monde a commencé à l'utiliser comme à tous les endroits possibles. l'exemple et ce qu'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 essentiel à savoir pour chaque programmeur c ++
Lambda n'est pas destiné à être utilisé partout et chaque fonction ne peut pas être remplacée par lambda. Ce n'est pas non plus le plus rapide par rapport à la fonction normale. car il a des frais généraux qui doivent être géré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 ce morceau de code n'est pas nécessaire ailleurs pour que vous puissiez créer une fonction autonome pour cela.
Voici l'exemple de base de lambda et ce qui se passe en arrière-plan.
Code d'utilisateur:
int main()
{
// Lambda & auto
int member=10;
auto endGame = [=](int a, int b){ return a+b+member;};
endGame(4,5);
return 0;
}
Comment compiler 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 voir, quel type de surcharge il ajoute lorsque vous l'utilisez. donc ce n'est 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 appel dans un 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 redonnant sa sortie comme paramètre de sortie.