Pointeurs de fonctions et signatures de fonctions

Si vous programmez en C++ depuis l'avènement de C++ 11, vous avez peut-être remarqué une nouvelle syntaxe pour exprimer une signature de fonction. Par exemple, dans <functional>, on trouve le type std::function qui peut s'utiliser ainsi :

#include <iostream>
#include <functional>
using namespace std;
int f(double x)
   { return static_cast<int>(x); }
int main()
{
   function<int(double)> fct = f;
   cout << fct(3.5) << endl; // affichera 3
   fct = [](double x) { return static_cast<int>(x * 2); };
   cout << fct(3.5) << endl; // affichera probablement 7 
}

La signature int(double), pour « quelque chose qui s'appelle en passant en paramètre un double et qui retourne un int », est différente de la syntaxe plus traditionnelle des pointeurs de fonctions, avec laquelle on aurait écrit int(*)(double) pour « pointeur sur une fonction acceptant un double en paramètre et retournant un int ».

Ces deux signatures sont-elles équivalentes? Pas exactement, en fait. Les deux ont des forces et des faiblesses distinctes. Un exemple de programme aidera à comprendre les nuances de ces deux écritures.

Notre programme de test définira deux types correspondant à ces deux façons d'exprimer une signature, soit le type fptr, pour « pointeur de fonction », et le type fsig, pour « signature de fonction ». Dans les deux cas, nous utiliserons des fonctions acceptant un int en paramètre et ne retournant rien, ceci dans le but de faire des tests simples.

Nous pouvons être très génériques ici et utiliser des templates, même variadiques. Par exemple, ftempl<R,A> décrira quelque chose qui s'appelle en passant un A en paramètre et qui retournera un R, et fvaria<R,A...> fera de même mais pour autant de paramètres que souhaité à l'appel.

Clairement, les types ftempl<void,int> et fvaria<void,int> équivalent tous deux au type fsig.

#include <iostream>
using namespace std;
using fptr = void(*)(int);
using fsig = void(int);
// nous pouvons être très génériques
template <class R, class A>
   using ftempl = R(A);
// ... même très, très génériques
template <class R, class ... A>
   using fvaria = R(A...);

Pour les besoins du test, nous utiliserons des fonctions appliquant une opération sur des paramètres à partir des types fptr, fsig, ftempl et fvaria définis plus haut.

 Notez que pour ftempl et (surtout) pour fvaria, nous aurions pu utiliser des exemples d'appels très variés, mais notre intention dans ce qui suit est surtout d'explorer les nuances entre fptr et fsig; si vous êtes curieuses ou curieux, je vous invite à poursuivre l'exploration par vous-mêmes.

void appliquer_fptr(fptr fct, int n)
   { fct(n); }
void appliquer_fsig(fsig fct, int n)
   { fct(n); }
template <class R, class A>
   R appliquer_ftempl(ftempl<R,A> f, A arg)
      { return f(arg); }
template <class R, class ... A>
   R appliquer_fvaria(fvaria<R, A...> f, A && ...args)
      { return f(forward<A>(args)...); }

Notez que l'écriture générique suivante, elle, fonctionnerait dans tous les cas :

template <class F, class ... Args>
   auto appliquer(F f, Args && ... args) // C++ 14
   {
      return f(forward<Args>(args)...);
   }

... ou encore :

template <class F, class ... Args>
   auto appliquer(F f, Args && ... args) -> decltype(f(forward<Args>(args)...)) // C++ 11
   {
      return f(forward<Args>(args)...);
   }

Pour les besoins de l'exemple, nous utiliserons trois entités appelables :

  • Une fonction f(), visible à droite
  • Une expression λ que vous verrez dans le programme de test (plus bas), et
  • Un foncteur F, visible à droite, dont chaque instance est capable de se convertir en pointeur de fonction sur demande

Le foncteur requiert sans doute quelques explications. À la manière des expressions λ, il expose un opérateur de conversion en pointeur de fonction, ce qui est possible du fait qu'il est sans état (il ne contient aucun attribut d'instance). Ainsi, son opérateur d'appel de fonction, operator(), est en mesure de déléguer son travail à une méthode de classe, et l'opérateur de conversion en pointeur de fonction peut retourner l'adresse de cette méthode.

On constate ici une différence entre fptr et fsig : l'opérateur de conversion en fptr compile et s'exécute correctement, alors que si vous tentez de compiler l'opérateur de conversion en fsig, vous aurez droit à une erreur de compilation (lors de mes tests : « error: function returning function »).

void f(int n)
   { cout << "f(" << n << ")" << endl; }
class F
{
   static void impl(int n)
      { cout << "F{}(" << n << ")" << endl; }
public:
   void operator()(int n)
      { impl(n); }
   //operator fsig() const
   //   { return impl; }
   operator fptr() const
      { return impl; }
};

Le code de test est proposé à droite. À noter :

  • Les quatre appels utilisant le pointeur de fonction f fonctionnent : tout compile et s'exécute correctement. Conséquemment, un pointeur de fonction sied à nos quatre signatures d'appelables
  • L'expression λ, à travers un objet nommé ou simplement créée au point d'utilisation, peut être utilisée à la fois comme un fptr et comme un fsig. Le compilateur n'étant pas capable de faire implicitement le pont entre le type int du deuxième paramètre et la signature générique d'un ftempl ou d'un fvaria, par contre, il n'est pas possible de passer l'expression λ aux fonctions appliquer_ftempl() et appliquer_fvaria() à moins d'expliciter les types impliqués, ce qui permet à tout le moins de passer la λ en tant que ftempl<void,int>
  • La situation est analogue dans le cas du foncteur. C'est compréhensible : une λ est en fait un foncteur généré à partir d'un minimum d'informations, après tout.
  • Enfin, il est possible d'affecter un appelable à un fptr mais pas à un fsig ou à ses équivalents plus génériques. Lors de mes tests, le message d'erreur obtenu fut « f1: initialization of a function ».

Tout cela nous indique que les signatures de fonctions sont en fait considérées comme représentant des fonctions, alors que les pointeurs de fonctions sont des indirections qui peuvent être copiées, pointer à divers endroits en mémoire et servir à titre de variables.

int main()
{
   //
   appliquer_fptr(f, 3);
   appliquer_fsig(f, 3);
   appliquer_ftempl(f, 3);
   appliquer_fvaria(f, 3);
   //
   auto lam = [](int n) {
      cout << "lambda(" << n << ")" << endl;
   };
   appliquer_fptr(lam, 3);
   appliquer_fsig(lam, 3);
   // appliquer_ftempl(lam, 3);
   // appliquer_fvaria(lam, 3);
   appliquer_ftempl<void, int>(lam, 3);
   // appliquer_fvaria<void, int>(lam, 3);
   //
   appliquer_fptr([](int n) {
      cout << "lambda(" << n << ")" << endl;
   }, 3); // même chose
   appliquer_fsig([](int n) {
      cout << "lambda(" << n << ")" << endl;
   }, 3); // même chose
   //
   F ftor;
   appliquer_fptr(ftor, 3);
   appliquer_fsig(ftor, 3);
   // appliquer_ftempl(ftor, 3);
   // appliquer_fvaria(ftor, 3);
   appliquer_ftempl<void, int>(ftor, 3);
   // appliquer_fvaria<void, int>(ftor, 3);
   //
   appliquer_fptr(F{}, 3); // même chose
   appliquer_fsig(F{}, 3); // même chose
   //
   fptr f0 = f;
   // fsig f1 = f; // illégal
   fptr f0 = f;
   // fctsig f1 = f;
   // ftempl<void, int> f1 = f;
   // fvaria<void, int> f2 = f;
}

Voilà.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !