Lambdas: de C ++ 11 à C ++ 20. 2e partie

Salut, Habrovsk. Dans le cadre du démarrage du recrutement dans un nouveau groupe au cours «Développeur C ++» , nous partageons avec vous la traduction de la deuxième partie de l'article «Lambdas: du C ++ 11 au C ++ 20». La première partie peut être lue ici .



Dans la première partie de la série, nous avons examiné les lambdas en termes de C ++ 03, C ++ 11 et C ++ 14. Dans cet article, j'ai décrit les motivations derrière cette puissante fonctionnalité C ++, l'utilisation de base, la syntaxe et les améliorations de chacune des normes de langage. J'ai également mentionné quelques cas limites.
Il est maintenant temps de passer au C ++ 17 et de regarder vers l'avenir (très proche!): C ++ 20.

Entrée

Petit rappel: l'idée de cette série est venue après l'une de nos récentes réunions du groupe d'utilisateurs C ++ à Cracovie.

Nous avons eu une session de programmation en direct sur «l'histoire» des expressions lambda. La conversation a été menée par l'expert C ++ Thomas Kaminsky ( voir le profil Linkedin de Thomas ). Voici l'événement:
Lambdas: de C ++ 11 à C ++ 20 - C ++ User Group Krakow .

J'ai décidé de prendre le code de Thomas (avec sa permission!) Et d'écrire des articles basés sur lui. Dans la première partie de la série, j'ai parlé des expressions lambda comme suit:

  • Syntaxe de base
  • Type lambda
  • Opérateur d'appel
  • Capture de variables (variables mutables, globales, statiques, membres de classe et ce pointeur, objets mobiles uniquement, stockage de constantes):

    • Type de retour
    • IIFE - Expression de fonction immédiatement invoquée
    • Conversion en pointeur de fonction
    • Type de retour
    • IIFE - Expressions immédiatement appelées
    • Convertir en pointeur de fonction
  • Améliorations dans C ++ 14

    • Sortie de type retour
    • Capture avec initialiseur
    • Capturer une variable membre
    • Expressions lambda génériques

La liste ci-dessus n'est qu'une partie de l'histoire des expressions lambda!

Voyons maintenant ce qui a changé en C ++ 17 et ce que nous obtenons en C ++ 20!

Améliorations en C ++ 17

Norme (projet avant publication) Section N659 sur les lambdas: [expr.prim.lambda] . C ++ 17 a apporté deux améliorations significatives aux expressions lambda:

  • constexpr lambda
  • Capturez * ceci

Que signifient ces innovations pour nous? Voyons cela.

expressions lambda constexpr

À partir de C ++ 17, la norme définit implicitement operator() pour un type lambda comme constexpr , si possible:
De expr.prim.lambda # 4 :
L'opérateur d'appel de fonction est une fonction constexpr si la déclaration du paramètre de condition de l'expression lambda correspondante est suivie de constexpr, ou s'il satisfait aux exigences de la fonction constexpr.

Par exemple:

 constexpr auto Square = [] (int n) { return n*n; }; // implicitly constexpr static_assert(Square(2) == 4); 

Rappelons qu'en C ++ 17 constexpr fonction doit suivre ces règles:

  • il ne doit pas être virtuel;

    • son type de retour doit être un type littéral;
    • chacun des types de ses paramètres doit être un type littéral;
    • son corps doit être = delete, = default ou une instruction composée qui ne contient pas
      • définitions asm
      • expressions de goto,
      • balises
      • essayez de bloquer ou
      • la définition d'une variable non littérale, d'une variable statique ou d'une variable de mémoire en streaming pour laquelle l'initialisation n'est pas effectuée.

Et un exemple plus pratique?

 template<typename Range, typename Func, typename T> constexpr T SimpleAccumulate(const Range& range, Func func, T init) { for (auto &&elem: range) { init += func(elem); } return init; } int main() { constexpr std::array arr{ 1, 2, 3 }; static_assert(SimpleAccumulate(arr, [](int i) { return i * i; }, 0) == 14); } 

Vous pouvez jouer avec le code ici: @Wandbox

Le code utilise constexpr lambda, puis il est transmis à l'algorithme simple SimpleAccumulate . L'algorithme utilise plusieurs éléments C ++ 17: les ajouts constexpr à std::array , std::begin et std::end (utilisés dans une boucle for avec une plage) sont désormais également constexpr , ce qui signifie que tout le code peut être exécuté au moment de la compilation.

Bien sûr, ce n'est pas tout.

Vous pouvez capturer des variables (à condition qu'elles soient également constexpr ):

 constexpr int add(int const& t, int const& u) { return t + u; } int main() { constexpr int x = 0; constexpr auto lam = [x](int n) { return add(x, n); }; static_assert(lam(10) == 10); } 

Mais il existe un cas intéressant où vous ne passez pas plus loin la variable capturée, par exemple:

 constexpr int x = 0; constexpr auto lam = [x](int n) { return n + x }; 

Dans ce cas, à Clang, nous pouvons obtenir l'avertissement suivant:

warning: lambda capture 'x' is not required to be captured for this use

Cela est probablement dû au fait que x peut être changé en place à chaque utilisation (à moins que vous ne le transfériez davantage ou que vous ne preniez l'adresse de ce nom).

Mais dites-moi s'il vous plaît si vous connaissez les règles officielles de ce comportement. Je n'ai trouvé que (à partir de cppreference ) (mais je ne le trouve pas dans le brouillon ...)

(Note du traducteur: comme nos lecteurs l'écrivent, je veux probablement dire substituer la valeur de 'x' à chaque endroit où il est utilisé. Il est définitivement impossible de le changer.)

Une expression lambda peut lire la valeur d'une variable sans la capturer si la variable
* a un entier non-volatile constant ou un type énuméré et a été initialisé avec constexpr ou
* est constexpr et n'a pas de membres mutables.

Soyez prêt pour l'avenir:

En C ++ 20, nous constexpr des algorithmes standard constexpr et, éventuellement, même certains conteneurs, donc les lambdas constexpr seront très utiles dans ce contexte. Votre code sera le même pour la version d'exécution ainsi que pour la version constexpr (version au moment de la compilation)!

En bref:

constexpr lambda vous permet d'être cohérent avec la programmation passe-partout et éventuellement d'avoir un code plus court.

Passons maintenant à la deuxième fonctionnalité importante disponible en C ++ 17:

Capture de * this
Capturez * ceci

Vous souvenez-vous de notre problème lorsque nous voulions capturer un membre de la classe? Par défaut, nous capturons cela (comme un pointeur!), Et donc nous pouvons avoir des problèmes lorsque des objets temporaires sortent de la portée ... Cela peut être corrigé en utilisant la méthode de capture avec un initialiseur (voir la première partie de la série). Mais maintenant, en C ++ 17, nous avons une manière différente. Nous pouvons envelopper une copie de * this:

 #include <iostream> struct Baz { auto foo() { return [*this] { 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

La capture de la variable membre souhaitée à l'aide de la capture avec l'initialiseur vous protège des erreurs possibles avec des valeurs temporaires, mais nous ne pouvons pas faire de même lorsque nous voulons appeler une méthode comme:

Par exemple:

 struct Baz { auto foo() { return [this] { print(); }; } void print() const { std::cout << s << '\n'; } std::string s; }; 

En C ++ 14, la seule façon de rendre le code plus sûr est de capturer this avec un initialiseur:

 auto foo() { return [self=*this] { self.print(); }; }   C ++ 17    : auto foo() { return [*this] { print(); }; } 

Une dernière chose:

Notez que si vous écrivez [=] dans une fonction membre, this implicitement capturé! Cela peut conduire à des erreurs à l'avenir ... et il deviendra obsolète en C ++ 20.

Nous arrivons donc à la section suivante: l'avenir.

L'avenir avec C ++ 20

En C ++ 20, nous obtenons les fonctions suivantes:

  • Autoriser [=, this] comme capture lambda - P0409R2 et annuler la capture implicite de ceci via [=] - P0806
  • Extension de package dans lambda init-capture: ... args = std::move (args)] () {} - P0780
  • thread_local statique, thread_local et lambda pour les liaisons structurées - P1091
  • modèle lambda (également avec concepts) - P0428R2
  • Simplification de la capture implicite Lambda - P0588R1
  • Lambda constructif et assignable sans enregistrer l'état par défaut - P0624R2
  • Lambdas dans un contexte non calculé - P0315R4

Dans la plupart des cas, les fonctions nouvellement introduites «effacent» l'utilisation de lambda et permettent des cas d'utilisation avancés.

Par exemple, avec P1091, vous pouvez capturer une liaison structurée.

Nous avons également des clarifications concernant la capture de ceci. En C ++ 20, vous obtiendrez un avertissement si vous capturez [=] dans une méthode:

 struct Baz { auto foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; GCC 9: warning: implicit capture of 'this' via '[=]' is deprecated in C++20 

Si vous avez vraiment besoin de capturer ceci, vous devez écrire [=, this] .

Il existe également des modifications liées aux cas d'utilisation avancés, tels que les contextes sans état et les lambdas sans état qui peuvent être construits par défaut.

Avec les deux changements, vous pouvez écrire:

 std::map<int, int, decltype([](int x, int y) { return x > y; })> map; 

Lisez les motifs de ces fonctionnalités dans la première version des phrases: P0315R0 et P0624R0 .

Mais regardons une fonctionnalité intéressante: les modèles lambda.

Motif lambd

En C ++ 14, nous avons obtenu des lambdas généralisés, ce qui signifie que les paramètres déclarés comme auto sont des paramètres de modèle.

Pour lambda:

 [](auto x) { x; } 

Le compilateur génère une instruction d'appel qui correspond à la méthode passe-partout suivante:

 template<typename T> void operator(T x) { x; } 

Mais il n'y avait aucun moyen de modifier ce paramètre de modèle et d'utiliser les arguments de modèle réels. En C ++ 20, cela sera possible.

Par exemple, comment pouvons-nous limiter notre lambda à fonctionner uniquement avec des vecteurs d'un certain type?

Nous pouvons écrire un lambda général:

 auto foo = []<typename T>(const auto& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

Mais si vous l'appelez avec un paramètre int (par exemple, foo(10); ), vous pouvez obtenir une erreur difficile à lire:

 prog.cc: In instantiation of 'main()::<lambda(const auto:1&)> [with auto:1 = int]': prog.cc:16:11: required from here prog.cc:11:30: error: no matching function for call to 'size(const int&)' 11 | std::cout<< std::size(vec) << '\n'; 

En C ++ 20 on peut écrire:

 auto foo = []<typename T>(std::vector<T> const& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

Le lambda ci-dessus autorise l'instruction d'appel de modèle:

 <typename T> void operator(std::vector<T> const& s) { ... } 

Le paramètre de modèle suit la clause de capture [] .

Si vous l'appelez avec int (foo(10);) , vous obtiendrez un message plus agréable:

 note: mismatched types 'const std::vector<T>' and 'int' 


Vous pouvez jouer avec le code ici: @Wandbox

Dans l'exemple ci-dessus, le compilateur peut nous avertir des incohérences dans l'interface lambda que dans le code à l'intérieur du corps.

Un autre aspect important est que dans un lambda universel, vous n'avez qu'une variable, pas son type de modèle. Par conséquent, si vous souhaitez y accéder, vous devez utiliser decltype (x) (pour une expression lambda avec l'argument (auto x)). Cela rend certains codes plus verbeux et compliqués.

Par exemple (en utilisant le code de P0428):

 auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; T copy = x; T::static_function(); using Iterator = typename T::iterator; } 

Vous pouvez maintenant écrire comme:

 auto f = []<typename T>(T const& x) { T::static_function(); T copy = x; using Iterator = typename T::iterator; } 

Dans la section ci-dessus, nous avons eu un bref aperçu de C ++ 20, mais j'ai un autre cas d'utilisation supplémentaire pour vous. Cette technique est même possible en C ++ 14. Alors lisez la suite.

Bonus - LIFTing avec des lambdas

Nous avons actuellement un problème lorsque vous avez des surcharges de fonctions et que vous souhaitez les transmettre à des algorithmes standard (ou à tout ce qui nécessite un objet appelé):

 // two overloads: void foo(int) {} void foo(float) {} int main() { std::vector<int> vi; std::for_each(vi.begin(), vi.end(), foo); } 

Nous obtenons l'erreur suivante de GCC 9 (tronc):

 error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^ 

Cependant, il existe une astuce dans laquelle nous pouvons utiliser un lambda, puis appeler la fonction de surcharge souhaitée.

Sous forme de base, pour les types de valeurs simples, pour nos deux fonctions, nous pouvons écrire le code suivant:

 std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); }); 

Et dans la forme la plus générale, nous devons taper un peu plus:

 #define LIFT(foo) \ [](auto&&... x) \ noexcept(noexcept(foo(std::forward<decltype(x)>(x)...))) \ -> decltype(foo(std::forward<decltype(x)>(x)...)) \ { return foo(std::forward<decltype(x)>(x)...); } 

Code assez compliqué ... non? :)

Essayons de le décrypter:

Nous créons un lambda générique, puis passons tous les arguments que nous obtenons. Pour le déterminer correctement, nous devons spécifier noexcept et le type de la valeur de retour. C'est pourquoi nous devons dupliquer le code d'appel - afin d'obtenir les bons types.
Une telle macro LIFT fonctionne dans n'importe quel compilateur qui prend en charge C ++ 14.

Vous pouvez jouer avec le code ici: @Wandbox

Conclusion

Dans cet article, nous avons examiné les changements importants dans C ++ 17 et donné un aperçu des nouvelles fonctionnalités de C ++ 20.

Vous pouvez remarquer qu'à chaque itération du langage, les expressions lambda se mélangent avec d'autres éléments C ++. Par exemple, avant C ++ 17, nous ne pouvions pas les utiliser dans le contexte de constexpr, mais maintenant c'est possible. De même avec les lambdas génériques commençant par C ++ 14 et leur évolution en C ++ 20 sous forme de lambdas modèles. Suis-je en train de manquer quelque chose? Peut-être avez-vous un exemple passionnant? Merci de me le faire savoir dans les commentaires!

Les références

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
C ++ 17 - [expr.prim.lambda]
Expressions lambda en C ++ | Documents Microsoft
Simon Brand - Passer des ensembles de surcharge aux fonctions
Jason Turner - C ++ Weekly - Ep 128 - Syntaxe du modèle C ++ 20 pour Lambdas
Jason Turner - C ++ Weekly - Ep 41 - Support Lambda constexpr C ++ 17

Nous invitons tout le monde au traditionnel webinaire gratuit sur le cours, qui aura lieu demain 14 juin.

Source: https://habr.com/ru/post/fr455978/


All Articles