Menu

Lambdas en C++ : les fonctions anonymes expliquées avec des exemples

Écrivez de petites fonctions en ligne à la volée avec les lambdas du C++ : la syntaxe, le fonctionnement des captures, quand utiliser mutable et le piège de la capture pendante qui piège tout le monde.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Des fonctions que vous écrivez là où vous les utilisez

À la page précédente, vous avez vu comment la surcharge permet à plusieurs fonctions de partager un même nom. Mais parfois, vous ne voulez pas du tout d'une fonction nommée - vous avez besoin d'un petit bout de logique une seule fois, juste là où vous l'utilisez, et lui donner un nom ne ferait qu'ajouter du désordre. C'est exactement ce qu'est une lambda : une fonction anonyme que vous pouvez définir en ligne.

Une lambda a une forme caractéristique en quatre parties :

[capture](parameters) -> return_type { body }

Les [] sont le signe révélateur que vous regardez une lambda. Le type de retour est facultatif - le compilateur le déduit généralement. Voici la plus simple possible :

greet n'est qu'une variable (son type est indicible, donc vous la stockez avec auto) que vous pouvez appeler avec (). Les lambdas avec paramètres fonctionnent exactement comme des fonctions ordinaires :

Les captures : accéder à la portée environnante

La partie qui fait des lambdas bien plus que de simples fonctions sans nom, c'est la liste de capture - les []. Elle permet à la lambda d'utiliser des variables de la portée où elle a été définie, et pas seulement ses propres paramètres.

Capturez par valeur avec [x] : la lambda obtient sa propre copie, figée au moment où la lambda est créée.

Remarquez que scale(5) a affiché 50, en utilisant la valeur de factor égale à 10 qui existait lorsque la lambda a été créée. La capture par valeur prend un instantané.

Capturez par référence avec [&x] : la lambda fait référence à la variable d'origine, voit les modifications ultérieures et peut la modifier.

Vous pouvez aussi capturer tout ce qu'utilise la lambda avec [=] (tout par valeur) ou [&] (tout par référence). Ils sont pratiques, mais être explicite - [total] ou [&total] - documente exactement ce que la lambda touche et est plus facile à raisonner.

Le piège de la référence pendante

La capture par référence est aussi puissante que dangereuse. La référence n'est valide que tant que la variable d'origine est en vie. Si la lambda survit à ce qu'elle a capturé, vous obtenez une référence pendante et un comportement indéfini - le programme peut planter, afficher n'importe quoi, ou sembler fonctionner par accident.

Voici l'erreur classique : renvoyer une lambda qui capture une variable locale par référence.

auto makeCounter() {
    int count = 0;
    return [&count]() { return ++count; };  // BOGUE : count meurt ici
}
// La lambda renvoyée référence désormais de la mémoire détruite.

Lorsque makeCounter renvoie, sa variable locale count est détruite, mais la lambda en conserve toujours une référence. Appeler la lambda renvoyée touche de la mémoire morte. La solution est de capturer par valeur pour que la lambda possède son propre état :

Règle empirique : ne capturez par référence que lorsque la lambda est utilisée immédiatement et localement (comme avec les algorithmes ci-dessous). Dès qu'une lambda est stockée, renvoyée ou exécutée plus tard, préférez la capture par valeur.

mutable et types de retour

Avez-vous repéré le mutable dans ce dernier exemple ? Par défaut, une capture par valeur est const à l'intérieur de la lambda - vous pouvez lire la copie mais pas la modifier. Ajouter mutable permet à la lambda de modifier ses propres copies entre les appels.

mutable n'affecte que la copie privée de la lambda - le seen extérieur reste intact, ce qui est justement tout l'intérêt de la capture par valeur.

La plupart du temps, le compilateur déduit très bien le type de retour. Vous n'avez besoin de l'expliciter avec -> qu'en cas d'ambiguïté, comme une lambda qui pourrait renvoyer des types différents selon les branches :

// Sans -> le compilateur ne peut pas choisir entre int et double
auto half = [](int n) -> double {
    if (n % 2 == 0) return n / 2;   // int
    return n / 2.0;                 // double
};

Lambdas et algorithmes : la vraie récompense

La raison pour laquelle les lambdas ont été ajoutées au C++ est de fournir de petits morceaux de logique aux algorithmes de la bibliothèque standard. Avant les lambdas, il fallait écrire une fonction nommée distincte ou un objet fonction encombrant loin de l'endroit où il était utilisé. Désormais, la logique se trouve directement sur le site d'appel.

L'exemple le plus courant est un ordre de tri personnalisé :

Les captures brillent ici parce que la lambda peut récupérer une valeur pour filtrer ou compter par rapport à elle. Ceci compte combien de nombres dépassent un seuil choisi par l'utilisateur :

Comme ces lambdas sont utilisées immédiatement et ne survivent pas à la fonction environnante, capturer par référence ([&passMark]) serait également sûr ici - mais par valeur est tout aussi clair et ne devient jamais pendant.

Suite : les pointeurs

Les lambdas ont discrètement soulevé une question plus profonde : lorsque vous capturez [&x], la lambda s'accroche à l'emplacement de x, et cet emplacement ne reste valide que tant que x est en vie. Cette idée - une valeur qui fait référence à l'endroit où quelque chose vit en mémoire, et ce qui se passe lorsque la chose pointée disparaît - est exactement le sujet de la page suivante. Nous affronterons les pointeurs de plein fouet : comment prendre une adresse, comment la suivre, et comment le même problème de référence pendante que vous venez de voir se manifeste à travers tout le C++.

Questions fréquentes

Qu'est-ce qu'une lambda en C++ ?

Une lambda est une fonction anonyme que vous pouvez écrire en ligne, juste là où vous l'utilisez. La syntaxe est [captures](paramètres){ corps }. Elle est parfaite pour de courtes opérations ponctuelles - comme le comparateur que vous passez à std::sort - sans avoir à déclarer une fonction nommée distincte ailleurs.

Quelle est la différence entre la capture par valeur et par référence dans une lambda C++ ?

[x] capture une copie de x figée au moment où la lambda est créée. [&x] capture une référence au x original, de sorte que la lambda voit les modifications ultérieures et peut le modifier. N'utilisez [&] que tant que les variables capturées sont garanties de survivre à la lambda, sinon vous obtenez une référence pendante.

Pourquoi ma lambda C++ dit-elle qu'elle ne peut pas modifier une variable capturée ?

Les captures par valeur sont const à l'intérieur de la lambda par défaut. Ajoutez le mot-clé mutable - [x]() mutable { x++; } - pour permettre à la lambda de modifier sa propre copie. Notez que cela ne change que la copie de la lambda, pas la variable d'origine à l'extérieur.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER