Partager via


Expressions lambda en C++

En C++11 et versions ultérieures, une expression lambda( souvent appelée lambda) est un moyen pratique de définir un objet de fonction anonyme (fermeture) à l’emplacement où il est appelé ou passé en tant qu’argument à une fonction. En général, les lambdas sont utilisées pour encapsuler quelques lignes de code passées à des algorithmes ou à des fonctions asynchrones. Cet article définit les lambdas et les compare à d’autres techniques de programmation. Il décrit leurs avantages et fournit quelques exemples de base.

Parties d’une expression lambda

Voici un lambda simple passé en tant que troisième argument à la std::sort() fonction :

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
    std::sort(x, x + n,
        // Lambda expression begins
        [](float a, float b) {
            return (std::abs(a) < std::abs(b));
        } // end of lambda expression
    );
}

Cette illustration montre les parties de la syntaxe lambda :

Diagram that identifies the various parts of a lambda expression.

L’exemple d’expression lambda est [=]() mutable throw() -> int { return x+y ; } [=] est la clause de capture ; également appelé introduction lambda dans la spécification C++. Les parenthèses concernent la liste des paramètres. Le mot clé mutable est facultatif. throw() est la spécification d’exception facultative. -> int est le type de retour de fin facultatif. Le corps lambda se compose de l’instruction à l’intérieur des accolades, ou retourne x+y ; Celles-ci sont expliquées plus en détail en suivant l’image.

  1. clause capture (également connue sous le nom d’introductionlambda dans la spécification C++.)

  2. Liste de paramètres facultative. (Également appelé déclarateur lambda)

  3. spécification mutable Facultative.

  4. spécification d’exception facultative.

  5. trailing-return-type Optional.

  6. corps lambda.

Capture, clause

Une lambda peut introduire de nouvelles variables dans son corps (en C++14), et elle peut également accéder ou capturer des variables à partir de l’étendue environnante. Une expression lambda commence par la clause de capture. Elle spécifie les variables capturées et indique si la capture est par valeur ou par référence. Les variables qui ont le préfixe ampersand (&) sont accessibles par référence et les variables qui ne l’ont pas sont accessibles par valeur.

Une clause de capture vide, [ ], indique que le corps de l’expression lambda n’accède à aucune variable dans la portée englobante.

Vous pouvez utiliser un mode par défaut de capture pour indiquer comment capturer toutes les variables extérieures référencées dans le corps lambda : [&] signifie que toutes les variables auxquelles vous faites référence sont capturées par référence, et [=] les moyens qu’elles sont capturées par valeur. Vous pouvez utiliser un mode de capture par défaut, puis spécifier le mode opposé explicitement pour des variables spécifiques. Par exemple, si le corps d'une expression lambda accède à la variable externe total par référence et à la variable externe factor par valeur, les clauses de capture suivantes sont équivalentes :

[&total, factor]
[factor, &total]
[&, factor]
[=, &total]

Seules les variables mentionnées dans le corps lambda sont capturées lorsqu’une capture par défaut est utilisée.

Si une clause de capture inclut une capture par défaut &, aucun identificateur dans une capture de cette clause de capture ne peut avoir le formulaire &identifier. De même, si la clause de capture inclut une capture par défaut =, aucune capture de cette clause de capture ne peut avoir le formulaire =identifier. Un identificateur ou this ne peut pas apparaître plusieurs fois dans une clause de capture. L’extrait de code suivant illustre quelques exemples :

struct S { void f(int i); };

void S::f(int i) {
    [&, i]{};      // OK
    [&, &i]{};     // ERROR: i preceded by & when & is the default
    [=, this]{};   // ERROR: this when = is the default
    [=, *this]{ }; // OK: captures this by value. See below.
    [i, i]{};      // ERROR: i repeated
}

Une capture suivie d’un point de suspension est une extension de pack, comme illustré dans cet exemple de modèle variadicique :

template<class... Args>
void f(Args... args) {
    auto x = [args...] { return g(args...); };
    x();
}

Pour utiliser des expressions lambda dans le corps d’une fonction membre de classe, passez le this pointeur à la clause de capture pour fournir l’accès aux fonctions membres et aux membres de données de la classe englobante.

Visual Studio 2017 version 15.3 et ultérieure (disponible en /std:c++17 mode et versions ultérieures) : le this pointeur peut être capturé par valeur en spécifiant *this dans la clause de capture. Capture par valeur copie l’intégralité de la fermeture sur chaque site d’appel où l’expression lambda est appelée. (Une fermeture est l’objet de fonction anonyme qui encapsule l’expression lambda.) La capture par valeur est utile lorsque l’expression lambda s’exécute en parallèle ou en opérations asynchrones. Il est particulièrement utile sur certaines architectures matérielles, telles que NUMA.

Pour obtenir un exemple montrant comment utiliser des expressions lambda avec des fonctions membres de classe, consultez « Exemple : Utilisation d’une expression lambda dans une méthode » dans Exemples d’expressions lambda.

Lorsque vous utilisez la clause de capture, nous vous recommandons de garder ces points à l’esprit, en particulier lorsque vous utilisez des lambda avec multithreading :

  • Les captures de référence peuvent être utilisées pour modifier des variables en dehors, mais les captures de valeur ne peuvent pas. (mutable autorise la modification des copies, mais pas les originaux.)

  • Les captures de référence reflètent les mises à jour des variables en dehors, mais les captures de valeur ne le font pas.

  • les captures de référence introduisent une dépendance liée à la durée de vie, contrairement aux captures de valeur. Il est particulièrement important lorsque l’expression lambda s’exécute de manière asynchrone. Si vous capturez un local par référence dans une lambda asynchrone, ce local peut facilement être passé par le moment où l’exécution lambda s’exécute. Votre code peut entraîner une violation d’accès au moment de l’exécution.

Capture généralisée (C++14)

En C++14, vous pouvez introduire et initialiser de nouvelles variables dans la clause de capture, sans avoir besoin d’avoir ces variables dans l’étendue englobante de la fonction lambda. L'initialisation peut être exprimée comme n'importe quelle expression arbitraire ; le type de la nouvelle variable est déduit du type produit par l'expression. Cette fonctionnalité vous permet de capturer des variables de déplacement uniquement (par exemple std::unique_ptr) à partir de l’étendue environnante et de les utiliser dans une expression lambda.

pNums = make_unique<vector<int>>(nums);
//...
      auto a = [ptr = move(pNums)]()
        {
           // use ptr
        };

Liste de paramètres

Les lambda peuvent à la fois capturer des variables et accepter des paramètres d’entrée. Une liste de paramètres (déclarateur lambda dans la syntaxe Standard) est facultative et, dans la plupart des aspects, ressemble à la liste de paramètres d’une fonction.

auto y = [] (int first, int second)
{
    return first + second;
};

En C++14, si le type de paramètre est générique, vous pouvez utiliser le auto mot clé comme spécificateur de type. Cette mot clé indique au compilateur de créer l’opérateur d’appel de fonction en tant que modèle. Chaque instance d’une liste de auto paramètres équivaut à un paramètre de type distinct.

auto y = [] (auto first, auto second)
{
    return first + second;
};

Une expression lambda peut accepter une autre expression lambda comme argument. Pour plus d’informations, consultez « Expressions lambda d’ordre supérieur » dans l’article Exemples d’expressions lambda.

Étant donné qu’une liste de paramètres est facultative, vous pouvez omettre les parenthèses vides si vous ne transmettez pas d’arguments à l’expression lambda et que son déclarateur lambda ne contient pas de spécification d’exception, de type de fin de retour ou mutable.

Spécification mutable

En règle générale, l’opérateur d’appel de fonction lambda est const-by-value, mais l’utilisation de l’mot clé mutable annule cette opération. Il ne produit pas de membres de données mutables. La mutable spécification permet au corps d’une expression lambda de modifier les variables capturées par valeur. Certains exemples plus loin dans cet article montrent comment utiliser mutable.

Spécification d'exception

Vous pouvez utiliser la noexcept spécification d’exception pour indiquer que l’expression lambda ne lève aucune exception. Comme pour les fonctions ordinaires, le compilateur Microsoft C++ génère un avertissement C4297 si une expression lambda déclare la noexcept spécification d’exception et que le corps lambda lève une exception, comme illustré ici :

// throw_lambda_expression.cpp
// compile with: /W4 /EHsc
int main() // C4297 expected
{
   []() noexcept { throw 5; }();
}

Pour plus d’informations, consultez Spécifications d’exception (throw).

Type de retour

Le type de retour d’une expression lambda est déduit automatiquement. Vous n’avez pas besoin d’utiliser le auto mot clé, sauf si vous spécifiez un type de retour de fin. Le type de retour de fin ressemble à la partie de type retour d’une fonction ou d’une fonction membre ordinaire. Toutefois, le type de retour doit suivre la liste de paramètres. Vous devez donc ajouter le mot clé trailing-return-type -> avant le type de retour.

Vous pouvez omettre la partie de type retour d’une expression lambda si le corps lambda contient une seule instruction return. Sinon, si l’expression ne retourne pas de valeur. Si le corps d’une expression lambda contient une seule instruction return, le compilateur déduit le type de retour du type de l’expression de retour. Sinon, le compilateur déduit le type de retour en tant que void. Considérez les exemples d’extraits de code suivants qui illustrent ce principe :

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; };  // ERROR: return type is void, deducing
                                  // return type from braced-init-list isn't valid

Une expression lambda peut générer une autre expression lambda comme valeur de retour. Pour plus d’informations, consultez « Expressions lambda d’ordre supérieur » dans Exemples d’expressions lambda.

Corps lambda

Le corps lambda d’une expression lambda est une instruction composée. Elle peut contenir tout ce qui est autorisé dans le corps d’une fonction ou d’une fonction membre ordinaire. Le corps d’une fonction ordinaire et d’une expression lambda peut accéder aux types de variables suivants :

  • Variables capturées dans la portée englobante, comme décrit précédemment.

  • Paramètres.

  • Variables déclarées localement.

  • Les membres de données de classe, lorsqu’ils sont déclarés à l’intérieur d’une classe et this capturés.

  • Toute variable qui a une durée de stockage statique, par exemple des variables globales.

L’exemple suivant contient une expression lambda qui capture explicitement la variable n par valeur, et capture implicitement la variable m par référence :

// captures_lambda_expression.cpp
// compile with: /W4 /EHsc
#include <iostream>
using namespace std;

int main()
{
   int m = 0;
   int n = 0;
   [&, n] (int a) mutable { m = ++n + a; }(4);
   cout << m << endl << n << endl;
}
5
0

La variable n étant capturée par valeur, sa valeur reste 0 après l'appel à l'expression lambda. La mutable spécification permet n d’être modifiée dans l’expression lambda.

Une expression lambda ne peut capturer que des variables qui ont une durée de stockage automatique. Toutefois, vous pouvez utiliser des variables qui ont une durée de stockage statique dans le corps d’une expression lambda. L'exemple suivant utilise la fonction generate et une expression lambda pour assigner une valeur à chaque élément dans un objet vector. L'expression lambda modifie la variable statique pour générer la valeur de l'élément suivant.

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

Pour plus d’informations, consultez générer.

L’exemple de code suivant utilise la fonction de l’exemple précédent et ajoute un exemple d’expression lambda qui utilise l’algorithme generate_nde bibliothèque standard C++ . Cette expression lambda affecte un élément d’un objet vector à la somme des deux éléments précédents. La mutable mot clé est utilisée afin que le corps de l’expression lambda puisse modifier ses copies des variables x externes et y, que l’expression lambda capture par valeur. Étant donné que l'expression lambda capture les variables x et y d'origine par valeur, leurs valeurs restent égales à 1 après l'exécution de l'expression.

// compile with: /W4 /EHsc
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template <typename C> void print(const string& s, const C& c) {
    cout << s;

    for (const auto& e : c) {
        cout << e << " ";
    }

    cout << endl;
}

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

int main()
{
    // The number of elements in the vector.
    const int elementCount = 9;

    // Create a vector object with each element set to 1.
    vector<int> v(elementCount, 1);

    // These variables hold the previous two elements of the vector.
    int x = 1;
    int y = 1;

    // Sets each element in the vector to the sum of the
    // previous two elements.
    generate_n(v.begin() + 2,
        elementCount - 2,
        [=]() mutable throw() -> int { // lambda is the 3rd parameter
        // Generate current value.
        int n = x + y;
        // Update previous two values.
        x = y;
        y = n;
        return n;
    });
    print("vector v after call to generate_n() with lambda: ", v);

    // Print the local variables x and y.
    // The values of x and y hold their initial values because
    // they are captured by value.
    cout << "x: " << x << " y: " << y << endl;

    // Fill the vector with a sequence of numbers
    fillVector(v);
    print("vector v after 1st call to fillVector(): ", v);
    // Fill the vector with the next sequence of numbers
    fillVector(v);
    print("vector v after 2nd call to fillVector(): ", v);
}
vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34
x: 1 y: 1
vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9
vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18

Pour plus d’informations, consultez generate_n.

constexpr expressions lambda

Visual Studio 2017 version 15.3 et ultérieure (disponible en /std:c++17 mode et versions ultérieures) : vous pouvez déclarer une expression lambda comme constexpr (ou l’utiliser dans une expression constante) lorsque l’initialisation de chaque membre de données capturé ou introduit est autorisée dans une expression constante.

    int y = 32;
    auto answer = [y]() constexpr
    {
        int x = 10;
        return y + x;
    };

    constexpr int Increment(int n)
    {
        return [n] { return n + 1; }();
    }

Une lambda est implicitement constexpr si son résultat répond aux exigences d’une constexpr fonction :

    auto answer = [](int n)
    {
        return 32 + n;
    };

    constexpr int response = answer(10);

Si une expression lambda est implicitement ou explicitement constexpr, la conversion en pointeur de fonction produit une constexpr fonction :

    auto Increment = [](int n)
    {
        return n + 1;
    };

    constexpr int(*inc)(int) = Increment;

Spécifique à Microsoft

Les lambda ne sont pas pris en charge dans les entités managées CLR (Common Language Runtime) suivantes : ref class, , ref structou value classvalue struct.

Si vous utilisez un modificateur spécifique à Microsoft, par __declspecexemple, vous pouvez l’insérer dans une expression lambda immédiatement après le parameter-declaration-clause. Par exemple :

auto Sqr = [](int t) __declspec(code_seg("PagedMem")) -> int { return t*t; };

Pour déterminer si un modificateur particulier est pris en charge par les lambdas, consultez l’article sur le modificateur dans la section modificateurs spécifiques à Microsoft.

Visual Studio prend en charge les fonctionnalités lambda standard C++11 et les lambda sans état. Un lambda sans état est convertible en pointeur de fonction qui utilise une convention d’appel arbitraire.

Voir aussi

Informations de référence sur le langage C++
Objets de fonction dans la bibliothèque standard C++
Appel de fonction
for_each