Newer
Older
# [Fonctions lambdas](https://bitbucket.org/hyhg/revisions/wiki/Home)
Les fonctions lambda (ou anonymes), introduites en C++11, peuvent être définies au vol
n'importe où dans le code, là ou l'on en a besoin. Cela permet de se passer d'une
déclaration préalable de la fonction, relativement inutile quand la fonction ne doit servir
qu'une fois. Par ailleurs, la mécanique de capture des variables locales permet d'éviter
l'écriture fastidieuse d'un foncteur (objet appelable à travers l'opérateur ()). Dans les cas simples,
le code produit est plus concis et plus clair. Si on pousse le mécanisme dans ses
retranchements avec des fonctions lambda très compliquées, c'est moins évident... à vous
de juger.
* prérequis : connaissance du C++ historique, et si possible des objets-fonctions.
* préinstallation : Docker.
* fichiers : https://gitlab.in2p3.fr/MaitresNageurs/PiscineJI/tree/master/CppLambda
---
## Motivation : la définition au vol de fonctions simples
Le C++ permet d'utiliser une fonction comme argument d'une autre fonction.
Parfois, cette fonction est triviale, ne va servir qu'une fois, et il est
fastidieux et "encombrant" de devoir la déclarer.
Ci-dessous, l'implémentation classique d'une fonction `applique` qui applique
une fonction `affiche*` à toutes les valeurs d'une collection.
#include <iostream>
#include <vector>
#include <list>
template< typename Collection >
void applique( Collection & col, void (*f)( typename Collection::value_type ) )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ f(*itr) ; ++itr ; }
}
void affiche( int i )
{
std::cout<<i<<std::endl ;
}
int main()
{
std::vector<int> v {1,2,3,4,5} ;
applique(v,affiche) ;
}
```
Essayez d'exécuter ce code au sein de la machine docker `gcc 6.1` :
```
> docker pull gcc 6.1
> docker run -it -v `pwd`:/home/<USER>/Piscine gcc:6.1 /bin/bash
> cd /home/<USER>/Piscine
> g++ -std=c++11 lbd_motivation_1.cpp -o lbd_motivation_1
> ./lbd_motivation_1
```
Avec C++11 apparait la possibilité d'écrire directement la fonction là ou elle est
appelée, en remplacant son nom par "[]", ce qui permet d'alléger le code.
Ces fonctions sans nom sont appelées **fonctions anonymes** ou **fonctions lambdas**.
Ci-dessous, nous réécrivons notre exemple en utilisant cette nouvelle syntaxe :
#include <iostream>
#include <vector>
template< typename Collection >
void applique( Collection & col, void (*f)( typename Collection::value_type ) )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ f(*itr) ; ++itr ; }
}
int main()
{
std::vector<int> v {1,2,3,4,5} ;
applique(v, [] (int i) { std::cout<<i<<std::endl ; } ) ;
Exercice 1 : modifiez le code de la fonction anonyme pour qu'elle
affiche le carré de chaque élement de la collection.
Si votre fonction lambda doit retourner un résultat, c'est à peu
prêt aussi facile. En C++11, si le corps de la fonction est fait d'une
seule instruction return qui renvoit une expression, le type de retour de
la fonction est déduit de celui de l'expression retournée.
Modifions l'exemple précédent. La fonction à appliquer est
maintenant une tranformation, et la fonction `applique()` se charge
d'afficher le résultat :
// Fichier lbd_resultat_1.cpp
#include <iostream>
#include <vector>
template< typename Collection >
void applique( Collection & col, typename Collection::value_type (*f)( typename Collection::value_type ) )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ std::cout<<f(*itr)<<std::endl ; ++itr ; }
}
int main()
{
std::vector<int> v {1,2,3,4,5} ;
applique(v, [] (int i) { return i*i ; } ) ;
}
Ci-dessus, le type de retour de la lambda est celui de l'expression
`i*i`, donc `int`. Si jamais le corps de la lambda est plus complexe
et que votre compilateur est pointilleux, il peut devenir néessaire de
préciser explicitement le type de retour, à l'aide d'une flèche (->) suivie du type,
placée entre la spécification des arguments et le corps de fonction. En reprenant
le même exemple :
// Fichier lbd_resultat_2.cpp
#include <iostream>
#include <vector>
template< typename Collection >
void applique( Collection & col, typename Collection::value_type (*f)( typename Collection::value_type ) )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ std::cout<<f(*itr)<<std::endl ; ++itr ; }
}
int main()
{
std::vector<int> v {1,2,3,4,5} ;
applique(v, [] (int i) -> int { return i*i ; } ) ;
}
En réalité, le compilateur se permet souvent de déuire le type au-delà de la spécification
stricte de C++11, et C++14 relâche certaines contraintes, en autorisant notamment les instructions
multiples, du moment que toutes les instructions `return` retournent des expressions du
même type. Il est donc peu probable que vous soyez obligés de préciser le type de retour,
si ce n'est pour rendre votre code un peu plus lisible quand la fonction commence à
se complexifier :
// Fichier lbd_resultat_3.cpp
#include <iostream>
#include <vector>
template< typename Collection >
void applique( Collection & col, typename Collection::value_type (*f)( typename Collection::value_type ) )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ std::cout<<f(*itr)<<std::endl ; ++itr ; }
}
int main()
{
std::vector<int> v {-2,-1,0,1,2} ;
applique(v,[] (int i) -> int
{
if (i>=0) return i*i ;
else return -i*i ;
}) ;
}
Exercice 2 : dans le code ci-dessus, transformez la collection en vecteur de valeurs réelles,
et la fonction anonyme pour qu'elle calcule la racine carrée (fonction `std::sqrt` définie
dans le fichier d'entête `cmath`). Pour les nombres négatifs, on prendra la racine carrée
de l'opposé.
Ces premiers exemples montrent déjà des utilisations pratiques des
lambdas. Mais là où elles deviennent réellement redoutables, c'est dans
leur capacité à capturer des variables locales.
---
## Capturer une variable par valeur
On veut parfois doter une fonction de certains paramètres qui sont
fixés lors de sa création, puis réutilisés, sans avoir à les redonner
en argument à chaque appel. La façon "orientée objet" de le faire consiste à
créer un objet-fonction, ou "foncteur", instance d'une classe dotée
d'un opérator (), telle que la classe Mult ci-dessous.
```c++
#include <iostream>
#include <vector>
#include <functional>
template< typename Collection >
void applique( Collection & col, std::function<typename Collection::value_type(typename Collection::value_type)> f )
{
auto itr = col.begin() ;
while (itr!=col.end())
MultFoncteur( int m ) : m_{m} {}
int operator() ( int i ) { return m_*i ; }
private :
int m_ ;
} ;
template< typename Collection >
std::cout<<std::endl ;
}
int main()
{
std::vector<int> v{1,2,3,4,5} ;
mult(v,1) ;
mult(v,2) ;
mult(v,3) ;
Au passage, on remarquera ci-dessus l'utilisation du type `std::function<...>` à la place du
pointeur ordinaire. Ceci permet à l'argument `f` de recevoir indifféremment un pointeur
de fonction ou un foncteur, du moment que ce dernier dispose d'un opérateur "()" doté des
types appropriés.
Revenons aux captures. Une fonction lambda, lorsqu'elle est introduite par des crochets vides "[]",
n'a accès qu'à ses propres arguments, où aux variables globales, comme n'importe quelle fonction.
Mais ces crochets permettent aussi de "capturer" la valeur d'une variable disponible là ou notre
lambda est créée. Dans notre exemple précédent, on peut capturer `m` et se passer facilement du
foncteur :
// Fichier lbd_capture_2.cpp
#include <iostream>
#include <vector>
#include <functional>
void applique( Collection & col, std::function<typename Collection::value_type(typename Collection::value_type)> f )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ std::cout<<" "<<f(*itr) ; ++itr ; }
}
template< typename Collection >
void mult( Collection & col, int m )
applique(col, [m] (int i) -> int { return m*i ; } ) ;
int main()
{
std::vector<int> v{1,2,3,4,5} ;
mult(v,1) ;
mult(v,2) ;
mult(v,3) ;
}
```
D'un point de vue interne, vous pouvez imaginez qu'une classe équivalente
à notre ancienne `MultFoncteur` a été créée au vol.
L'écriture des types bâtis avec `std::function` pouvant rapidement s'avérer fastidieux,
on l'évitera quand c'est possible. Par exemple, ici, à l'aide d'un deuxième paramètre
dans le patron :
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// Fichier lbd_capture_3.cpp
#include <iostream>
#include <vector>
template< typename Collection, typename Fonction >
void applique( Collection & col, Fonction f )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ std::cout<<" "<<f(*itr) ; ++itr ; }
}
template< typename Collection >
void mult( Collection & col, int m )
{
std::cout<<"*"<<m<<" :" ;
applique(col, [m] (int i) -> int { return m*i ; } ) ;
std::cout<<std::endl ;
}
int main()
{
std::vector<int> v{1,2,3,4,5} ;
mult(v,1) ;
mult(v,2) ;
mult(v,3) ;
}
```
Ci-dessous, pour démontrer que la variable capturée est *copiée*, on stocke la lambda
dans une variable (de type `std::function<>`), afin de pouvoir la réappeler après avoir
modifier la variable originale, et on constatera que notre lambda renvoit toujours le
même résultat, indépendamment de la valeur courante de m.
```c++
// Fichier lbd_capture_4.cpp
#include <iostream>
#include <functional>
int main()
{
int i = 1 ;
std::function<void(void)> f = [i] () { std::cout<<i<<std::endl ; } ;
f() ;
i = 2 ;
f() ;
i = 3 ;
f() ;
}
```
Cette fois, pour faire l'économie de la déclaration explicite de `std::function<>`,
redondante, on peut s'appuyer sur le mécanisme de déduction de type disponible dans
C++11, via le mot-clef `auto` :
```c++
// Fichier lbd_capture_5.cpp
#include <iostream>
#include <functional>
int main()
{
int i = 1 ;
auto f = [i] () { std::cout<<i<<std::endl ; } ;
f() ;
i = 2 ;
f() ;
i = 3 ;
f() ;
}
```
---
## Capture de variable par référence
La capture par valeur est la plus sûre, parce que les variables
sont copiées, mais on peut également capturer par référence,
en ajoutant un "&". Dans ce cas, comme pour n'importe quelle
référence, le comportement est indéfini si jamais la variable originale
vient à disparaitre avant la fonction lambda. Dans notre exemple
précédent, une capture par référence permet d'obtenir une lambda
qui prend en compte la valeur courante de m :
```c++
int i = 1 ;
auto f = [&i] () { std::cout<<i<<std::endl ; } ;
f() ;
i = 2 ;
f() ;
i = 3 ;
f() ;
}
```
Exercice 3 : au lieu de la fonction lambda ci-dessus, écrivez le foncteur équivalent.
---
## Captures multiples
Vous n'êtes pas limité à capturer une seule variable. Les crochets
peuvent inclure autant de variables que vous le désirez, séparées par
des virgules, en choississant individuellement pour chaque variable
si elle doit être capturée par valeur ou par référence.
```c++
#include <iostream>
int main()
{
int i{100}, j{10}, k{1} ;
auto f = [i,&j,k] () { std::cout<<(i+j+k)<<std::endl ; } ;
f() ;
i = 200, j = 20, k = 2 ;
f() ;
i = 300, j = 30, k = 2 ;
f() ;
}
```
Si on raisonne en terme d'objets-fonctions, on peut imaginer une classe qui
capture i, j et k, ce qu'en termes plus savants on appelera une fermeture
ou une clôture (closure) :
#include <iostream>
class Cloture
{
public :
Cloture( int i, int & j, int k ) : i_{i}, j_{j}, k_{k} {}
void operator() () { std::cout<<(i_+j_+k_)<<std::endl ; }
private :
int i_, & j_, k_ ;
} ;
int main()
{
int i{100}, j{10}, k{1} ;
f() ;
i = 200, j = 20, k = 2 ;
f() ;
i = 300, j = 30, k = 2 ;
f() ;
}
```
Retournons aux lamndas : vous pouvez également décider de capturer tout ce
qui peut l'être sans discernement, à l'aide de la notation "[=]" pour le faire par
valeur, ou "[&]" pour le faire par référence.
Enfin, vous pouvez utilisez cette dernière notation comme capture
par défaut, et spécifier explicitement le mode de capture de certaines
variables échappant au mode par défaut.
```c++
// Fichier lbd_capture_9.cpp
#include <iostream>
auto f = [=,&j] () { std::cout<<(i+j+k)<<std::endl ; } ;
f() ;
i = 200, j = 20, k = 2 ;
f() ;
i = 300, j = 30, k = 2 ;
f() ;
}
```
Pour finir, une remise en garde : chaque capture par reference porte un danger,
celui que la variable originale cesse d'exister avant la fonction lambda, auquel
cas la capture devient invalide.
---
## Au sein d'une fonction membre
Au sein d'une fonction membre, pour accéder aux variables membres de l'objet courant,
il faut capturer this.
```c++
template< typename Collection, typename Fonction >
void applique( Collection & col, Fonction f )
{
auto itr = col.begin() ;
while (itr!=col.end())
}
class Mult
{
public :
Mult( int m ) : m_{m} {}
template< typename Collection >
void parcourt( Collection & col )
{
std::cout<<"*"<<m_<<" :" ;
applique(col, [this] (int i) { return m_*i ; } ) ;
std::cout<<std::endl ;
}
private :
int m_ ;
} ;
int main()
{
std::vector<int> v{1,2,3,4,5} ;
Pour en terminer avec la théorie, revenons à notre exemple initial, et imaginons
qu'on cherche à traiter des collections de nature différente :
template< typename Collection, typename Fonction >
void applique( Collection & col, Fonction f )
{
auto itr = col.begin() ;
while (itr!=col.end())
{ f(*itr) ; ++itr ; }
}
int main()
{
std::vector<int> vi{1,2,3,4,5} ;
applique(vi,[](int i){ std::cout<<i<<std::endl ; }) ;
std::vector<double> vd{1.1,2.2,3.3,4.4} ;
applique(vd,[](double i){ std::cout<<i<<std::endl ; }) ;
}
```
On aurait naturellement envie de créer un patron de lambda, ou quelque
chose qui y ressemble et qui apporte une forme de polymorphisme, plutot
que de redéclarer notre lambda à chaque fois.
C++14 répond à cette envie, en nous permettant de remplacer les
types de notre choix par `auto` :
```c++
{ std::cout<<i<<std::endl ; } ;
std::vector<int> vi{1,2,3,4,5} ;
Pour pouvoir compiler un tel code, passez au compilateur l'option
`-std=c++14`.
Si l'on recherche la clôture équivalente, cela revient
à transformer en patron l'opérateur (), et notre objet
class Cloture
{
public :
template<typename Value>
void operator() (Value v)
{ std::cout<<v<<std::endl ; }
} ;
Notez que c'est l'opérateur d'éxecution (`operator ()`) qui est
paramétré, et non la classe elle-même.
Pour notre exercice principal, nous allons utiliser un programme un peu plus riche,
qui génère aléatoirement 5 nombres complexes, les élève à la puissance 3, et affiche
ces complexes et leur produit :
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
```c++
// Fichier lbd_exercice_4.cpp
#include <complex>
#include <vector>
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <cmath>
// alias de types
typedef double Real ;
typedef std::complex<Real> Complex ;
typedef std::vector<Complex> Complexes ;
// affichage d'un complexe
std::ostream & operator<<( std::ostream & os, const Complex & c )
{
return os
<<std::scientific<<std::setprecision(4)
<<"( "<<std::setw(11)<<c.real()<<" , "<<std::setw(11)<<c.imag()<<" )" ;
}
// generation aleatoire d'un complexe sur le cercle unitaire
void random_complex( Complex & c )
{
int r = rand() ;
Real d = ((Real) r)/RAND_MAX ;
Real e = 2*M_PI*d ;
c = Complex(cos(e),sin(e)) ;
}
// affichage d'un tableau de complexe
void print_complexes( const Complexes & cs )
{
int i = 0 ;
Complexes::const_iterator itr ;
for ( itr = cs.begin() ; itr != cs.end() ; ++itr )
{ std::cout<<"x["<<i++<<"] = " <<(*itr)<<"\n" ; }
}
// calcul et affichage du produit des complexes d'un tableau
void print_product( const Complexes & cs )
{
Complex res(1.,0.) ;
Complexes::const_iterator itr ;
for ( itr = cs.begin() ; itr != cs.end() ; ++itr )
{ res *= (*itr) ; }
std::cout<<"*... = "<<res<<"\n" ;
}
// foncteur qui élève tout complexe à une puissance
// donnée à la construction
class ComplexPow
{
public :
ComplexPow( int p ) : p_(p) {}
Complex operator()( const Complex & c ) const
{
int ip ;
Complex res(1.,0.) ;
for ( ip=0 ; ip<p_ ; ++ip )
{ res *= c ; }
return res ;
}
private :
int p_ ;
} ;
// programme principal
int main ()
{
int d = 5 ;
int p = 3 ;
Complexes input(d) ;
Complexes output(d) ;
// generation aleatoire des complexes d'entree
srand(20151214) ;
std::for_each(input.begin(),input.end(),random_complex) ;
// elevation à la puissance p
std::transform(input.begin(),input.end(),output.begin(),ComplexPow(p)) ;
// affichage des résultats
print_complexes(output) ;
print_product(output) ;
return 0 ;
}
Dans le programme principal, on notera l'utilisation de la fonction `std::for_each()`,
qui ressemble à notre fonction `applique()`, à ceci près qu'elle prend en arguments
un itérateur de début et de fin, plutôt que la collection elle-même (ce qui permet de
traiter une sous-partie de la collection si on le souhaite).
Question 1 : remplacez la fonction `random_complex()` par une fonction anonyme donnée
directement dans le programme principal.
On notera également l'utilisation de la fonction `std::transform()`, qui effectue
un calcul sur chaque élément d'une collection, et stocke le résultat dans une autre
collection. Ici on utilise un foncteur, car on veut y stocker la puissance à laquelle
on veut élever les éléments d'entrée.
Question 2 : remplacez le foncteur `ComplexPow` par une fonction anonyme qui capture
la valeur de `p`.
La bonne façon de simplifier l'écriture des fonctions `print_complexes()` et
`print_product()`, en C++ moderne, serait sans doute d'utiliser un "for
généralisé". Mais pour finir en beauté ce plongeon, essayons d'y glisser des fonctions
lambdas.
Question 3 : réécrivez les boucles de `print_complexes()` et `print_product()`
avec un appel à `std::for_each()` combiné à une fonction lambda. Une capture
par référence s'impose.
---
© *CNRS 2016*
*Assemblé et rédigé par David Chamont, cette œuvre est mise à disposition selon les termes de la*
*[Licence Creative Commons - Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International](http://creativecommons.org/licenses/by-nc-sa/4.0/)*