[C++] Une explication détaillée de l'utilisation et du développement des expressions lambda

1. Syntaxe des expressions lambda

L'expression Lambda est une fonctionnalité de base des langages de programmation modernes, tels que LISP, Python, C#, etc. Mais malheureusement, jusqu'à la norme C++11, C++ ne prenait pas en charge les expressions lambda au niveau des fonctionnalités du langage. Les programmeurs ont essayé d'utiliser des bibliothèques pour implémenter la fonction des expressions lambda, telles que Boost.Bind ou Boost.Lambda, mais elles présentent des inconvénients communs. Le code d'implémentation est très compliqué et vous devez être très prudent lorsque vous l'utilisez. Une erreur se produit, il peut y avoir un tas d'erreurs et de messages d'avertissement, et l'expérience de programmation n'est pas bonne en bref.

De plus, bien que C++ n'ait jamais pris en charge les expressions lambda, sa demande en expressions lambda est très élevée. Le plus évident est STL. Dans STL, il existe un grand nombre de fonctions algorithmiques qui doivent transmettre des prédicats, tels que std::find_if, std::replace_if, etc. Dans le passé, il existait deux manières d'implémenter des fonctions de prédicat : l'écriture de fonctions pures ou de foncteurs. Cependant, aucune de leurs définitions ne peut être directement appliquée aux paramètres réels des appels de fonction. Face à des codes d'ingénierie complexes, nous devrons peut-être changer de fichier source pour rechercher ces fonctions ou foncteurs.

Afin de résoudre les problèmes ci-dessus, la norme C++11 nous fournit la prise en charge des expressions lambda, et la syntaxe est très simple et claire. Cette simplicité peut nous donner l’impression qu’elle n’est pas à sa place avec la syntaxe C++ traditionnelle. Mais après vous être habitué à la nouvelle syntaxe, vous découvrirez la commodité des expressions lambda.

La syntaxe des expressions lambda est très simple et les définitions spécifiques sont les suivantes :

[ captures ] ( params ) specifiers exception -> ret { body }
  • [ captures ] —— une liste de capture, qui peut capturer zéro ou plusieurs variables dans la portée de la fonction actuelle, et les variables sont séparées par des virgules. Dans l'exemple correspondant, [x] est une liste de capture, mais elle ne capture qu'une variable x dans la portée de la fonction actuelle. Après avoir capturé la variable, nous pouvons utiliser cette variable dans le corps de la fonction d'expression lambda, comme return x * y. De plus, il existe deux manières de capturer une liste de capture : la capture par valeur et la capture par référence, qui seront décrites en détail ci-dessous.
  • ( params ) —— La liste de paramètres facultative, la syntaxe est la même que la liste de paramètres des fonctions ordinaires, et la liste de paramètres peut être ignorée lorsqu'aucun paramètre n'est nécessaire. Correspond à (int y) dans l'exemple.
  • spécificateurs - des qualificatifs facultatifs, mutables peuvent être utilisés en C++11, ce qui nous permet de modifier les variables capturées par valeur dans le corps de la fonction d'expression lambda, ou d'appeler des fonctions membres non const. Aucun spécificateur n’est utilisé dans l’exemple ci-dessus.
  • exception - Spécificateur d'exception facultatif, nous pouvons utiliser nosauf pour indiquer si le lambda lèvera une exception. Les exemples correspondants n'utilisent pas de spécificateurs d'exception.
  • ret —— Type de valeur de retour facultatif. Différentes des fonctions ordinaires, les expressions lambda utilisent la syntaxe derrière le type de retour pour indiquer le type de retour. S'il n'y a pas de valeur de retour (type vide), la partie entière, y compris ->, peut être ignorée. De plus, nous ne pouvons pas non plus spécifier le type de retour lorsqu’il y a une valeur de retour, et le compilateur en déduira un type de retour pour nous. Correspondant à l'exemple ci-dessus est ->int.
  • { body } —— le corps de fonction de l'expression lambda, cette partie est la même que le corps de fonction d'une fonction normale. Correspond à { return x * y; } dans l'exemple.

Étant donné que la liste des paramètres, les qualificatifs et la valeur de retour sont tous facultatifs, l'expression lambda la plus simple que nous puissions écrire est :

[]{}

Même si cela semble très étrange, il s’agit bien d’une expression lambda légale. Il convient de souligner que la définition de grammaire ci-dessus n'appartient qu'à la norme C++11, et que les normes C++14 et C++17 ont apporté des extensions utiles aux expressions lambda, qui seront introduites plus tard.

2. Liste de capture

Dans la grammaire des expressions lambda, la partie la plus différente de la grammaire C++ traditionnelle doit être considérée comme la liste de capture. En fait, c'est aussi la partie la plus compliquée de l'expression lambda, en plus de la grande différence de syntaxe. Ensuite, nous décomposerons la liste de capture pour discuter de ses caractéristiques étape par étape.

2.1 Portée

Nous devons comprendre la portée de la liste de capture.Habituellement, nous disons qu'un objet est dans une certaine portée, mais cette déclaration a changé dans la liste de capture.

Les variables d'une liste de capture existent dans deux portées : la portée de la fonction définie par l'expression lambda et la portée du corps de la fonction de l'expression lambda. Le premier sert à capturer des variables, le second à utiliser des variables. De plus, la norme stipule également que la variable pouvant être capturée doit être de type stockage automatique. En termes simples, il s'agit d'une variable locale non statique.

Jetons un coup d'œil à l'exemple suivant :

int x = 0;
int main()
{
int y = 0;
static int z = 0;
auto foo = [x, y, z] {};
}

Le code ci-dessus peut ne pas pouvoir être compilé (pourquoi est-ce possible ? Parce que les compilateurs de différents fabricants peuvent gérer différemment, par exemple, GCC ne signalera pas d'erreur, mais donnera un avertissement). Il y a deux raisons : premièrement, les variables x et z ne sont pas des variables de type stockage automatique ; deuxièmement, x n'existe pas dans le périmètre défini par l'expression lambda. Et si vous souhaitez utiliser des variables globales ou des variables locales statiques dans les expressions lambda ? La solution qui vient tout de suite à l’esprit est d’utiliser la liste de paramètres pour passer des variables globales ou des variables locales statiques. En fait, cela ne doit pas être si compliqué, il suffit de l’utiliser directement. Jetons un coup d’œil au code suivant :

#include <iostream>
int x = 1;
int main()
{
int y = 2;
static int z = 3;
auto foo = [y] { return x + y + z; };
std::cout << foo() << std::endl;
}

Dans le code ci-dessus, même si nous n'avons pas capturé les variables x et z, nous pouvons toujours les utiliser. De plus, si nous définissons une expression lambda dans la portée globale, alors la liste de capture de l'expression lambda doit être vide. Car selon les règles mentionnées ci-dessus, la variable de la liste de capture doit être un type de stockage automatique, mais la portée globale n'a pas un tel type, tel que :

int x = 1;
auto foo = [] { return x; };
int main()
{
foo();
}

2.2 Capturer la valeur et capturer la référence

La méthode de capture de la liste de capture est divisée en valeur de capture et référence de capture. Nous avons vu la syntaxe de la valeur de capture dans l'exemple précédent. Écrivez le nom de la variable directement dans [], et s'il y a plusieurs variables, séparez-les par des virgules, par exemple :

int main()
{
int x = 5, y = 8;
auto foo = [x, y] { return x * y; };
}

La valeur de capture consiste à copier les valeurs de x et y dans la portée de la fonction à l'intérieur de l'objet d'expression lambda, tout comme les variables membres de l'expression lambda.

Il n'y a qu'une seule différence & entre la syntaxe de référence de capture et la valeur de capture. Pour exprimer une référence de capture, il suffit d'ajouter & avant la variable de capture, ce qui revient à prendre un pointeur de variable. C'est juste que ce qui est capturé ici est une référence au lieu d'un pointeur. Dans une expression lambda, vous pouvez utiliser directement le nom de la variable pour accéder à la variable sans déréférencer, par exemple :

int main()
{
int x = 5, y = 8;
auto foo = [&x, &y] { return x * y; };
}

Les deux exemples ci-dessus lisent simplement la valeur de la variable. D'après les résultats, il n'y a aucune différence entre les deux captures, mais si l'opération d'affectation de la variable est ajoutée, la situation est différente. Veuillez consulter l'exemple suivant :

void bar1()
{
int x = 5, y = 8;
auto foo = [x, y] {
x += 1; // 编译失败,无法改变捕获变量的值
y += 2; // 编译失败,无法改变捕获变量的值
return x * y;
};
std::cout << foo() << std::endl;
}
void bar2()
{
int x = 5, y = 8;
auto foo = [&x, &y] {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}

Dans le code ci-dessus, la fonction bar1 ne parvient pas à se compiler car nous ne pouvons pas modifier la valeur de la variable capturée. Cela conduit à une fonctionnalité des expressions lambda : la variable capturée est par défaut une constante , ou lambda est une fonction constante (similaire à une fonction membre constante).

L'expression lambda dans la fonction bar2 peut être compilée avec succès, bien que le corps de la fonction ait également pour comportement de modifier les variables x et y. En effet, la variable capturée est par défaut une constante et fait référence à la variable elle-même. Lorsque la variable est capturée par valeur, la variable elle-même est la valeur, donc modifier la valeur provoquera une erreur. Au contraire, dans le cas de la capture de références, la variable capturée est en fait une référence. Ce que nous modifions dans le corps de la fonction n'est pas la référence elle-même, mais la valeur de la référence, elle n'est donc pas rejetée par le compilateur.

Rappelez-vous également le spécificateur facultatif mutable mentionné ci-dessus ? L'utilisation du spécificateur mutable peut supprimer la constance de l'expression lambda, ce qui signifie que nous pouvons modifier la variable qui capture la valeur dans le corps de la fonction de l'expression lambda, par exemple :

void bar3()
{
int x = 5, y = 8;
auto foo = [x, y] () mutable {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}

Le code ci-dessus peut être compilé, ce qui signifie que l'expression lambda modifie avec succès les valeurs de x et y dans sa portée. Il convient de noter que, par rapport à la fonction bar1, la fonction bar3 a une paire supplémentaire de () en plus du spécificateur mutable. En effet, la grammaire stipule que s'il y a un spécificateur dans l'expression lambda, la liste de paramètres formels ne peut pas être omis.

Compiler et exécuter les deux fonctions bar2 et bar3 produira les mêmes résultats, mais cela ne signifie pas que les deux fonctions sont équivalentes, et il existe toujours une différence essentielle entre capturer des valeurs et capturer des références.

Lorsqu'une expression lambda capture une valeur, ce qui est réellement obtenu dans l'expression est une copie de la variable capturée. Nous pouvons modifier arbitrairement la variable interne capturée, mais cela n'affectera pas la variable externe. Cependant, la capture des références est différente. Modifier la variable capturée dans l'expression lambda modifiera également la variable externe correspondante :

#include <iostream>
int main()
{
int x = 5, y = 8;
auto foo = [x, &y]() mutable {
x += 1;
y += 2;
std::cout << "lambda x = " << x << ", y = " << y <<
std::endl;
return x * y;
};
foo();
std::cout << "call1 x = " << x << ", y = " << y << std::endl;
foo();
std::cout << "call2 x = " << x << ", y = " << y << std::endl;
}

Le résultat de l'opération est le suivant :

lambda x = 6, y = 10
call1 x = 5, y = 10
lambda x = 7, y = 12
call2 x = 5, y = 12

Il y a encore une chose à noter à propos de l'expression lambda qui capture la valeur. La variable qui capture la valeur a été corrigée lorsque l'expression lambda est définie. Quelle que soit la manière dont la fonction modifie la valeur de la variable externe une fois l'expression lambda définie , la valeur capturée par l'expression lambda Ni l'une ni l'autre ne changera.

#include <iostream>
int main()
{
    int  x = 5, y = 8;
    auto foo = [x, &y]() mutable
    {
        x += 1;
        y += 2;
        std::cout << "lambda x = " << x << ", y = " << y << std::endl;
        return x * y;
    };
    x = 9;
    y = 20;
    foo();
}

Le résultat de l'opération est le suivant :

lambda x = 6, y = 22

Dans le code ci-dessus, bien que les valeurs de x et y soient respectivement modifiées avant d'appeler foo, la variable x qui capture la valeur continue la valeur lorsque le lambda a été défini, et après la réaffectation de la variable y qui capture la référence, l'expression lambda La valeur de la variable capturée y change également en conséquence.

2.3 Méthodes de capture spéciales

En plus de spécifier les variables de capture, la liste de capture des expressions lambda dispose de trois méthodes de capture spéciales.

  1. [this] —— Capturez ce pointeur, la capture de ce pointeur nous permet d'utiliser des variables membres et des fonctions de ce type.
  2. [=] —— capture les valeurs de toutes les variables dans la portée définie par l'expression lambda, y compris celle-ci.
  3. [&] —— capture les références à toutes les variables dans la portée définie par l'expression lambda, y compris celle-ci.

Tout d'abord, examinons la situation de capture de ceci :

#include <iostream>
class A
{
public:
    void print()
    {
        std::cout << "class A" << std::endl;
    }
    void test()
    {
        auto foo = [this]
        {
            print();
            x = 5;
        };
        foo();
    }

private:
    int x;
};
int main()
{
    A a;
    a.test();
}

Dans le code ci-dessus, étant donné que l'expression lambda capture le pointeur this, la fonction membre print de ce type peut être appelée dans l'expression lambda ou sa variable membre x peut être utilisée.

Il est plus facile de comprendre en capturant la valeur ou la référence de toutes les variables :

#include <iostream>
int main()
{
int x = 5, y = 8;
auto foo = [=] { return x * y; };
std::cout << foo() << std::endl;
}

3. Principe de mise en œuvre de l'expression lambda

Si vous êtes un vétéran du C++, vous avez peut-être découvert que les expressions lambda sont très similaires aux objets fonction (foncteurs), commençons donc par les objets fonction et explorons en profondeur les principes d'implémentation des expressions lambda. Voir l'exemple ci-dessous :

#include <iostream>
class Bar
{
public:
    Bar(int x, int y) : x_(x), y_(y) {}
    int operator()()
    {
        return x_ * y_;
    }

private:
    int x_;
    int y_;
};
int main()
{
    int  x = 5, y = 8;
    auto foo = [x, y]
    {
        return x * y;
    };
    Bar bar(x, y);
    std::cout << "foo() = " << foo() << std::endl;
    std::cout << "bar() = " << bar() << std::endl;
}

Dans le code ci-dessus, foo est une expression lambda et bar est un objet fonction. Ils peuvent tous obtenir les valeurs des variables x et y dans la fonction principale au moment de l'initialisation, et renvoyer le même résultat après l'appel. Les différences les plus évidentes entre les deux sont les suivantes :

  1. L'utilisation d'expressions lambda ne nécessite pas de définir explicitement une classe, ce qui présente un grand avantage pour implémenter rapidement des fonctions.
  2. L'utilisation d'objets fonction peut avoir des opérations plus riches lors de l'initialisation, telles que Bar bar(x+y, x * y), et cette opération n'est pas autorisée dans l'expression lambda du standard C++11 (C++14 Elle a ensuite été étendue pour permettre cela). De plus, l'utilisation de variables locales globales ou statiques ne pose aucun problème lorsque Bar initialise des objets.

Il semble que dans le standard C++11, l'avantage des expressions lambda est qu'elles sont faciles à écrire et à maintenir, tandis que l'avantage des objets fonction est qu'ils sont plus flexibles et sans restriction, mais en général ils sont très similaires . En fait, c’est exactement ainsi que les expressions lambda sont implémentées.

L'expression lambda générera automatiquement une classe de fermeture par le compilateur au moment de la compilation, et un objet sera généré à partir de cette classe de fermeture au moment de l'exécution, nous appelons cela une fermeture. En C++, la soi-disant fermeture peut être simplement comprise comme un objet fonction anonyme pouvant contenir le contexte de portée au moment de la définition. Laissons maintenant ces concepts de côté et voyons à quoi ressemble réellement une expression lambda.

Tout d’abord, définissez une expression lambda simple :

#include <iostream>
int main()
{
int x = 5, y = 8;
auto foo = [=] { return x * y; };
int z = foo();
}

Ensuite, nous utilisons GCC pour afficher son code intermédiaire GIMPLE :

main()
{
    int D .39253;
    {
        int                      x;
        int                      y;
        struct __lambda0         foo;
        typedef struct __lambda0 __lambda0;
        int                      z;
        try
        {
            x       = 5;
            y       = 8;
            foo.__x = x;
            foo.__y = y;
            z       = main()::<lambda()>::operator()(&foo);
        }
        finally
        {
            foo = { CLOBBER };
        }
    }
    D .39253 = 0;
    return D .39253;
}
main()::<lambda()>::operator()(const struct __lambda0* const __closure)
{
    int       D .39255;
    const int x [value - expr:__closure->__x];
    const int y [value - expr:__closure->__y];
    _1       = __closure->__x;
    _2       = __closure->__y;
    D .39255 = _1 * _2;
    return D .39255;
}

À partir du code intermédiaire ci-dessus, nous pouvons voir que le type d'expression lambda est nommé __lambda0, l'objet foo est instancié via ce type, puis les membres __x et __y de l'objet foo sont attribués dans la fonction, et enfin via la coutume ( ) effectue un calcul sur l'expression et affecte le résultat à la variable z. Dans ce processus, __lambda0 est une structure avec l'opérateur personnalisé Operator(), qui est exactement la caractéristique du type d'objet fonction. Par conséquent, dans une certaine mesure, les expressions lambda ne sont qu'un morceau de sucre grammatical fourni par C++ 11. Les fonctions des expressions lambda peuvent être implémentées manuellement, et si l'implémentation est raisonnable, le code ne sera pas efficace dans son fonctionnement. L'écart est simplement que l'expression lambda pratique facilite l'écriture de code.

4. Expressions lambda apatrides

La norme C++ accorde une attention particulière aux expressions lambda sans état, c'est-à-dire qu'elles peuvent être implicitement converties en pointeur de fonction, par exemple :

void f(void (*)()) {}
void g()
{
    f([] {});
}   // 编译成功

Dans le code ci-dessus, l'expression lambda []{} est implicitement convertie en un pointeur de fonction de type void(*)(). De même, regardez le code suivant :

void f(void (&)()) {}
void g()
{
    f(*[] {});
}

Ce code peut également être compilé avec succès. On rencontre souvent cette application des expressions lambda dans le code STL.

5. Utilisation d'expressions lambda en STL

Pour discuter de l’utilisation courante des expressions lambda, il est nécessaire de discuter de la bibliothèque standard C++ STL. En STL, nous voyons souvent certaines fonctions d'algorithme dont les paramètres formels doivent transmettre un pointeur de fonction ou un objet de fonction pour compléter l'algorithme entier, comme std :: sort, std :: find_if, etc.

Avant la norme C++11, nous devions généralement définir une fonction d'assistance ou un type d'objet de fonction d'assistance en dehors de la fonction. Pour des besoins simples, nous pouvons également utiliser des fonctions auxiliaires fournies par STL, telles que std::less, std::plus, etc. De plus, des fonctions telles que std::bind1st et std::bind2nd peuvent également être utilisées pour des exigences légèrement plus complexes. Bref, quelle que soit la méthode utilisée ci-dessus, l’expression est assez obscure. La plupart du temps, nous devrons probablement écrire nous-mêmes des fonctions d’assistance ou des types d’objets de fonctions d’assistance.

Heureusement, avec les expressions lambda, ces problèmes sont facilement résolus. Nous pouvons implémenter des fonctions d'assistance directement dans la liste des paramètres de la fonction d'algorithme STL, par exemple :

#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> x = {1, 2, 3, 4, 5};
std::cout << *std::find_if(x.cbegin(),
x.cend(),
[](int i) { return (i % 3) == 0; }) <<
std::endl;
}

La fonction std::find_if a besoin d'une fonction auxiliaire pour aider à déterminer la valeur à trouver, et ici nous utilisons des expressions lambda pour définir la fonction auxiliaire directement lors du passage des paramètres. Qu'il s'agisse d'écrire ou de lire du code, définir directement des expressions lambda est plus concis et plus facile à comprendre que de définir des fonctions d'assistance.

6. Capture généralisée

La capture généralisée est définie dans la norme C++14. La capture dite généralisée est en fait composée de deux méthodes de capture. La première est appelée capture simple. Cette capture est la méthode de capture que nous avons mentionnée ci-dessus, à savoir [identifier], [ &identifier] et [ce] etc. La seconde est appelée capture d'initialisation. Cette méthode de capture a été introduite dans la norme C++ 14. Elle résout un problème important de capture simple, c'est-à-dire qu'elle ne peut capturer que les variables du contexte de définition de l'expression lambda, mais ne peut pas capturer l'expression. Résultats et noms de variables de capture personnalisées, tels que :

int main()
{
int x = 5;
auto foo = [x = x + 1]{ return x; };
}

Ce qui précède ne peut pas être compilé avant la norme C++14, car la norme C++11 ne prend en charge que la capture simple. La norme C++14 prend en charge de telles captures. Dans ce code, la liste de capture est une expression d'affectation, mais cette expression d'affectation est un peu spéciale car elle s'étend sur deux portées via le signe égal. La variable x à gauche du signe égal existe dans la portée de l'expression lambda, tandis que la variable x à droite du signe égal existe dans la portée de la fonction principale. Si les lecteurs trouvent que l’écriture des deux x est un peu alambiquée, on peut aussi utiliser une manière d’écrire plus claire :

int main()
{
int x = 5;
auto foo = [r = x + 1]{ return r; };
}

Évidemment, la variable r n'existe ici que dans l'expression lambda. Si la variable x est utilisée dans le corps de la fonction de l'expression lambda à ce moment, une erreur de compilation se produira. La capture d'initialisation est très pratique dans certains scénarios. Voici deux exemples. Le premier scénario consiste à utiliser des opérations de déplacement pour réduire la surcharge d'exécution du code, par exemple :

#include <string>
int main()
{
std::string x = "hello c++ ";
auto foo = [x = std::move(x)]{ return x + "world"; };
}

Le code ci-dessus utilise std::move pour initialiser la variable de liste de capture x, ce qui évite l'opération d'affectation de copie de l'objet, améliorant ainsi l'efficacité du code.

Le deuxième scénario consiste à copier l'objet this lors d'un appel asynchrone pour éviter un comportement indéfini dû à la destruction de l'objet this original lorsque l'expression lambda est appelée, tel que :

#include <iostream>
#include <future>
class Work
{
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn()
{
return std::async([=]() -> int { return value; });
}
};
std::future<int> foo()
{
Work tmp;
return tmp.spawn();
}
int main()
{
std::future<int> f = foo();
f.wait();
std::cout << "f.get() = " << f.get() << std::endl;
}

Le résultat est le suivant :

f.get() = 32766

Ici, nous nous attendons à ce que f.get() renvoie 42, mais il renvoie en réalité 32766. Il s'agit d'un comportement non défini, qui provoque une erreur de calcul du programme, et peut même provoquer un crash du programme. Pour résoudre ce problème, nous introduisons la fonctionnalité de capture d'initialisation pour copier l'objet dans l'expression lambda, modifions simplement la fonction spawn :

class Work
{
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn()
{
return std::async([=, tmp = *this]() -> int { return
tmp.value; });
}
};

Le code ci-dessus utilise la capture d'initialisation, copie *this dans l'objet tmp, puis renvoie la valeur de l'objet tmp dans le corps de la fonction. Étant donné que l'objet entier est transmis dans l'expression lambda par copie, même si l'objet pointé par celui-ci est détruit, cela n'affectera pas le calcul de l'expression lambda. Compilez et exécutez le code modifié, et le programme affiche correctement f.get() = 42.

7. Expressions lambda génériques

La norme C++14 permet aux expressions lambda d'avoir la capacité de modéliser des fonctions, que nous appelons expressions lambda génériques. Bien qu'il ait la capacité de fonctions de modèle, sa méthode de définition n'utilise pas le mot-clé template. En fait, la syntaxe générique de l'expression lambda est beaucoup plus simple, il suffit d'utiliser l'espace réservé auto, par exemple :

int main()
{
auto foo = [](auto a) { return a; };
int three = foo(3);
char const* hello = foo("hello");
}

8. Expressions lambda constantes et capture de *this

La norme C++17 apporte également deux améliorations aux expressions lambda, l'une est une expression lambda constante et l'autre est une amélioration pour capturer *this. Ici, nous expliquons principalement l'amélioration de la capture de cela. Vous vous souvenez du code qui a initialisé l'objet *this capturé plus tôt ? Nous copions l'objet pointé par ceci dans tmp dans la liste de capture, puis utilisons la valeur de tmp. Oui, cela résout le problème asynchrone, mais la solution n’est pas élégante. Imaginez, si un grand nombre d'objets pointés par ceci sont utilisés dans des expressions lambda, alors nous devons tous les modifier, et toute omission posera des problèmes. Afin de copier et d'utiliser l'objet *this plus facilement,C++17 ajoute la syntaxe de la liste de capture pour simplifier cette opération.Plus précisément, ajoutez [*this] directement à la liste de capture, puis utilisez-le directement dans le corps de la fonction d'expression lambda Cela pointe vers le membre de l'objet, ou prenez la classe Work précédente comme exemple :

class Work
{
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn()
{
return std::async([=, *this]() -> int { return value; });
}
};

Dans le code ci-dessus, au lieu d'utiliser tmp=*this pour initialiser la liste de capture, *this est utilisé directement. Dans l'expression lambda, tmp.value n'est plus utilisé mais la valeur est renvoyée directement. Compilez et exécutez ce code pour obtenir le résultat attendu 42. Il ressort des résultats que la syntaxe de [*this] permet au programme de générer une copie de l'objet *this et de la stocker dans l'expression lambda, et que les membres de l'objet copié sont directement accessibles dans l'expression lambda. , éliminant l'expression lambda précédente. La gêne de devoir accéder aux membres de l'objet via tmp.

9. Capturez [=, ceci]

Dans le standard C++20, les expressions lambda ont été légèrement modifiées. Cette modification ne renforce pas la capacité des expressions lambda, mais rend plus claire la sémantique associée à ce pointeur. Nous savons que [=] peut capturer ce pointeur, de même, [=,*this] capturera une copie de cet objet. Mais quand il y a beaucoup de [=] et [=,*this] dans le code, on peut facilement oublier la différence entre le premier et le second. Afin de résoudre ce problème, la syntaxe de [=, this] pour capturer ce pointeur a été introduite dans la norme C++ 20. Elle exprime en fait la même signification que [=], et le but est de permettre aux programmeurs de le distinguer de [=, * cette] différence :

[=, this]{}; // C++17 编译报错或者报警告, C++20成功编译

Bien que [=, this]{}; soit considéré comme ayant un problème de syntaxe dans la norme C++17, en pratique, GCC et CLang ne donnent que des avertissements sans signaler d'erreurs. De plus, dans le standard C++20, il est également souligné que [=, this] doit être utilisé à la place de [=]. Si vous compilez le code suivant avec GCC :

template <class T>
void g(T) {}
struct Foo {
int n = 0;
void f(int a) {
g([=](int k) { return n + a * k; });
}
};

Le compilateur affichera un message d'avertissement, indiquant que la norme ne prend plus en charge l'utilisation de [=] pour capturer implicitement le pointeur this, et invitera l'utilisateur à ajouter explicitement this ou *this. Enfin, il convient de noter qu'il n'est pas autorisé de capturer le pointeur this avec deux syntaxes en même temps, telles que :

[this, *this]{};

Cette façon d'écrire donnera certainement une erreur de compilation dans CLang, tandis que GCC donnera un avertissement légèrement doux. À mon avis, cette façon d'écrire n'a aucun sens et devrait être évitée.

10. Expressions lambda génériques pour la syntaxe du modèle

Nous avons expliqué comment les expressions lambda de la norme C++14 implémentent les génériques en prenant en charge auto. Dans la plupart des cas, c'est une fonctionnalité intéressante, mais malheureusement, cette syntaxe rend également difficile l'interaction avec le type, et l'opération sur le type devient extrêmement compliquée. En utilisant l'exemple du document de proposition :

template <typename T> struct is_std_vector : std::false_type { };
template <typename T> struct is_std_vector<std::vector<T>> :
std::true_type { };
auto f = [](auto vector) {
static_assert(is_std_vector<decltype(vector)>::value, "");
};

Les modèles de fonctions ordinaires peuvent facilement correspondre à un objet conteneur dont le paramètre réel est vector via le modèle de paramètre formel, mais pour les expressions lambda, auto n'a pas cette capacité d'expression, il doit donc implémenter is_std_vector et utiliser static_assert pour aider à juger le paramètre réel. Si le type réel est vectoriel. De l'avis des experts du comité C++, il est inapproprié de confier à static_assert une tâche qui aurait pu être accomplie grâce à la déduction de modèles. De plus, une telle syntaxe rend très compliquée l'obtention du type d'objet de stockage vectoriel, par exemple :

auto f = [](auto vector) {
using T = typename decltype(vector)::value_type;
// …
};

Bien sûr, c’est un hasard si nous pouvons y parvenir. Nous savons que le type de conteneur vectoriel utilisera le type intégré value_type pour représenter le type d'objet de stockage. Mais nous ne pouvons pas garantir que tous les conteneurs auxquels nous sommes confrontés implémenteront cette règle, donc s'appuyer sur des types intégrés n'est pas fiable. De plus, decltype(obj) ne peut parfois pas obtenir directement le type souhaité. Les lecteurs qui ne se souviennent pas des règles de dérivation decltype peuvent consulter les chapitres précédents, et voici directement l'exemple de code :

auto f = [](const auto& x) {
using T = decltype(x);
T copy = x; // 可以编译,但是语义错误
using Iterator = typename T::iterator; // 编译错误
};
std::vector<int> v;
f(v);

Veuillez noter que dans le code ci-dessus, le type déduit par decltype(x) n'est pas std::vector, mais const std::vector &, donc T copy = x; n'est pas une copie mais une référence. Pour un type référence, T::iterator est également agrammatical, donc une erreur de compilation se produit. Dans le document de proposition, l'auteur a gentiment donné une solution. Il a utilisé le déclin de STL, afin que les attributs cv et référence du type puissent être supprimés, donc le code suivant :

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

Bien que le problème soit résolu, nous devons toujours faire attention à auto, afin de ne pas entraîner de problèmes inattendus dans le code. De plus, cela ne peut continuer que lorsque le conteneur lui-même est bien conçu.

Compte tenu des problèmes ci-dessus, le comité C++ a décidé d'ajouter le support des modèles pour lambda en C++ 20. La syntaxe est très simple :

[]<typename T>(T t) {}

Ainsi, les exemples ci-dessus qui nous déroutent peuvent être réécrits comme suit :

auto f = []<typename T>(std::vector<T> vector) {
// …
};

ainsi que

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

Le code ci-dessus attire-t-il votre attention ? Ces codes sont non seulement beaucoup plus concis, mais aussi plus conformes aux habitudes de programmation générique C++.

Enfin, laissez-moi vous raconter une histoire intéressante. En fait, dès 2012, le document de proposition N3418 pour que lambda prenne en charge les modèles avait été soumis au comité C++, mais la proposition n'a pas été acceptée à ce moment-là. C++14, et la proposition de prise en charge lambda pour les modèles a été à nouveau proposée en 2017. Cette fois, on peut dire qu'il a rejoint avec succès la norme C++20 en marchant sur les épaules du N3559. En regardant l'ensemble du processus, même s'il n'est pas tortueux, il est aussi assez intrigant. En tant que langage développé depuis près de 30 ans, le C++ continue d'avancer grâce à l'exploration continue et à la correction d'erreurs.

11. Expressions lambda apatrides constructibles et assignables

Nous avons mentionné que les expressions lambda sans état peuvent être converties en pointeurs de fonction, mais malheureusement, avant la norme C++20, les types d'expressions lambda sans état ne pouvaient ni être construits ni attribués, ce qui entravait la mise en œuvre de nombreuses applications. Par exemple, nous avons appris que des fonctions comme std::sort et std::find_if ont besoin d'un objet fonction ou d'un pointeur de fonction pour faciliter le tri et la recherche. Dans ce cas, nous pouvons utiliser des expressions lambda pour terminer la tâche. Mais si vous rencontrez un type de conteneur tel que std::map, ce sera difficile à gérer, car l'objet de fonction de comparaison de std::map est déterminé par le paramètre template. À ce stade, nous avons besoin d'un type :

auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> mymap;

L'intention de ce code est évidente : il définit d'abord une expression lambda apatride greatate, puis utilise decltype(greater) pour obtenir son type et le transmettre au modèle en tant qu'argument de modèle. Cette idée est très bonne, mais elle n'est pas réalisable dans le standard C++17, car le type d'expression lambda ne peut pas être construit. Le compilateur informera clairement que le constructeur par défaut de l'expression lambda a été supprimé ("remarque : un type de fermeture lambda a un constructeur par défaut supprimé"). En plus d'être impossibles à construire, les expressions lambda sans état ne peuvent pas être attribuées, telles que :

auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> mymap1, mymap2;
mymap1 = mymap2;

Ici, mymap1 = mymap2; sera également signalé par le compilateur, car la fonction d'affectation de copie a également été supprimée (« remarque : un type de fermeture lambda a un opérateur d'affectation de copie supprimé »). Afin de résoudre les problèmes ci-dessus, la norme C++20 permet la construction et l'affectation de types d'expressions lambda sans état, il est donc possible d'utiliser l'environnement de compilation standard C++20 pour compiler le code ci-dessus.

Résumer

Ce qui précède présente la syntaxe, l'utilisation et le principe des expressions lambda. En général, les expressions lambda sont non seulement faciles à utiliser, mais également faciles à comprendre le principe. Cela résout l’embarras de ne pas pouvoir écrire directement des fonctions en ligne en C++ dans le passé. Bien qu'une extension du langage C appelée fonction nest soit fournie dans GCC, cette extension nous permet d'écrire des fonctions imbriquées à l'intérieur de fonctions, mais cette fonctionnalité n'a pas été incluse dans la norme. Bien sûr, nous n'avons pas besoin de le regretter, car l'expression lambda fournie maintenant est meilleure que la fonction nest en termes de simplicité grammaticale et de polyvalence. Une utilisation raisonnable des expressions lambda peut rendre le code à la fois plus court et plus lisible.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_43717839/article/details/132637639
conseillé
Classement