La fonction lambda est une fonction inline créée directement dans votre code, éventuellement passée directement dans les paramètres d’une de vos méthodes. Au lieu d’écrire une fonction que vous appelez dans les paramètres d’une méthode, vous placez ainsi directement le corps de cette fonction anonyme ("lambda") dans les paramètres d’entrées de votre fonction/méthode. Cela vous évite de créer une fonction à part et de l’appeler.
- La fonction est inline et donc pas d’appel en langage machine (pas de call) à une fonction avec les contraintes bien connues de travaux sur la pile à l’appel et au retour : plus de performance.
- La fonction est privée à votre code, elle ne sera pas publiée dans un fichier header.
- Pour des fonctions courtes, elle évite la création de fonctions, de publier une déclaration, code plus concis.
La syntaxe pour déclarer une lambda a des points communs avec celles des fonctions classiques, mais aussi quelques différences.
"[zone de capture](paramètres de la lambda) -> type de retour { instructions }"
La zone de capture : par défaut, une lambda est en totale isolation et ne peut manipuler aucune variable de l’extérieur. Grace à cette zone, la lambda va pouvoir modifier des variables extérieures.
Les paramètres de la lambda : exactement comme pour les fonctions, les paramètres de la lambda peuvent être présents ou non, avec utilisation de références et/ou const possible.
Le type de retour : encore un élément qui nous est familier. Il est écrit après la flèche ->. Il peut être omis dans quelques cas (notament quand la fonction renvoi void)
Les instructions : le code en lui même
l’exemple le plus simple et minimaliste (qui ne fait rien) :
int main()
{
// Ou version explicite :
[]() -> void {};
// sans le type de renvoi
[]{};
return 0;
}Si l'on décripte cette fonction :
- [] : je déclare une fonction lambda sans utiliser de paramètre de la fonction mère ...
- () : ... qui n'a aucun paramètre ...
- -> void : ... et qui ne renvoit rien ...
- {} : ... et qui ne fait rien.
En gros ça ne sert à rien...
Un exemple qui fait quelque chose : on veut parcourir un tableau de chaînes de caractères.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main()
{
std::vector<std::string> const chaines { "Un mot", "Autre chose", "Du blabla", "Du texe", "Des lettres" };
std::for_each(std::begin(chaines), std::end(chaines), [](std::string const & message) -> void { std::cout << "Message reçu : " << message << std::endl; });
return 0;
}La fonction lambda est ici : [](std::string const & message) -> void { std::cout << "Message reçu : " << message << std::endl; }
Si l'on décripte cette fonction :
- [] : je déclare une fonction lambda
- (std::string const & message) : le type de données pris en compte par la fonction
- -> void : la fonction ne renvoie rien
- { std::cout << "Message reçu : " << message << std::endl; } : le contenu de la fonction
Au passage, on voit que plusieurs fonction de la stl permettent l'utilisation de std::for_each().
Un autre exemple : on souhaite trier un vecteur par ordre décroissant. Par défaut la fonction std::sort tri dans le sens croissant. Mais là encore, cette fonction peut être paramétrée via une fonction lambda (qui est par défaut à tri croissant).
#include <algorithm>
#include <vector>
#include <iostream>
int main()
{
// déclaration du tableau
std::vector<double> tableau { 1, 3.1415, 2.1878, 1.5 };
// Tri dans l'ordre décroissant.
std::sort(std::begin(tableau), std::end(tableau), [](double a, double b) -> bool
{
return a > b;
});
// affichage des éléments triés
for (auto nombre : tableau)
{
std::cout << nombre << std::endl;
}
return 0;
}
On l’a juste sous les yeux, on voit directement ce que fait l’algorithme. Et en plus, on ne pollue pas l’espace global avec des fonctions à usage unique. Bref c'est élégant !
Il est possible de stocker une fonction. Cela permet par exemple d'appliquer plusieurs fois une fonction lambda sans copier coller.
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
auto lambda_avec_accolades { [](int nombre) -> void { std::cout << "Nombre reçu : " << nombre << std::endl; } };
auto lambda_avec_egal = [](int nombre) -> void { std::cout << "Nombre reçu : " << nombre << std::endl; };
// S'utilise comme une fonction classique, de façon totalement transparente.
lambda_avec_accolades(5);
std::vector<int> const nombres { 1, 2, 3, 8, 94, 548 };
// Ou bien dans un algorithme, aucun problème.
std::for_each(std::cbegin(nombres), std::cend(nombres), lambda_avec_egal);
return 0;
}On peut, comme le démontre le code, utiliser la syntaxe de C++ moderne, à base d’accolades {}, ou bien utiliser l’initialisation héritée du C à base de égal =. Tout est une question de goût.
Tout comme il est possible de laisser le compilateur deviner le type de retour, on peut lui laisser le soin de déduire les paramètres de la lambda, en utilisant auto à la place du type explicite des paramètres. Bien entendu, le fait de l’utiliser pour certains paramètres ne nous empêche absolument pas d’avoir des paramètres au type explicitement écrit.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main()
{
auto enumeration = [](auto const & parametre) -> void
{
std::cout << "Paramètre reçu : " << parametre << std::endl;
};
// On affiche une collection de std::string.
std::vector<std::string> const chaines { "Un mot", "Autre chose", "Du blabla", "Du texe", "Des lettres" };
std::for_each(std::begin(chaines), std::end(chaines), enumeration);
std::cout << std::endl;
// Pas de problème non plus avec des entiers.
std::vector<int> const nombres { 1, 5, -7, 67, -4454, 7888 };
std::for_each(std::begin(nombres), std::end(nombres), enumeration);
return 0;
}
Il est également possible de prendre en compte des éléments de la fonction mère dans la fonction.
Exemple :
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::vector<int> const nombres { 8, 7, 5, 4, 31, 98, 2, 77, 648 };
int const diviseur { 2 };
int somme { 0 };
// Il n'y a aucun problème à mélanger les captures par valeur et par référence.
// diviseur : capture par valeur
// somme : capture par référence
std::for_each(std::cbegin(nombres), std::cend(nombres), [diviseur, &somme](int element) -> void
{
if (element % diviseur == 0)
{
somme += element;
}
});
std::cout << "La somme de tous les éléments divisibles par " << diviseur << " vaut " << somme << "." << std::endl;
return 0;
}