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. |
|
Notez qu'il est possible d'écrire ceci...
... tout comme il est possible d'écrire ceci...
... selon vos préférences. 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. |
|
Notez que l'écriture générique suivante, elle, fonctionnerait dans tous les cas :
template <class F, class ... Args>
decltype(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 :
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 »). |
|
Le code de test est proposé à droite. À noter :
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. |
|
Voilà.
Quelques liens pour enrichir le propos.