Lambda ✱
Les algorithmes de la STL sont très puissants et robustes. Les utiliser, c’est ainsi éviter de reprogrammer la roue. Mais, nos besoins sont souvent un peu différents des algorithmes proposés ce qui nous empêche de les utiliser directement. Cependant, on peut ces algorithmes en leur fournissant des fonctions (lambdas ou foncteurs) pour ajuster leur comportement. Ce chapitre montre comment exploiter cette souplesse pour adapter les algorithmes de la STL à des besoins très variés.
Functor
Un functor (ou fonction objet, ou encore foncteur_FR) n’est rien d’autre qu’une classe/struct qui surcharge l’opérateur (). Autrement dit, il est alors possible d”’utiliser un objet comme s’il s’agissait d’une fonction. L’intérêt ? Une fonction classique ne conserve pas d’état interne, tandis qu’un foncteur peut stocker des informations supplémentaires dans ses champs, ce qui permet d’associer des données persistantes au comportement. Cette approche est donc très pratique dans certains cas, mais dans la pratique, les foncteurs deviennent plus rares, éclipsés par les expressions lambda que nous découvrirons plus loin.
Point sur la syntaxe
Lorsque vous redéfinissez l’operateur +, vous écrivez par exemple : T operator+(const T&a,const T& b). L’opérateur associé à l’appel de function s’intitule : operator(), ainsi, pour le redéfinir, il faudra écrire :
struct stepGenerator
{
int _val = 0;
int _step = 2;
stepGenerator(int start, int step) { _val = start; _step = step; }
// operator() with 1 argument
int operator()(int s) { _step = s; return (*this)(); }
// operator() with no argument - wierd syntax !
int operator()() { int v = _val; _val+=_step; return v; }
};
Exemple
Utilisons l’algorithme generate de la STL servant à remplir l’intervalle [first, last) avec les valeurs produites par l’appel répété à un générateur codé sous forme de functeur. Dans cet exemple, le foncteur est utile car il nous permet de conserver le dernier nombre généré.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename Container>
void print(const Container& c,string t="") { for (const auto& x : c) cout << x << " "; cout << t << "\n"; }
int main()
{
stepGenerator gen(0,2);
cout << gen(5) << endl; // 0
cout << gen() << endl; // 5
cout << gen(10) << endl; // 10
cout << gen() << endl; // 20
vector<int> v(10);
stepGenerator gen2(1,2); // odd number generator
generate(v.begin(),v.end(),gen2);
print(v); // 1 3 5 7 9 11 13 15 17 19
}
Le foncteur offre aussi l’avantage de la réutilisabilité : contrairement à une expression lambda, il peut être instancié et employé à différents endroits du programme.
Lambda
Syntaxe
Une lambda expression ou lamda est une syntaxe introduite à partir de C++11 qui permet de définir une fonction anonyme (une fonction sans nom) que l’on peut écrire directement dans le code, souvent pour la passer en argument.
Syntaxe d’une lambda : [captures] (paramètres) -> typeRetour { … }
Avec :
captures : indique comment la lambda accède aux variables environnantes.
paramètres : comme une fonction classique.
-> typeRetour : type de retour de la lambda expression - optionnel
corps : instructions à exécuter.
Voici un exemple simple :
auto square = [](int x) { return x * x; };
cout << square(5); // 25
Ici, la variable square correspond à une lambda qui reçoit une valeur et retourne son carré.
Les captures
Les paramètres de capture déterminent comment une expression lambda accède aux variables locales au moment :
[ ] : rien n’est capturé.
[x] : capture la variable x par valeur (copie).
[&x] : capture la variable x par référence.
[=] : capture toutes les variables visibles par valeur.
[&] : capture toutes les variables visibles par référence.
[=, &y] : capture par valeur par défaut, mais y par référence.
Exemples
Il est possible de reprendre l’exemple du stepGenerator, voici le code associé :
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename Container>
void print(const Container& c,string t="") { for (const auto& x : c) cout << x << " "; cout << t << "\n"; }
int main()
{
int val = 1;
int step = 2;
vector<int> v(10);
generate(v.begin(),v.end(),[&val,step] () { int v=val; val+=step; return v; } );
print(v); // 1 3 5 7 9 11 13 15 17 19
}
Les champs du functeur sont remplacés par deux variables locales val et step. Certe, cette approche permet d’écrire rapidement le code attendu, le problème est que la qualité du code chute.
Predicate
En C++, un predicate (prédicat_FR) est simplement une fonction (un functeur ou une lambda) qui retourne un booléen. L’injection d’un prédicat à l’intérieur des algorithmes de la STL permet de faire du code sur mesure. Prenons un exemple où l’on veut copier depuis un vector tous les nombres impairs.
Le prédicat écrit comme une fonction donne :
bool isOdd(int x) { return x % 2 != 0; }
Il reste ensuite à utiliser l’algorithme copy_if :
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename Container>
void print(const Container& c,string t="") { for (const auto& x : c) cout << x << " "; cout << t << "\n"; }
bool isOdd(int x) { return x % 2 != 0; }
int main()
{
vector<int> v = { 4, 5, 7, 8 ,2, 12, 7, 9, 17 };
vector<int> r;
copy_if(v.begin(),v.end(), back_inserter(r), isOdd);
print(r);
}
>> 5 7 7 9 17
Il est aussi possible d’utiliser la version avec une lambda :
copy_if(v.begin(), v.end(), back_inserter(r), [](int x) { return x % 2 != 0; });
function<>
Si nous avons besoin de stocker une lambda expression dynamiquement, il va falloir créer un vector de quelque chose. Or, le type lambda, ça n’existe pas en C++ ! Heureusement, il existe un type générique std::function<retour(params)> (technique hors programme) désignant tout ce qui peut jouer le rôle de fonction :
lambda
fonction
foncteur
Ainsi, on peut écrire :
#include <iostream>
#include <functional>
using namespace std;
int add(int a, int b) { return a + b; }
struct sub { int operator()(int a, int b) const { return a - b; } };
void Apply(function<int (int,int)> f,int a ,int b) { cout << f(a,b) << endl; }
int main()
{
Apply(add,2,3); // 5
Apply([](int a, int b) { return a * b; } ,2,3); // 6
Apply(sub(),2,3); // -1
}
Travail à rendre sur Github Classroom
Exercice 1
Créez un fichier nommé 7_STL_lambda.cpp
A partir de l’exemple ci-dessous, effectuez les actions demandées en utilisant la STL et les lambdas.
Uploadez le fichier sur votre github
#include <iostream> #include <vector> #include <algorithm> using namespace std; template <typename Container> void print(const Container& c,string t="") { for (const auto& x : c) cout << x << " "; cout << t << "\n"; } int main() { vector<int> v = { 4, 5, 7, 5, 3, 8, 6, 4, 9, 5, 10}; vector<int> w = {1, 2, 3, 4, 5, 6, 7, 8, 9}; }
En C++, les algorithmes de la STL comme remove_if ne suppriment pas réellement des éléments d’un conteneur. Ils déplacent simplement les éléments conservés vers le début et retournent un itérateur marquant la nouvelle fin. Il faut ensuite appeler erase pour raccourcir le vector et supprimer les éléments à effacer.
Opération |
Description |
---|---|
transform(first1, last1, out1, UnaryFnt) |
Créez un vector contenant les carrés de v |
replace_if(first1, last1, UnaryPredicate p, val) |
Dans le vector v remplacez tous les multiples de 5 par la valeur 0 |
iter remove_if(first1, last1, predicate) + erase(first1,last1) |
Retirez les multiples de 3 du vector v |
it partition(first1,last1, predicate) |
Partitionnez le vector w avec les nombres impairs à gauche et les pairs à droite |
count_if(first1,last1, predicate) |
Dans w, comptez la quantité de nombres pairs |
bool all_of(first1,last1,predicate) |
Vérifiez si tous les éléments du vector w sont inférieurs à 100 |
bool any_of(first1,last1,predicate) |
Détectez si au moins un élément de w est supérieur à 5 |
Exercice 2
Créez un fichier nommé 7_talkingClock.cpp
Effectuez les tâches demandées ci-dessous.
Uploadez le fichier sur votre github
A faire :
Créez une structure horloge parlante
Ajoutez une méthode permettant d’ajouter un foncteur ou une lambda à cette horloge
Créez une méthode Time(int hour) qui affiche tous les messages associés à l’heure donnée
Chaque message doit être géré par UN fonction/lambda :
Avant 17h : « Bonjour » sinon « Bonsoir »
S’il est 7h : « Levez-vous ! »
Entre 17h et 19h : « C’est l’heure des devoirs »
S’il est 12h ou 20h : « A table ! »
Testez votre horloge en utilisant dans le main()