Maîtrise des Captures Lambda en C++

L'essence des expressions lambda en C++ réside dans leur capacité à "capturer" des variables de leur environnement externe. Contrairement à certains langages gérés où l'accès aux variables locales par une lambda est implicite, C++ exige une déclaration explicite de la manière dont ces variables doivent être intégrées à la "fermeture" (closure) – l'entité combinant le code de la fonction et son environnement de variables capturées.

Qu'est-ce qu'une Capture ?

Une capture est le mécanisme par lequel une expression lambda intègre des variables locales de sa portée englobante pour les utiliser en interne. Sans une liste de capture, une lambda est une fonction anonyme ordinaire, limitée à ses propres paramètres. Pour qu'elle accède à d'autres variables du contexte de définition, vous devez les spécifier entre crochets [].

Pourquoi utiliser la liste de capture [] ?

Ces crochets définissent la liste de capture, un élément crucial qui répond à deux impératifs fondamentaux en C++ :

A. Sécurité Mémoire (Prévention des Références Dangereuses)

C++ ne dispose pas de ramasse-miettes. Si une lambda référence une variable externe dont la durée de vie est plus courte que celle de la lambda (par exemple, dans un rappel asynchrone), cela peut entraîner des crashs dus à des références pendantes. La liste de capture permet de spécifier un mécanisme de copie :

  • En capturant par valeur (ex: [maVariable]), une copie est effectuée au moment de la création de la lambda. Cette copie est alors gérée par la lambda elle-même, garantissant la sécurité même si la variable originale est détruite.

B. Contrôle des Performances et de la Sémantique

La philosophie de C++ est de "ne pas payer pour ce que l'on n'utilise pas". La liste de capture offre un contrôle fin :

  • Si aucune variable externe n'est nécessaire, la liste est vide ([]), résultant en un code minimal.
  • Pour un accès en lecture seule à une petite variable, la capture par valeur ([facteur]) est appropriée.
  • Si une modification est requise, ou si la variable est volumineuse (ex: un grand vecteur), la capture par référence ([&facteur]) peut être préférée pour éviter une copie coûteuse, à condition de gérer la durée de vie.

Sémantique des Captures : Explicitation des Noms de Variables

Le C++ privilégie la déclaration explicite. Lorsqu'un nom de variable est placé dans les crochets, cela indique une intention claire de "copier" ou "référencer" cette variable spécifique. Considérez l'exemple suivant :

int valeurInitiale = 50;

// [valeurInitiale] signifie : "Lors de la création de 'calculateur',
// créez une copie de la valeur actuelle de 'valeurInitiale' (50)
// et stockez-la en interne dans la lambda."
auto calculateur = [valeurInitiale](int addition) {
    return valeurInitiale + addition; // 'valeurInitiale' est ici une copie privée
};

valeurInitiale = 100; // La variable externe est modifiée.

// L'appel à calculateur(10) retournera 60 (50 + 10),
// car la lambda utilise sa propre copie de 'valeurInitiale' qui était 50.

Cette approche contraste avec des langages comme C#, où une expression (x => x + maVariable) capturerait implicitement une référence à maVariable, reflétant toute modification ultérieure. En C++, vous avez le choix délibéré :

  1. [maVariable] (Capture par valeur) : Une copie est faite lors de la création de la lambda. La variable interne est immmuable par défaut (sauf si la lambda est mutable).
  2. [&maVariable] (Capture par référence) : La lambda détient une référence à la variable originale. Les modifications internes affectent l'externe, et vice-versa. La gestion de la durée de vie est sous votre responsabilité.
  3. [=] (Capture implicite par valeur) : Toutes les variables externes utilisées dans le corps de la lambda sont automatiquement capturées par valeur.
  4. [&] (Capture implicite par référence) : Toutes les variables externes utilisées sont automatiquement capturées par référence.

Anatomie d'une Lambda : Le "Foncteur" Caché

Pour comprendre en profondeur, il est utile de savoir que le compilateur C++ transforme une expression lambda en un objet de classe anonyme (un foncteur). Les variables capturées deviennent des membres de cet objet. Voici une représentation conceptuelle :

// Votre code :
auto calculateur = [valeurInitiale](int addition) { return valeurInitiale + addition; };

// L'équivalent conceptuel généré par le compilateur :
class TypeAnonymeLambda {
    int m_valeurInitiale; // Les variables capturées deviennent des membres de la classe.
public:
    // Le constructeur initialise les membres avec les valeurs capturées.
    TypeAnonymeLambda(int val) : m_valeurInitiale(val) {}

    // L'opérateur d'appel de fonction permet d'appeler l'objet comme une fonction.
    int operator()(int addition) const {
        return m_valeurInitiale + addition;
    }
};

// Utilisation équivalente :
TypeAnonymeLambda calculateur_instance(valeurInitiale); // Instanciation avec la capture.

En résumé, les noms dans la liste de capture [] sont intrinsèquement liés à la définition des membres de cette "classe anonyme" générée, façonnant ainsi le comportement et l'état interne de votre lambda.

Étiquettes: C++ Lambda Captures fermetures C++11

Publié le 19 juillet à 21h39