les fonctions lambdas en c++11 et c++14

35
Les fonctions lambdas en C++11 et C++14 Montpellier C++, 21 oct. 2014 http://www.meetup.com/Montpellier-CPP/ Aurélien Regat-Barrel http://cyberkarma.net

Upload: aurelien-regat-barrel

Post on 04-Jul-2015

362 views

Category:

Software


5 download

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

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

Ne pas confondre...

Page 3: Les fonctions lambdas en C++11 et C++14

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

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

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

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

<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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Risques / abus d'utilisation ?

Prepare for unforeseen consequences...

Page 31: Les fonctions lambdas en C++11 et C++14

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

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

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

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

Conclusion

Au final, une lambda c'est quoi ?

Du sucre syntactique de foncteur sous stéroïde !