les fonctions lambdas en c++11 et c++14
DESCRIPTION
C++11 introduit les fonctions lambda : qu'est-ce que c'est ? Comment (bien) les utiliser ? Voici le support d'une présentation donnée à l'occasion des rencontres C++ à Montpellier le 21 oct. 2014.TRANSCRIPT
![Page 1: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/1.jpg)
Les fonctions lambdas
en C++11 et C++14
Montpellier C++, 21 oct. 2014http://www.meetup.com/Montpellier-CPP/
Aurélien Regat-Barrelhttp://cyberkarma.net
![Page 2: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/2.jpg)
Ne pas confondre...
![Page 3: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/3.jpg)
Lλmbdλ ?
Les lambdas sont des fonctions anonymes...
Wikipedia : « Les fonctions anonymes sont desfonctions n'ayant pas de nom. »
En gros, c'est une fonction avec :
● un corps
● (éventuellement) des paramètres
● (éventuellement) un type de retour
mais pas de nom !
Mais alors, comment ça s'utilise ?
![Page 4: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/4.jpg)
Principe
Alors qu'une fonction nommée peut être référencée avant ou après sa définition, une expression lambda est référencée à l'endroit de sa création.
Il n'y a pas donc pas de déclaration de symbole, seulement une définition de bloc fonction.
● Généralement à usage unique, temporaire.● Typiquement destinée à être passée en argument à une autre
fonction…
Lambda = callback sous stéroïde ?
![Page 5: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/5.jpg)
Syntaxe générale
[] // lambda introducer : capture de variables() // paramètre[s] de la fonction (facultatif){ // corps de la fonction}(); // appel de la fonction (facultatif)
// Exemple :auto f = [](int i) { return i + 10; };f(1);
std::vector<int> v = { 1, 2, 3, 4 };std::transform(cbegin(v), cend(v), begin(v), f);
std::for_each(cbegin(v), cend(v), [](int n) { std::cout << n << ' ';});// affiche : 11 12 13 14
![Page 6: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/6.jpg)
Closure et foncteur
Un objet fonction créé via une lambda est une fermeture lexicale (closure) : il y a capture de paramètres.
std::vector<int> v = { 0, 5, 10, 15, 20, 25 };auto it = std::find_if(v.cbegin(), v.cend(), [](int i) { return i > 0 && i < 10; });
Le compilateur génère quelque chose qui ressemble à :
struct Lambda1 { bool operator()(int i) const { return i > 0 && i < 10; }};auto it = std::find_if(v.cbegin(), v.cend(), Lambda1());
Lambda = sucre syntactique de foncteur ?
![Page 7: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/7.jpg)
<algorithm>
Les lambdas se combinent parfaitement avec les algorithmes de la STL :
● all_of● any_of● count_if● equal● mismatch● none_of
● copy_if● generate● remove_if● sort● transform● ...
● binary_search● find_if● find_if_not● for_each● includes● minmax
Il est désormais plus facile d'utiliser ces algorithmes au lieu de les recoder / dissimuler via une boucle for.
● boucle for = goto moderne ?
![Page 8: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/8.jpg)
std::async
Les lambdas sont aussi très pratiques en programmation concurrente / asynchrone :
#include <future>
// exécution asynchrone d'une tâchestd::future<int> f = std::async([] {
// calcul qui prend du temps…
return result;});
// faire autre chose...
// résultat de l'opération asynchroneint r = f.get();
![Page 9: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/9.jpg)
Qt 5
Depuis Qt5, elles peuvent être utilisées comme slot :
QTcpSocket * socket = new QTcpSocket;socket->connectToHost("www.example.com", 80);
QObject::connect(socket, &QTcpSocket::connected, [socket]() { socket->write(QByteArray("GET index.html\r\n"));});
QObject::connect(socket, &QTcpSocket::readyRead, [socket]() { qDebug() << "GOT DATA " << socket->readAll();});
QObject::connect(socket, &QTcpSocket::disconnected, [socket]() { qDebug() << "DISCONNECTED"; socket->deleteLater();});
![Page 10: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/10.jpg)
Possibilités
A peu près tout ce qui est autorisé dans une fonction nommée l'est aussi dans une lambda :
● Expressions complexes● Multiples return● Lancer / attrapper des exceptions● Définir d'autres lambdas● …
Mais l'idée générale est d'avoir quelque chose de concis, en lien étroit avec le contexte de son utilisation.
Ce qui n'est pas possible : accéder au pointeur this du foncteur généré par le compilateur.
● Une lambda ne peut donc pas s'appeler elle-même de façon directe.
![Page 11: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/11.jpg)
Type d'une lambda
Une lambda qui ne capture aucune variable peut être convertie en pointeur de fonction. Exemple :
std::atexit([]{ LOG_INFO("Exiting...");});
Mais le type de la lambda elle-même est non spécifié. Chaque lambda introduit en effet un nouveau type qui lui est spécifique :
int main() { [] { std::cout << __FUNCTION__ << "\n"; }();}
main::<lambda_a8379e393dcd443e8683ae1a31573b62>::operator ()
![Page 12: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/12.jpg)
std::function
On ne peut donc pas spécifier de type « lambda » en paramètre / retour d'une fonction (puisqu'il n'y a pas de tel type global).
Pour ce faire, on utilisera std::function qui peut encapsuler une lambda, mais aussi d'autre objets appelables :
● Un foncteur● Un pointeur de fonction « à la C »● Un objet fonction créé avec std::bind
#include <functional>
void call(std::function<void(void)> f) { f();}call([] { std::exit(1); });call(std::bind(&std::exit, 1));
![Page 13: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/13.jpg)
Durée de vie
Les fermetures lexicales peuvent « survivre » aux fonctions qui les ont créées :
int a = 1;
std::function<int(int)> returnClosure() { return [](int x) { return (x + a); };}
int main() { auto f = returnClosure(); std::cout << f(1); // affiche 2 a += 1; std::cout << f(1); // affiche 3}
![Page 14: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/14.jpg)
Types de retour des lambdas
Préciser le type de retour est optionnel quand :● il s'agit de void● le corps de la fonction lambda consiste en un return expr;
Autrement le type de retour doit être spécifié via la syntaxe « à la traîne » (trailing return type) :
auto f = [](int i) -> int { g(); return i + h();};
C++14 assouplit les règles à ce niveau.
![Page 15: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/15.jpg)
Trailing return type notation
Cette syntaxe à la traîne :● Est la seule façon de préciser le type de retour des lambdas quand
cela est nécessaire● est permise pour n'importe quelle fonction (précédée de auto), y
compris main()● se combine souvent avec decltype
void f(int x); // syntaxe traditionnelleauto f(int x)->void; // déclaration équivalente
class A {public: bool f1() const; auto f2() const -> bool;};
![Page 16: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/16.jpg)
Capture de variables
Pour référencer des variables locales (non statiques), la lambda doit les capturer (principe de la closure) :
std::vector<int> v = { 5, 10, 20 };int minVal = 10;
// capture de minValauto l = [minVal](int i) { return i > minVal;};
// affiche 20std::cout << *std::find_if( v.cbegin(), v.cend(), l);
C++11 : le type capturé doit être copiable (donc pas de unique_ptr)C++14 introduit la généralisation de capture
class Lambda {public: Lambda(int m) : minVal(m) {} bool operator()(int i) const { return i > minVal; }private: int minVal;};
![Page 17: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/17.jpg)
Capture de variables
La capture peut aussi être effectuée par référence :
auto l = [&minVal](int i) { return i > minVal;};
class Lambda {public: Lambda(int m) : minVal(m) {} bool operator()(int i) const { return i > minVal; }private: int & minVal;};
![Page 18: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/18.jpg)
Généralités
On peut combiner les types de capture :
int minVal = 10;int maxVal = 20;auto l = [&minVal, maxVal](int i) { return i > minVal && i < maxVal;};
class Lambda {public: Lambda(int m1, int m2) : minVal(m1), maxVal(m2) {} bool operator()(int i) const { return i > minVal && i < maxVal; }private: int & minVal; int maxVal;};
![Page 19: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/19.jpg)
Généralités
Le mode de capture par défaut peut aussi être spécifié :
int minVal = 10;int maxVal = 20;
auto f1 = [=](int i) { // défaut : par valeur return i > minVal && i < maxVal;};
auto f2 = [&](int i) { // défaut : par référence return i > minVal && i < maxVal;};
Quand un mode de capture par défaut est spécifié, les variables capturées n'ont plus besoin d'être listées.
![Page 20: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/20.jpg)
Généralités
On peut bien sûr ajuster le mode de capture au besoin :
int minVal = 10;int maxVal = 20;
auto f = [=, &minVal](int i) { return i > minVal && i < maxVal;};
minVal est capturé par référence, maxVal par valeur.
![Page 21: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/21.jpg)
Capturer des membres de classe
On ne peut pas capturer directement les membres d'une classe :
class A {public: void f() { // erreur: this->minVal ne peut pas être capturé ! auto l = [minVal](int i) { return i > minVal; }; }private: std::vector<int> data; int minVal;};
![Page 22: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/22.jpg)
Généralités
Pour accéder aux membres d'une classe, il faut capturer this :
class A {public: void f() { /// OK: "minVal" => "this->minVal" auto l = [this](int i) { return i > minVal; }; }private: std::vector<int> data; int minVal;};
Tous les membres de la classe (même privés) sont accessibles car le type de la closure fait partie intégrante de la classe où il a été défini.
![Page 23: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/23.jpg)
Capture implicite de this
On peut aussi préciser un mode de capture par défaut afin de capturer implicitement this :
class A { void f() { auto it = std::find_if(data.cbegin(), data.cend(), // OK: copie this dans la closure [=](int i) { return i > minVal; } ); }
int minVal = 0; std::vector<int> data;};
![Page 24: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/24.jpg)
Capture de this par référence
Version avec capture implicite par référence :
void A::f() { auto it = std::find_if(data.cbegin(), data.cend(), // OK: maintient une référence vers this dans la closure [&](int i) { return i > minVal; } );}
A noter que :● la capture de this par référence est potentiellement plus lente à
cause de la double indirection (reference->this->minVal).● comme toute référence, l'objet référencé peut ne plus exister…● de même que la capture de this !
![Page 25: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/25.jpg)
Capture de this par référence
Si un objet est capturé par référence, celui-ci peut être modifié :
int n = 10;auto f = [&n] { n = 20; // OK};
Et oui : c'est l'objet référencé qui est modifié, pas la référence !
struct Lambda1 { Lambda1(int & N) : n(N) {} void operator()() const { n = 20; // OK (bien que fonction const!) } int & n;};
![Page 26: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/26.jpg)
Capture de this par référence
Par contre, cela ne fonctionne pas avec une capture par copie :
int n = 10;auto f = [n] { n = 20; // « impossible de modifier une capture par valeur
dans une expression lambda non mutable »};
Une lambda devrait en effet produire le même résultat si appelée deux fois de suite avec les mêmes arguments (stateless).
struct Lambda1 { Lambda1(int N) : n(N) {} void operator()() const { n = 20; // Erreur : modification depuis const ! } int n;};
![Page 27: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/27.jpg)
Lambda mutable
Pour pouvoir modifier une variable capturée par copie, il faut que la lambda soit mutable :
int n = 10;auto f = [n]() mutable { n = 20; // OK};
operator() n'est plus const.
struct Lambda1 { Lambda1(int N) : n(N) {} void operator()() { n = 20; } int n;};
![Page 28: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/28.jpg)
Lambdas en C++14
C++14 vient compléter C++11 à divers niveaux.
En ce qui concerne les lambdas, la modification majeure est la possibilité d'utiliser auto comme type des paramètres.
Les lambdas deviennent alors génériques (polymorphiques).
![Page 29: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/29.jpg)
Lambda générique
auto add = [](auto a, auto b) { return a + b; }
Lambda = foncteur sous stéroïde ?
struct Lambda { template<typename T1, typename T2> auto operator()(T1 a, T2 b) const -> decltype(a + b) { return a + b; }};
![Page 30: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/30.jpg)
Risques / abus d'utilisation ?
Prepare for unforeseen consequences...
![Page 31: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/31.jpg)
Lambda vs fonction nommée
auto isValidId = [](QString s) { return s.size() >= 4 && s.size() <= 8) && (s.toUpper() == s);};
for (auto & item : group1){ if (isValidId(item->id)) // ...}
for (auto & item : group2) { if (isValidId(item->id)) // ...}
static bool isValidId(QString s) { return s.size() >= 4 && s.size() <= 8) && (s.toUpper() == s);};
for (auto & item : group1){ if (isValidId(item->id)) // ...}
for (auto & item : group2) { if (isValidId(item->id)) // ...}
Si une lambda doit être utilisée plusieurs fois, faut-il lui préférer une fonction nommée (locale) ?
![Page 32: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/32.jpg)
Code déstabilisant à lire
class ScopeGuard {public: ScopeGuard(std::function<void(void)> F) : f(F) {} ~ScopeGuard() { f(); } std::function<void(void)> f;};
int main() { FILE * file = nullptr;
ScopeGuard guard([&file] { if (file != nullptr) { fclose(file); } }); // ... file = fopen("test.txt", "r");}
![Page 33: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/33.jpg)
Attention à la capture par référence
class A {public: int compute(); // résultat long à calculer};
future<int> computeAsync(shared_ptr<A> pA) { return async([&pA]() { return pA->compute(); });}
int main() { auto f = computeAsync(make_shared<A>()); // ... cout << f.get();}
Ce pointeur intelligent est un temporaire qui est détruit une fois la fonction computeAsync() appelée.
Le pointeur intelligent reçu a été capturé sous forme de référence… son compteur d'utilisation n'est pas incrémenté !
![Page 34: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/34.jpg)
Récapitulatif
Les expressions lambda génèrent des fermetures lexicales (closures).
Le contexte d'appel peut être capturé par valeur ou par référence.
Le type de retour - si spécifié - utilise la syntaxe dite « à la traîne ».
Les fermetures peuvent être conservées avec auto ou std::function.
● Attention à la durée de vie des variables capturées !
Les lambdas devraient rester concises et spécifiques à un contexte particulier (utilisées à un seul endroit).
C++14 ajoute le support de paramètres auto, de la capture généralisée, ainsi que plus de souplesse au niveau de la déduction du type de retour.
![Page 35: Les fonctions lambdas en C++11 et C++14](https://reader031.vdocuments.mx/reader031/viewer/2022020123/5597e4851a28abf30e8b47d7/html5/thumbnails/35.jpg)
Conclusion
Au final, une lambda c'est quoi ?
Du sucre syntactique de foncteur sous stéroïde !