Introduction à std::function

Cet article débute par une explication des pointeurs de fonctions traditionnels de C, puis décrit brièvement std::function. Si vous n'êtes intéressé(e) que par cette dernière, vous pouvez y aller directement en passant par ici. Notez que nous utiliserons des éléments syntaxiques de C++ 11 à l'occasion, et que std::function fait d'ailleurs partie de ce standard.

Imaginons que vous souhaitiez manipuler une indirection vers une fonction de manière telle que celle-ci puisse correspondre tantôt à une fonction f(), tantôt vers une fonction g() dans la mesure où les deux soient de même signature.

Traditionnellement, en C comme en C++, la solution à ce problème est le recours à des pointeurs de fonctions. La syntaxe est rébarbative, mais le concept a fait ses preuves. Un exemple est donné à droite pour deux fonctions f() et g() de même signature (fonction nullaire – sans paramètres – qui retourne un int) et qui sont appelées successivement dans main() à travers un seul et même pointeur de fonction pf. Notez qu'utiliser le nom seul d'une fonction (à droite : utiliser f plutôt que f()) signifie prendre son adresse, alors que l'appeler donne accès au résultat de son exécution. À droite, toujours, f est de type int (*)() mais f() est de type int.

Notez la syntaxe de la déclaration de pf, qui est aussi le type des fonctions vers lesquelles pf pourra pointer, soit :

type (*nom)(params)

...où type est le type de ce que retourne la fonction pointée et params est la signature des paramètres. Notez les parenthèses autour du nom : c'est ce qui fait la différence par exemple entre

string (*p)(const string&);

...qui est un pointeur sur une fonction recevant une const string& et retournant une string, et

string *p(const string&);

...qui est une fonction recevant une const string& et retournant un string*. L'opérateur * associe normalement à gauche, ce qui explique que sans ces parenthèses, il s'applique au type de ce qui est retourné par la fonction.

int f() {
   return 3;
}
int g() {
   return 4;
}
#include <iostream>
int main() {
   using namespace std;
   int (*pf)() = f;
   cout << pf() << endl;
   pf = g;
   cout << pf() << endl;
}

Selon les compilateurs, il peut y avoir des subtilités dans cette signature. Si vous utilisez un compilateur de Microsoft, par exemple, et souhaitez placer un pointeur sur une fonction ne respectant pas la convention d'appel C, il vous faudra ajouter au type du pointeur de fonction les qualifications pour la convention d'appel souhaitée. Pour un savoir plus sur le sujet, voir cet article.

Pointeurs de fonctions et typedef

Vous aurez remarqué que la syntaxe des pointeurs de fonctions est quelque peu rébarbative. Ceci explique qu'on ait souvent recours à des alias, définis à travers typedef, lorsqu'on les utilise.

À droite, en caractères gras, se trouve une version légèrement modifiée (pour varier les signatures) de l'exemple précédent. Vous remarquerez que la déclaration de la variable pf dans main() est beaucoup moins lourde cette fois. Notez aussi la syntaxe particulière du typedef; elle diffère en effet de ce à quoi nous sommes habitués pour les alias sur des types de variables. Par exemple, définir quantite comme équivalent à unsigned int s'exprimerait ainsi :

typedef unsigned int quantite;

alors que définir ptr_fct comme équivalent à int (*)(int) s'exprimera comme suit :

typedef int (*ptr_fct)(int);

La position du nom du type dans les deux déclarations n'est pas la même.

int f(int n) {
   return n+1;
}
int g(int n) {
   return -n;
}
typedef int (*ptr_fct)(int);
#include <iostream>
int main() {
   using namespace std;
   ptr_fct pf = f;
   cout << pf() << endl;
   pf = g;
   cout << pf() << endl;
}

Pointeurs de fonctions et using

Depuis C++ 11, une nouvelle syntaxe est possible pour définir un type, en particulier dans le cas des pointeurs de fonctions. Cette syntaxe est plus puissante, surtout lorsque jointe à des templates, et est aussi – à mon humble avis – plus claire pour la majorité des programmeuses et des programmeurs. Ainsi, plutôt que d'écrire ceci :

typedef int (*ptr_fct)(int);

on préférera maintenant cela :

using ptr_fct = int(*)(int);

La position du nom du type dans la déclaration vous semblera probablement plus claire ici.

int f(int n) {
   return n+1;
}
int g(int n) {
   return -n;
}
using ptr_fct = int(*)(int);
#include <iostream>
int main() {
   using namespace std;
   ptr_fct pf = f;
   cout << pf() << endl;
   pf = g;
   cout << pf() << endl;
}

Pointeurs de fonctions en paramètres

Les pointeurs de fonctions font partie intégrante du langage C depuis toujours. Il y est possible par exemple de passer des pointeurs de fonctions en paramètre à une fonction, ou de retourner un pointeur de fonction. Certaines fonctions traditionnelles, par exemple qsort(), profitent de cette fonctionnalité et prennent un critère de tri (une fonction) en paramètre.

Un exemple d'utilisation de fonction prenant en paramètre une fonction serait appliquer(), à droite, qui prend en paramètre un vector<T> et une fonction T (*)(T), applique cette fonction à chaque élément du vecteur et retourne le vecteur résultant.

En pratique, n'écrivez pas appliquer(); il existe déjà std::for_each() et std::transform() dans <algorithm> qui sont bien meilleures et beaucoup plus flexibles. Ceci n'est qu'une illustration. La programmation générique permet d'élargir le champ des possibles pour des fonctions de haut niveau comme appliquer(), d'ailleurs; aujourd'hui, on escamoterait les pointeurs de fonctions et on écrirait plutôt ceci :

template <class T, class F>
   vector<T> appliquer(vector<T> v, F f)
   {
      for(auto &val : v)
         val = f(val);
      return v;
   }

...ce qui serait plus flexible et plus léger, le compilateur étant alors chargé de déterminer le type de f (ce qui permet entre autres d'utiliser à la fois des fonctions et des foncteurs). Le code client, quant à lui, demeurerait tel quel.

#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
// ... using ...
template <class T>
   vector<T> appliquer(vector<T> v, T(*f)(T)) {
      for(auto &val : v)
         val = f(val);
      return v;
   }
int negation(int n) {
   return -n;
}
int main() {
   vector<int> v;
   for(int i = 0; i < 10; ++i)
      v.push_back(i+1);
   auto u = appliquer(v, negation);
   cout << "Avant: ";
   copy(begin(v), end(v), ostream_iterator<int>{cout, " "});
   cout << endl;
   cout << "Apres: ";
   copy(begin(u), end(u), ostream_iterator<int>{cout, " "});
   cout << endl;
}

Le type std::function de C++ 11

Le système de types du langage C++ est beaucoup plus riche que celui du langage C. Entre autres choses, C++ supporte la programmation orientée objet, et permet de saisir l'adresse de méthodes d'instance. Nous n'entrerons pas dans la syntaxe ici, car elle est lourde (voir ceci pour un exemple d'utilisation) et utilise des opérateurs peu connus comme ::* et ->*, mais nous ferons remarquer que les méthodes d'instances utilisent un paramètre caché, this, et que pour cette raison, elles ne respectent pas la même signature que celle des fonctions globales ou des méthodes de classe (les méthodes qualifiées static dans une classe) qui, elles, n'ont pas ce paramètre silencieux.

Pour un exemple d'utilisation de délégués en C#, voir ceci. Pour un exemple d'implémentation simpliste en C++, voir ceci.

Certains langages, en particulier C#, VB.NET et Delphi supportent ce qu'on nomme des délégués, soit une entité ayant le comportement d'une fonction ou d'un foncteur mais qui est strictement polymorphique sur la base de la signature des appels. Les délégués sont très utilisés sur la plateforme .NET, étant à la base entre autres du système de gestion des événements et servant pour les threads.

Depuis C++ 11, une implémentation standard des délégués est livrée dans l'en-tête <functional>, soit le type std::function, lui-même fortement inspiré de Boost::Function. Comme c'est presque toujours le cas en pratique, il est hautement probable que l'implémentation standard soit meilleure que nos implémentations maison, les gens qui y ont oeuvré ayant sans doute faits des tests nettement plus exhaustifs que ce que nous aurions pu faire nous-mêmes.

L'exemple à droite montre la puissance et la flexibilité de std::function. En effet :

  • Nous définissons à la fois un foncteur (Plancher) et une fonction (plafond), tous deux de même signature à l'utilisation (opération prenant un double en paramètre et retournant un int)
  • Nous déclarons fct, un std::function sur une opération retournant un int et prenant en paramètre un double. Remarquez au passage la notation, permise en C++ 11, qui est légère mais aussi plus générale que celle des pointeurs de fonctions
  • fct mène initialement vers plafond(). Ainsi, lorsqu'elle sera appelée avec en paramètre 3.14159, elle retournera 4
  • Ensuite, fct se voit affecter une instance du foncteur Plancher. À partir de ce moment, lorsqu'on l'appellera avec 3.14159, elle retournera 3

La syntaxe atteinte est légère, allant à l'essentiel. Elle est flexible, supportant à la fois fonctions et foncteurs. Elle n'implique aucune gestion manuelle de ressources du côté du code client. Et elle fonctionne.

#include <functional>
#include <iostream>
struct Plancher {
   int operator()(double d) const {
      return static_cast<int>(d);
   }
};
int plafond(double d) {
   return Plancher{}(d) + 1;
}
int main() {
   using namespace std;
   function<int(double)> fct = plafond;
   cout << fct(3.14159) << endl;
   fct = Plancher{};
   cout << fct(3.14159) << endl;
}

C'est tout de même pas mal, n'est-ce pas?

Utiliser std::function pour une méthode d'instance

Il est possible de capturer un pointeur sur une méthode de classe (méthode static), mais ceci ne représente pas  véritablement un défi puisqu'une telle méthode n'est au fond qu'une fonction globale déguisée, avec contrôle d'accès en prime :

struct X {
   static int f(int n) {
      return n + 1;
   }
};
int f(int n) {
   return n * 2;
}
int main() {
   auto fct = f; // f est int(*)(int);
   fct(3); // retournera 6
   fct = &X::f; // &X::f est int(*)(int)
   fct(3); // retournera 4
}

Remarquez ici que fct peut pointer à la fois sur f est sur X::f puisque les deux fonctions sont de même signature. Toutefois, dans le cas d'une méthode d'instance, un this est requis. Normalement, ce this est passé de manière silencieuse à la fonction :

Ceci......est en fait cela
class X
{
   int val;
public:
   X(int val)
      : val{val}
   {
   }
   int f() const
      { return val; }
};
#include <iostream>
int main() {
   using namespace std;
   X x{3};
   cout << x.f() << endl;
}
class X {
   int val;
public:
   X(int val) : val{val} {
   }
   int f() const {
      return val;
   }
};
int appeler(const X &x, int (X::*fct)() const) {
   return (x.*fct)();
}
#include <iostream>
int main() {
   using namespace std;
   X x{3};
   cout << appeler(x, &X::f) << endl;
}

La syntaxe est plus complexe, mais met en relief qu'il faut une instance (un this) pour solliciter une méthode d'instance.

Il est possible d'encapsuler une méthode d'instance dans un std::function étant donné une signature adéquate. Dans le cas de cet exemple, l'écriture correcte serait la suivante (notez que Visual Studio 2013 contient un bogue non-résolu dans ce cas, mais le code fonctionne bien sur les compilateurs de la concurrence).

class X {
   int val;
public:
   X(int val) : val{val} {
   }
   int f() const {
      return val;
   }
};
#include <iostream>
#include <function>
int main() {
   using namespace std;
   X x{3};
   function<int(const X&)> fct = &X::f;
   cout << fct(x) << endl;
}

Voilà.

Lectures complémentaires

Quelques liens pour enrichir le propos.

Une alternative à std::function dans certains cas est std::bind(), bien que je ne le recommande pas car l'avènement des λ a nettement réduit l'utilité de ce mécanisme :


Valid XHTML 1.0 Transitional

CSS Valide !