C++ - Un gout de programmation fonctionelle

Dans ce billet, nous allons aborder quelques unes des nouvelles fonctionnalités offerte par le C++11. Elles sont clairement inspiré de la vie dans le monde fonctionnel.

Alpha, Beta, 
 Kappa, Lambda!

Bien que portant le mĂȘme nom, les lambda(C++ 11 powered) sont trĂšs diffĂ©rentes de leur homologues fonctionnelles, les lambda fonctions. Une lambda en C++11, c’est plutĂŽt une intĂ©gration au langage des foncteurs.

class AFunctor
{
public:
  int operator (int a) { return a * b; }
  int b;
}

// ...
AFunctor f;
f.b = 42;
std::cout << f(2) << std::endl; //Display 84

RĂ©-Ă©crivons la mĂȘme chose avec la syntaxe d’une lambda, que l’on dĂ©taillera un peu plus loin (auto permet de laisser le compilateur infĂ©rer(deviner) le type).

int b = 42;
auto f = [&b](int a){return a * b};

Les lambda permette de faire la mĂȘme chose de façon plus lĂ©gĂšre, et ajoute la sĂ©mantique de fonction (i.e. on ne peux pas confondre une lambda et un objet en lisant du code, alors qu’on ‘'’pourrait’’’ avec un foncteur et un objet). Les lambda sont aussi plus proche de d’une fonction anonyme, puisque certaines fonctions (constructeur, opĂ©rateur =), implicitement dĂ©clarĂ© dans l’exemple ci dessus (on peux faire f = g avec f et g deux AFunctor) n’existent pas (sont explicitement supprimĂ©) pour les lambda.

Par exemple, le constructeur du type d’une lambda (on rappelle qu’en c++11, on peux obtenir le type de f avec decltype(f). Par exemple decltype(3.5f) ou std::vector v; decltype(v)) n’existe pas.

Si f est une lambda, le code decltype(f) g; ne compilera pas. Pourtant, si f est un AFunctor, le code decltype(f) g; compilera et correspond Ă  AFunctor g;.

Bon, qu’on se rassure, on peux quand mĂȘme faire une copie d’une lambda :

auto f = [](){return 42};

auto        g1 = f;
//Or
auto        g2(f);
//Or
decltype(f) g3 = f;
//Or
decltype(f) g4(f);

Comment fonctionne une lambda?

En fait, c’est trĂšs simple, est tout est dĂ©crit sur la page “Lambda” du site “CPPReference”.

[ capture ] ( params ) mutable exception attribute -> ret { body }

Dans capture on trouve la façon dont les variables extĂ©rieurs Ă  la lambda sont capturĂ©. Il y a deux mode de capture : par valeur, et par rĂ©fĂ©rence. Par dĂ©faut, [] signifie [=] qui veux dire “tout est rĂ©cupĂ©rer par valeur”, et le comportement est identique Ă  une copie des variable(pour les objets comme std::string, c’est plutĂŽt un const std::string& que vous recevez). On peux aussi spĂ©cifier [&] et toute les variables sont rĂ©cupĂ©rĂ©es par rĂ©fĂ©rence (et peuvent donc ĂȘtre modifiĂ©es depuis la lambda). Enfin, pour ceux qui apprĂ©cient la finesse, on peux expliciter le comportement pour chacune des variables, par exemple :

int a = 42;
std::vector<int> v;
std::string msg = "Hello";
//Take a by value
auto f1 = [](){return a;}
//Same, but tell explicitely the return type
auto f2 = []() -> int {return a;}
//Always the same
auto f3 = [a](){return a;}

//Take v by ref and a by value (return type is void)
auto f4 = [&b, a]() {v.push_back(a);}

//Take everybody by ref
auto f5 [&]() {v.push_back(a); a++; msg.push_back('!'); std::cout << msg << std::endl;}

La partie “exception” correspond aux spĂ©cifications du genre throw (std::bad_alloc, MyExceptionType) ou encore noexcept (no throw exception safety).

Si vous voulez modifier un objet obtenu par valeur, il vous faudra rajouter “mutable”. Cela peux ĂȘtre trĂšs utile, si vous voulez appeler des mĂ©thodes non const sur une copie d’un objet dans le scope.

std::vector<int> v;
auto f = [v]() mutable {v.push_back(42); std::cout << v[0] << std::endl;}

Petite astuce parfois utile : Si une lambda ne capture aucune variable, alors elle peux ĂȘtre convertie en pointeur de fonction.

std::function :

Les std::function reprĂ©sente des fonctions. Ils sont basĂ© sur les templates variadique (l’un des ajout les plus puissant au langage), que l’on peux espĂ©rer disponible sous VisualStudio d’ici 2039 (si l’équipe de microsoft ne prend pas de retard). Le constructeur des std::function autorise de les construire avec plus ou moins n’importe quoi (pointeur de fonction, pointeur de fonction membre, lambda, foncteur, 
).

En code :

//Lambda
std::function<int(int, int)> f = [](int a, int b) = {return a + b;}

//Function
void print(int a)
{
  std::cout << a << std::endl;
}
std::function<void(int)> f = print;

//Functor
struct Functor
{
    int b;
    int operator (int a) {return a + b;};
};

Functor func;
func.b = 3;    
std::function<int(int)> f = func;

//Member function
struct St
{
  int b;
  int sum(int a) { return a + b; };
}
std::function<void(const St&, int)> f = &St::sum;

Application partielle.

Tout ça, pour en venir à vous parler de std::bind. Quand on travail avec des langages fonctionnels, on peux appeler une fonction avec seulement une partie de ses arguments. On parle d’application partielle. std::bind permet de reproduire ce comportement. Prenons une innocente fonction :

void display(int a, int b, int c)
{
  std::cout << "a : " << a << " - b :" << b << " - c : " << c << std::endl;
}

On peux alors construire, grùce à std::bind, différentes spécialisation de cette fonction :

//On fixe les trois arguments
std::function<void()> f = std::bind(display, 5, 6, 7);
f(); // Affiche a : 5 - b : 6 - c : 7
//On fixe les trois, et on en rajoute un qui sera ignoré
std::function<void(int)> f = std::bind(display, 5, 6, 7);
f(42); // Affiche a : 5 - b : 6 - c : 7

//Placeholders::_i dĂ©signe le i-iĂšme argument lors de l’appelle de f
std::function<void(int)> f = std::bind(display, 5, 6, std::placeholders::_1);
f(42); //  Affiche a : 5 - b : 6 - c : 42

//Ne fixe que le premier argument
std::function<void(int, int)> f = std::bind(display, 5, std::placeholders::_1, std::placeholders::_2);
f(10, 20); //  Affiche a : 5 - b : 10 - c : 20

//On peux changer l'ordre :
std::function<void(int, int)> f = std::bind(display, 5, std::placeholders::_2, std::placeholders::_1);
f(10, 20); //  Affiche a : 5 - b : 20 - c : 10

Bien entendu, on peux aussi faire des choses plus complexe (passage des arguments par référence avec std::ref et std::cref dans les arguments de bind, pointeurs de fonction membre, pointeur vers membres, etc.).

Si vous vous demandez Ă  quoi ça peux bien servir, et bien dite vous que lĂ  oĂč on attend une callback avec une certaine signature (c’est le cas avec beaucoup d’outil de <algorithm>) vous avez maintenant la possibilitĂ© de spĂ©cialiser vos fonctions.

Pour ce qui est du coĂ»t, il est faible (celons les cas beaucoup de choses peuvent ĂȘtre optimisĂ© lors de la compilation), et n’est un argument recevable que dans certains cas particulier. Donc, Ă  moins de faire du temps rĂ©el et de faire ce genre de manipulation dans les parties critique, vous pouvez vous lĂącher.

VoilĂ , j’espĂšre vous avoir donnĂ© un petit aperçu de l’apport du c++11 en matiĂšre de manipulation des fonctions.

Références :