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 : .. code-block:: cpp 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é. .. code-block:: cpp #include #include #include using namespace std; template 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 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. .. panels:: :column: col-lg-10 p-2 **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 : .. code-block:: cpp 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é : .. code-block:: cpp #include #include #include using namespace std; template 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 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 : .. code-block:: cpp bool isOdd(int x) { return x % 2 != 0; } Il reste ensuite à utiliser l'algorithme *copy_if* : .. code-block:: cpp #include #include #include using namespace std; template 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 v = { 4, 5, 7, 8 ,2, 12, 7, 9, 17 }; vector 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 : .. code-block:: cpp 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* (technique hors programme) désignant tout ce qui peut jouer le rôle de fonction : * lambda * fonction * foncteur Ainsi, on peut écrire : .. code-block:: cpp #include #include 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 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 .. code-block:: cpp #include #include #include using namespace std; template void print(const Container& c,string t="") { for (const auto& x : c) cout << x << " "; cout << t << "\n"; } int main() { vector v = { 4, 5, 7, 5, 3, 8, 6, 4, 9, 5, 10}; vector 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. .. list-table:: :header-rows: 1 :widths: 25 75 :align: left :class: wrap * - 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()