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.
3. Lλmbdλ ?
Les lambdas sont des fonctions anonymes...
Wikipedia : « Les fonctions anonymes sont des
fonctions 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 ?
4. 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 ?
5. 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
6. 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 ?
7. <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 ?
8. std::async
Les lambdas sont aussi très pratiques en programmation concurrente /
asynchrone :
#include <future>
// exécution asynchrone d'une tâche
std::future<int> f = std::async([] {
// calcul qui prend du temps…
return result;
});
// faire autre chose...
// résultat de l'opération asynchrone
int r = f.get();
9. 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.htmlrn"));
});
QObject::connect(socket, &QTcpSocket::readyRead, [socket]() {
qDebug() << "GOT DATA " << socket->readAll();
});
QObject::connect(socket, &QTcpSocket::disconnected, [socket]() {
qDebug() << "DISCONNECTED";
socket->deleteLater();
});
10. 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.
11. 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 ()
12. 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));
13. 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
}
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.
15. 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 traditionnelle
auto f(int x)->void; // déclaration équivalente
class A {
public:
bool f1() const;
auto f2() const -> bool;
};
16. 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 minVal
auto l = [minVal](int i) {
return i > minVal;
};
// affiche 20
std::cout << *std::find_if(
class Lambda {
public:
Lambda(int m) : minVal(m) {}
bool operator()(int i) const {
return i > minVal;
}
private:
int minVal;
};
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
17. 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;
};
18. 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;
};
19. 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.
20. 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.
21. 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;
};
22. 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.
23. 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;
};
24. 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 !
25. 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
};
struct Lambda1 {
Lambda1(int & N) : n(N) {}
void operator()() const {
n = 20; // OK (bien que fonction const!)
}
int & n;
};
Et oui : c'est l'objet référencé qui est modifié, pas la référence !
26. 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 »
};
struct Lambda1 {
Lambda1(int N) : n(N) {}
void operator()() const {
n = 20; // Erreur : modification depuis const !
}
int n;
};
Une lambda devrait en effet produire le même résultat si appelée deux
fois de suite avec les mêmes arguments (stateless).
27. 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
};
struct Lambda1 {
Lambda1(int N) : n(N) {}
void operator()() {
n = 20;
}
int n;
};
operator() n'est plus const.
28. 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).
29. Lambda générique
auto add = [](auto a, auto b) { return a + b; }
struct Lambda {
template<typename T1, typename T2>
auto operator()(T1 a, T2 b) const -> decltype(a + b) {
return a + b;
Lambda = foncteur sous stéroïde ?
}
};
30. Risques / abus d'utilisation ?
Prepare for unforeseen
consequences...
31. 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) ?
33. 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();
}
Le pointeur intelligent reçu a
été capturé sous forme de
référence… son compteur
d'utilisation n'est pas
incrémenté !
Ce pointeur intelligent est
un temporaire qui est
détruit une fois la fonction
computeAsync() appelée.
34. 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.
35. Conclusion
Au final, une lambda c'est quoi ?
Du sucre syntactique de foncteur sous stéroïde !