Bonjour mes amis. Aujourd'hui, nous avons préparé pour vous une traduction de la première partie de l'article
«Lambdas: du C ++ 11 au C ++ 20» . La publication de ce matériel est programmée pour coïncider avec le lancement du cours
"Développeur C ++" , qui commence demain.
Les expressions lambda sont l'un des ajouts les plus puissants de C ++ 11 et continuent d'évoluer avec chaque nouvelle norme de langage. Dans cet article, nous allons passer en revue leur histoire et regarder l'évolution de cette partie importante du C ++ moderne.

La deuxième partie est disponible ici:
Lambdas: du C ++ 11 au C ++ 20, partie 2EntréeLors d'une réunion locale du groupe d'utilisateurs C ++, nous avons eu une session de programmation en direct sur «l'historique» des expressions lambda. La conversation a été dirigée par Tomasz Kamiński, expert en C ++ (
voir le profil Linkedin de Thomas ). Voici l'événement:
Lambdas: de C ++ 11 à C ++ 20 - Groupe d'utilisateurs C ++ CracovieJ'ai décidé de prendre le code de Thomas (avec sa permission!), De le décrire et de créer un article séparé.
Nous allons commencer par explorer C ++ 03 et la nécessité d'expressions fonctionnelles locales compactes. Ensuite, nous passons à C ++ 11 et C ++ 14. Dans la deuxième partie de la série, nous verrons des changements en C ++ 17 et même regarderons ce qui se passera en C ++ 20.
Lambdas en C ++ 03Dès le début, les
std::algorithms
STL, tels que
std::sort
, pouvaient prendre n'importe quel objet appelé et l'appeler sur des éléments de conteneur. Cependant, en C ++ 03, cela n'impliquait que des pointeurs vers des fonctions et des foncteurs.
Par exemple:
#include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Code en cours d'exécution:
@WandboxMais le problème était que vous deviez écrire une fonction ou un foncteur séparé dans une portée différente, et non dans la portée de l'appel d'algorithme.
En tant que solution potentielle, vous pourriez envisager d'écrire une classe de foncteur locale - car C ++ prend toujours en charge cette syntaxe. Mais ça ne marche pas ...
Jetez un oeil à ce code:
int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Essayez de le compiler avec
-std=c++98
et vous verrez l'erreur suivante dans GCC:
error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor'
Essentiellement, en C ++ 98/03, vous ne pouvez pas créer une instance d'un modèle avec un type local.
En raison de toutes ces limitations, le Comité a commencé à développer une nouvelle fonctionnalité que nous pouvons créer et appeler «en place» ... «expressions lambda»!
Si nous regardons
N3337 - la version finale de C ++ 11, nous verrons une section distincte pour lambdas:
[expr.prim.lambda] .
À côté de C ++ 11Je pense que les lambdas ont été judicieusement ajoutés à la langue. Ils utilisent la nouvelle syntaxe, mais ensuite le compilateur «l'étend» à une classe réelle. Ainsi, nous avons tous les avantages (et parfois les inconvénients) d'un vrai langage strictement typé.
Voici un exemple de code de base qui montre également l'objet functor local correspondant:
#include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); }
Exemple:
@WandBoxVous pouvez également consulter CppInsights, qui montre comment le compilateur étend le code:
Jetez un œil à cet exemple:
CppInsighs: test lambdaDans cet exemple, le compilateur convertit:
[] (int x) { std::cout << x << '\n'; }
En quelque chose de similaire à cela (forme simplifiée):
struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance;
Syntaxe d'expression lambda:
[] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | | |
Quelques définitions avant de commencer:
De
[expr.prim.lambda # 2] :
L'évaluation d'une expression lambda entraîne une valeur temporaire. Cet objet temporaire est appelé un
objet de fermeture .
Et de
[expr.prim.lambda # 3] :
Le type d'expression lambda (qui est également le type d'un objet de fermeture) est un type de non-union sans nom unique de la classe appelée
type de fermeture .
Quelques exemples d'expressions lambda:
Par exemple:
[](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; }
Type lambdaComme le compilateur génère un nom unique pour chaque lambda, il n'est pas possible de le connaître à l'avance.
auto myLambda = [](int a) -> double { return 2.0 * a; }
De plus
[expr.prim.lambda] :
Le type de fermeture associé à l'expression lambda a un constructeur par défaut distant ([dcl.fct.def.delete]) et un opérateur d'affectation distant.
Par conséquent, vous ne pouvez pas écrire:
auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy;
Cela entraînera l'erreur suivante dans GCC:
error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor
Opérateur d'appelLe code que vous mettez dans le corps lambda est "traduit" en code operator () du type de fermeture correspondant.
Par défaut, il s'agit d'une méthode constante intégrée. Vous pouvez le changer en spécifiant mutable après avoir déclaré les paramètres:
auto myLambda = [](int a) mutable { std::cout << a; }
Bien que la méthode constante ne soit pas un "problème" pour un lambda sans liste de capture vide ... cela importe quand vous voulez capturer quelque chose.
Capturer[] introduit non seulement un lambda, mais contient également une liste de variables capturées. C'est ce qu'on appelle une liste de capture.
En capturant une variable, vous créez un membre de copie de cette variable dans le type de fermeture. Ensuite, à l'intérieur du corps lambda, vous pouvez y accéder.
La syntaxe de base est:
- [&] - capture par référence, toutes les variables du stockage automatique sont déclarées dans la portée
- [=] - capture par valeur, la valeur est copiée
- [x, & y] - capture explicitement x par valeur et y par référence
Par exemple:
int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; }
Vous pouvez jouer avec l'exemple complet ici:
@WandboxBien que spécifier
[=]
ou
[&]
peut être pratique - car il capture toutes les variables dans le stockage automatique, il est plus évident de capturer les variables explicitement. Ainsi, le compilateur peut vous avertir des effets indésirables (voir, par exemple, les notes sur les variables globales et statiques)
Vous pouvez également en savoir plus au paragraphe 31 de Effective Modern C ++ de Scott Meyers: «Évitez les modes de capture par défaut».
Et une citation importante:
Les fermetures C ++ n'augmentent pas la durée de vie des liens capturés.
MutablePar défaut, l'opérateur de type de fermeture () est constant et vous ne pouvez pas modifier les variables capturées à l'intérieur du corps d'une expression lambda.
Si vous souhaitez modifier ce comportement, vous devez ajouter le mot clé mutable après la liste des paramètres:
int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl;
Dans l'exemple ci-dessus, nous pouvons changer les valeurs de x et y ... mais ce ne sont que des copies de x et y de la portée attachée.
Capture de variable globaleSi vous avez une valeur globale et que vous utilisez ensuite [=] dans un lambda, vous pourriez penser que la valeur globale est également capturée par valeur ... mais ce n'est pas le cas.
int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); }
Vous pouvez jouer avec le code ici:
@Wandbox
Seules les variables du stockage automatique sont capturées. GCC peut même émettre l'avertissement suivant:
warning: capture of variable 'global' with non-automatic storage duration
Cet avertissement n'apparaîtra que si vous capturez explicitement la variable globale, donc si vous utilisez
[=]
, le compilateur ne vous aidera pas.
Le compilateur Clang est plus utile car il génère une erreur:
error: 'global' cannot be captured because it does not have automatic storage duration
Voir
@WandboxCapture de variables statiquesLa capture de variables statiques est similaire à la capture globale:
#include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); }
Vous pouvez jouer avec le code ici:
@WandboxConclusion:
10 11 12
Et encore, un avertissement n'apparaîtra que si vous capturez explicitement une variable statique, donc si vous utilisez
[=]
, le compilateur ne vous aidera pas.
Capture des membres de la classeSavez-vous ce qui se passe après l'exécution du code suivant:
#include <iostream> #include <functional> struct Baz { std::function<void()> foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
Le code déclare un objet Baz puis appelle
foo()
. Notez que
foo()
retourne un lambda (stocké dans
std::function
) qui capture un membre de la classe.
Puisque nous utilisons des objets temporaires, nous ne pouvons pas être sûrs de ce qui se passera lorsque f1 et f2 seront appelés. Il s'agit d'un problème de lien pendant qui provoque un comportement indéfini.
De même:
struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo();
Jouez avec le code
@WandboxEncore une fois, si vous spécifiez explicitement la capture ([s]):
std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; }
Le compilateur empêchera votre erreur:
In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ...
Voir un exemple:
@WandboxObjets mobiles uniquementSi vous avez un objet qui ne peut être déplacé (par exemple, unique_ptr), vous ne pouvez pas le placer dans un lambda en tant que variable capturée. La capture par valeur ne fonctionne pas, vous ne pouvez donc capturer que par référence ... cependant, cela ne vous sera pas transféré, et ce n'est probablement pas ce que vous vouliez.
std::unique_ptr<int> p(new int[10]); auto foo = [p] () {};
Enregistrement des constantesSi vous capturez une variable constante, la constance est préservée:
int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo();
Voir le code:
@WandboxType de retourEn C ++ 11, vous pouvez ignorer la
trailing
type de retour lambda, puis le compilateur le restituera pour vous.
Initialement, la sortie du type de valeur de retour était limitée aux lambdas contenant une instruction de retour, mais cette restriction a été rapidement supprimée, car il n'y avait aucun problème avec l'implémentation d'une version plus pratique.
Voir les
rapports de défauts de langage standard C ++ et les problèmes acceptés (merci à Thomas d'avoir trouvé le bon lien!)
Ainsi, à partir de C ++ 11, le compilateur peut déduire le type de la valeur de retour si toutes les instructions de retour peuvent être converties dans le même type.
Si toutes les instructions de retour renvoient l'expression et les types de retour après la conversion lvalue-to-rvalue (7.1 [conv.lval]), array-to-pointer (7.2 [conv.array]) et function-to-pointer (7.3 [conv. func]) est le même que le type générique;
auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; };
Vous pouvez jouer avec le code ici:
@WandboxIl y a deux
return
dans le lambda ci-dessus, mais elles pointent toutes vers le
double
, de sorte que le compilateur peut déduire le type.
IIFE - Expression de fonction immédiatement invoquéeDans nos exemples, j'ai défini un lambda, puis je l'ai appelé à l'aide de l'objet de fermeture ... mais il peut également être appelé immédiatement:
int x = 1, y = 1; [&]() { ++x; ++y; }();
Une telle expression peut être utile dans l'initialisation complexe d'objets constants.
const auto val = []() { }();
J'ai écrit plus à ce sujet dans le
post IIFE for Complex Initialization .
Convertir en pointeur de fonctionLe type de fermeture pour une expression lambda sans capture a une fonction implicite non virtuelle ouverte de conversion d'une constante en pointeur en une fonction qui a le même paramètre et les mêmes types de retour que l'opérateur d'appel d'une fonction du type de fermeture. La valeur renvoyée par cette fonction de conversion doit être l'adresse de la fonction qui, lorsqu'elle est appelée, a le même effet que d'appeler l'opérateur d'une fonction d'un type similaire à un type de fermeture.
En d'autres termes, vous pouvez convertir des lambdas sans captures en un pointeur de fonction.
Par exemple:
#include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); }
Vous pouvez jouer avec le code ici:
@WandboxAméliorations dans C ++ 14Norme N4140 et lambda:
[expr.prim.lambda] .
C ++ 14 a ajouté deux améliorations significatives aux expressions lambda:
- Captures avec initialiseur
- Lambdas communs
Ces fonctionnalités résolvent plusieurs problèmes visibles en C ++ 11.
Type de retourLa sortie du type de valeur de retour de l'expression lambda a été mise à jour pour se conformer aux règles de sortie automatique des fonctions.
[expr.prim.lambda # 4]Le type de retour du lambda est auto, qui est remplacé par le type de retour de fin, s'il est fourni et / ou déduit des instructions de retour, comme décrit dans [dcl.spec.auto].
Captures avec initialiseurEn bref, nous pouvons créer une nouvelle variable membre du type de fermeture, puis l'utiliser dans l'expression lambda.
Par exemple:
int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); }
Cela peut résoudre plusieurs problèmes, par exemple, avec des types qui ne sont disponibles que pour le déplacement.
DéménagementMaintenant, nous pouvons déplacer l'objet vers un membre du type de fermeture:
#include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; }
OptimisationUne autre idée est de l'utiliser comme technique d'optimisation potentielle. Au lieu de calculer une valeur à chaque fois que nous appelons le lambda, nous pouvons le calculer une fois dans l'initialiseur:
#include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); }
Capturer une variable membreUn initialiseur peut également être utilisé pour capturer une variable membre. Ensuite, nous pouvons obtenir une copie de la variable membre et ne pas nous soucier des liens pendants.
Par exemple:
struct Baz { auto foo() { return [s=s] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
Vous pouvez jouer avec le code ici:
@Wandbox
Dans
foo()
nous capturons une variable membre en la copiant dans le type de fermeture. De plus, nous utilisons auto pour afficher l'intégralité de la méthode (auparavant, en C ++ 11, nous pouvions utiliser
std::function
).
Expressions lambda génériquesUne autre amélioration significative est le lambda généralisé.
À partir de C ++ 14, vous pouvez écrire:
auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world");
Cela équivaut à utiliser une déclaration de modèle dans une instruction d'appel de type fermeture:
struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance;
Un tel lambda généralisé peut être très utile lorsqu'il est difficile de déduire un type.
Par exemple:
std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } };
Ai-je tort ici? L'entrée a-t-elle le bon type?
.
.
.
Probablement pas, car le type de valeur pour std :: map est
std::pair<const Key, T>
. Donc mon code fera des copies supplémentaires des lignes ...
Cela peut être corrigé avec
auto
:
std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } );
Vous pouvez jouer avec le code ici:
@WandboxConclusionQuelle histoire!
Dans cet article, nous sommes partis des premiers jours des expressions lambda en C ++ 03 et C ++ 11 et sommes passés à une version améliorée en C ++ 14.
Vous avez vu comment créer un lambda, quelle est la structure de base de cette expression, qu'est-ce qu'une liste de capture, et bien plus encore.
Dans la prochaine partie de l'article, nous allons passer à C ++ 17 et découvrir les futures fonctionnalités de C ++ 20.
La deuxième partie est disponible ici:
Lambdas: du C ++ 11 au C ++ 20, partie 2
Les références
C ++ 11 -
[expr.prim.lambda]C ++ 14 -
[expr.prim.lambda]Expressions lambda en C ++ | Documents MicrosoftDémystifier les lambdas C ++ - Sticky Bits - Propulsé par Feabhas; Sticky Bits - Propulsé par Feabhas
Nous attendons vos commentaires et invitons toutes les personnes intéressées par le cours
"Développeur C ++" .