Standard C++
Pointeurs vers des fonctions membres
Le type de « pointeur vers une fonction membre » est-il différent de « pointeur vers une fonction » ?
Oui.
Considérez la fonction suivante :
int f(char a, float b);
Le type de cette fonction est différent selon qu’il s’agit d’une fonction ordinaire ou d’une fonction membre nonstatic
d’une certaine classe :
Note : si c’est une fonction membre static
de class
Fred
, son type est le même que si c’était une fonction ordinaire : « int (*)(char,float)
« .
Comment puis-je passer un pointeur-à-fonction-membre à un gestionnaire de signal, un callback d’événement X, un appel système qui démarre un thread/tâche, etc ?
Ne le faites pas.
Parce qu’une fonction membre n’a pas de sens sans un objet sur lequel l’invoquer, vous ne pouvez pas le faire directement (si The X WindowSystem était réécrit en C++, il passerait probablement des références à des objets autour, pas seulement des pointeurs à des fonctions;naturellement les objets incarneraient la fonction requise et probablement beaucoup plus).
En tant que correctif pour un logiciel existant, utilisez une fonction de haut niveau (non membre) comme un wrapper qui prend un objet obtenu par une autre technique. Selon la routine que vous appelez, cette « autre technique » peut être triviale ou nécessiter un peu de travail de votre part. L’appel système qui démarre un thread, par exemple, peut exiger que vous passiez un pointeur de fonction avec un void*
, afin que vous puissiez passer le pointeur d’objet dans le void*
. De nombreux systèmes d’exploitation en temps réel font quelque chose de similaire pour la fonction qui démarre une nouvelle tâche. Dans le pire des cas, vous pourriez stocker le pointeur d’objet dans une variable globale ; cela pourrait être nécessaire pour les gestionnaires de signaux Unix (mais les globales sont, en général, indésirables). Dans tous les cas,la fonction de niveau supérieur appellerait la fonction membre désirée sur l’objet.
Voici un exemple du pire cas (en utilisant un global). Supposons que vous vouliez appeler Fred::memberFn()
sur une interruption:
Note : les fonctions membres static
ne nécessitent pas un objet réel pour être invoquées, sles pointeurs vers les fonctions membresstatic
sont généralement compatibles en termes de type avec les pointeurs vers les fonctions ordinaires. Cependant,bien que cela fonctionne probablement sur la plupart des compilateurs, il faudrait en fait qu’il s’agisse d’une extern "C"
fonction non-membre pour que ce soit correct, puisque le « linkage C » ne couvre pas seulement des choses comme le maniement des noms, mais aussi les conventions d’appel, qui peuvent être différentes entre C et C++.
Pourquoi est-ce que je continue à obtenir des erreurs de compilation (inadéquation de type) lorsque j’essaie d’utiliser une fonction membre comme routine de service d’interruption ?
C’est un cas particulier des deux questions précédentes, donc lisez d’abord les deux réponses précédentes.
Les fonctions membres non-static
ont un paramètre caché qui correspond au pointeur this
. Le pointeur this
pointe vers les données d’instance de l’objet. Le matériel/firmware d’interruption du système n’est pas capable de fournir l’argument du pointeur this
. Vous devez utiliser des fonctions « normales » (non membres de la classe) ou des fonctions membres static
comme routines de service d’interruption.
Une solution possible est d’utiliser un membre static
comme routine de service d’interruption et de faire en sorte que cette fonction cherche quelque part à trouver la paire instance/membre qui doit être appelée lors de l’interruption. Ainsi, l’effet est qu’une fonction membre estinvoquée sur une interruption, mais pour des raisons techniques, vous devez d’abord appeler une fonction intermédiaire.
Pourquoi ai-je du mal à prendre l’adresse d’une fonction C++ ?
Réponse courte : si vous essayez de la stocker dans (ou de la passer comme) un pointeur vers la fonction, alors c’est le problème – c’est un corollaire de la FAQ précédente.
Réponse longue : En C++, les fonctions membres ont un paramètre implicite qui pointe vers l’objet (le pointeur this
à l’intérieur de la fonction membre). Les fonctions C normales peuvent être considérées comme ayant une convention d’appel différente des fonctions membres, de sorte que les types de leurs pointeurs (pointeur vers la fonction membre vs pointeur vers la fonction) sont différents et incompatibles. C++ introduit un nouveau type de pointeur, appelé pointeur-à-membre, qui ne peut être invoqué qu’en fournissant un objet.
NOTE : n’essayez pas de « couler » une fonction pointeur-à-membre dans une fonction pointeur-à-fonction ; le résultat est indéfini et probablement désastreux. Par exemple, il n’est pas nécessaire qu’un pointeur vers une fonction membre contienne l’adresse machine de la fonction appropriée. Comme il a été dit dans le dernier exemple, si vous avez un pointeur vers une fonction C régulière, utilisez soit une fonction atop-level (non membre), soit une fonction static
(classe) membre.
Comment puis-je éviter les erreurs de syntaxe lors de la création de pointeurs vers des membres ?
Utilisez une typedef
.
Oui, d’accord, je sais : vous êtes différent. Vous êtes intelligent. Vous pouvez faire ce genre de choses sans un typedef
. Soupir. J’ai reçu de nombreux courriels de personnes qui, comme vous, ont refusé de suivre les conseils simples de cette FAQ. Ils ont perdu des heures et des heures de leur temps, alors que 10 secondes de typedef
s leur auraient simplifié la vie. De plus, regardez les choses en face, vous n’êtes pas en train d’écrire du code que vous seul pouvez lire ; vous espérez écrire votre code que d’autres pourront aussi lire – quand ils sont fatigués – quand ils ont leurs propres délais et leurs propres défis. Alors pourquoi se compliquer intentionnellement la vie et celle des autres ? Soyez intelligent : utilisez un typedef
.
Voici un exemple de classe :
class Fred {public: int f(char x, float y); int g(char x, float y); int h(char x, float y); int i(char x, float y); // ...};
Le typedef est trivial :
typedef int (Fred::*FredMemFn)(char x, float y); // Please do this!
C’est tout ! FredMemFn
est le nom du type, et un pointeur de ce type pointe vers n’importe quel membre de Fred
qui prend(char,float)
, comme les f
, g
, h
et i
de Fred
.
Il est alors trivial de déclarer un pointeur de fonction membre:
int main(){ FredMemFn p = &Fred::f; // ...}
Et il est également trivial de déclarer des fonctions qui reçoivent des pointeurs de fonction membre:
void userCode(FredMemFn p){ /*...*/ }
Et il est également trivial de déclarer des fonctions qui retournent des pointeurs de fonction membre:
FredMemFn userCode(){ /*...*/ }
Alors, s’il vous plaît, utilisez un typedef
. Soit cela, soit ne m’envoyez pas d’email sur les problèmes que vous avez avec vos pointeurs de fonctions-membres !
Comment puis-je éviter les erreurs de syntaxe lors de l’appel d’une fonction-membre en utilisant un pointeur de fonction-membre ?
Si vous avez accès à un compilateur et à une bibliothèque standard qui implémente les parties appropriées de la prochaine norme C++17, utilisez std::invoke
. Sinon, utilisez une macro #define
.
S’il vous plaît.
Pretty please.
Je reçois beaucoup trop de courriels de personnes confuses qui ont refusé de suivre ce conseil. C’est tellement simple. Je sais, vous n’avez pas besoin destd::invoke
ou d’une macro, et l’expert à qui vous avez parlé peut le faire sans l’un ou l’autre, mais s’il vous plaît, ne laissez pas votre ego se mettre en travers de ce qui est important : l’argent. D’autres programmeurs devront lire / maintenir votre code. Oui, je sais : vous êtes plus intelligent que tout le monde ; bien. Et vous êtes génial ; très bien. Mais n’ajoutez pas de complexité inutile à votre code.
Utiliser std::invoke
est trivial. Note : FredMemFn
est une typedef
pour un pointeur à type de membre:
Si vous ne pouvez pas utiliser std::invoke
, réduisez le coût de maintenance en utilisant, paradoxalement, une macro #define
dans ce cas particulier.
(Normalement, je n’aime pas les macros #define
, mais vous devriez les utiliser avec les tomembres pointeurs parce qu’ils améliorent la lisibilité et l’écriture de ce genre de code.)
La macro est triviale:
#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))
L’utilisation de la macro est également triviale. Note : FredMemFn
est un typedef
pour un pointeur vers un type de membre:
La raison pour laquelle std::invoke
ou cette macro est une bonne idée est que les invocations de fonctions membres sont souvent beaucoup plus complexes que l’exemple simple qui vient d’être donné. La différence en termes de lisibilité et d’écriture est significative.comp.lang.c++
a dû supporter des centaines et des centaines de messages de programmeurs confus qui ne parvenaient pas à maîtriser la syntaxe. Presque toutes ces erreurs auraient disparu s’ils avaient utilisé std::invoke
ou la macro ci-dessus.
Note : les macros #define
sont mauvaises de 4 façons différentes : mauvaise#1,mauvaise#2, mauvaise#3, et mauvaise#4. Mais elles sont encore utiles parfois. Mais vous devriez tout de même ressentir un vague sentiment de honte après les avoir utilisées.
Comment puis-je créer et utiliser un tableau de pointeur-à-fonction membre ?
Utilisez à la fois les typedef
et std::invoke
ou la macro #define
décrite plus tôt, et vous avez terminé à 90%.
Etape 1 : créer une typedef
:
Etape 2 : créer une macro #define
si vous n’avez pas std::invoke
:
#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))
Maintenant votre tableau de pointeurs vers les fonctions membres est simple :
FredMemFn a = { &Fred::f, &Fred::g, &Fred::h, &Fred::i };
Et votre utilisation d’un des pointeurs de fonctions membres est également simple :
void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: std::invoke(a, fred, 'x', 3.14);}
ou si vous n’avez pas std::invoke
,
void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: CALL_MEMBER_FN(fred, a) ('x', 3.14);}
Note : les macros #define
sont mauvaises de 4 façons différentes : mauvaise#1, mauvaise#2, mauvaise#3, et mauvaise#4. Mais elles sont toujours utiles parfois. Ayez honte, sentez-vous coupable, mais quand une construction maléfique comme une macro améliore votre logiciel, utilisez-la.
Comment puis-je déclarer un pointeur vers une fonction membre qui pointe vers une fonction membre const ?
Réponse courte : ajoutez un const
à droite du )
lorsque vous utilisez un typedef
pour déclarer le type de pointeur de fonction membre.
Par exemple, supposons que vous voulez un pointeur de fonction membre qui pointe sur Fred::f
, Fred::g
ou Fred::h
:
class Fred {public: int f(int i) const; int g(int i) const; int h(int j) const; // ...};
Alors quand vous utilisez un typedef
pour déclarer le type de pointeur de fonction membre, cela devrait ressembler à ceci:
C’est ça !
Alors, vous pouvez déclarer/passer/retourner des pointeurs de fonction membre comme d’habitude:
Quelle est la différence entre les opérateurs .* et ->*?
Vous n’aurez pas besoin de comprendre cela si vous utilisez std::invoke
ou une macro pour les appels de pointeur de fonction membre. Ohyea, veuillez utiliser std::invoke
ou une macro dans ce cas. Et ai-je mentionné que vous devriez utiliser std::invoke
ou une macro dans ce cas??!?
Par exemple:
Mais veuillez considérer l’utilisation d’un std::invoke
ou d’une macro à la place:
void sample(Fred x, Fred& y, Fred* z, FredMemFn func){ std::invoke(func, x, 42, 3.14); std::invoke(func, y, 42, 3.14); std::invoke(func, *z, 42, 3.14);}
ou
Comme discuté plus tôt, les invocations du monde réel sont souvent beaucoup plus compliquées que les simplesones ici, donc l’utilisation d’un std::invoke
ou d’une macro améliorera généralement l’écriture et la lisibilité de votre code.
Puis-je convertir un pointeur vers une fonction membre en un void* ?
Non !
Détails techniques : les pointeurs vers les fonctions membres et les pointeurs vers les données ne sont pas nécessairement représentés de la même manière. Un pointeur vers une fonction membre pourrait être une structure de données plutôt qu’un pointeur simple. Pensez-y : s’il pointe sur une fonction virtuelle, il pourrait ne pas réellement pointer sur un tas de code résoluble statiquement, donc il pourrait même ne pas être une adresse normale – il pourrait être une structure de données différente de quelque sorte.
Veuillez ne pas m’envoyer de courriel si ce qui précède semble fonctionner sur votre version particulière de votre compilateur particulier sur votre système d’exploitationparticulier. Je m’en moque. C’est illégal, point final.
Puis-je convertir un pointeur de fonction en un void* ?
Non !
Détails techniques : void*
les pointeurs sont des pointeurs vers des données, et les pointeurs de fonction pointent vers des fonctions. Le langage n’exige pas que les fonctions et les données soient dans le même espace d’adressage, donc, à titre d’exemple et non de limitation, sur lesarchitectures qui les ont dans des espaces d’adressage différents, les deux types de pointeurs différents ne seront pas comparables.
Veuillez ne pas m’envoyer de courriel si ce qui précède semble fonctionner sur votre version particulière de votre compilateur particulier sur votre système d’exploitation particulier. Je m’en moque. C’est illégal, point final.
J’ai besoin de quelque chose comme des pointeurs de fonction, mais avec plus de flexibilité et/ou de sécurité thread ; y a-t-il un autre moyen ?
Utilisez un fonctionnoïde.
Que diable est un fonctionnoïde, et pourquoi devrais-je en utiliser un ?
Les fonctionnoïdes sont des fonctions sur des stéroïdes. Les fonctionnoïdes sont strictement plus puissants que les fonctions, et ce pouvoir supplémentaire résout certains (pas tous) des défis typiquement rencontrés lorsque vous utilisez des pointeurs de fonction.
Travaillons un exemple montrant une utilisation traditionnelle des pointeurs de fonction, puis nous traduirons cet exemple en fonctionnoïdes. L’idée traditionnelle de pointeur de fonction est d’avoir un tas de fonctions compatibles:
int funct1( /*...params...*/ ) { /*...code...*/ }int funct2( /*...params...*/ ) { /*...code...*/ }int funct3( /*...params...*/ ) { /*...code...*/ }
Puis on y accède par des pointeurs de fonction:
typedef int(*FunctPtr)( /*...params...*/ );void myCode(FunctPtr f){ // ... f( /*...args-go-here...*/ ); // ...}
Parfois les gens créent un tableau de ces pointeurs de fonction:
FunctPtr array;array = funct1;array = funct1;array = funct3;array = funct2;// ...
Dans ce cas ils appellent la fonction en accédant au tableau :
array( /*...args-go-here...*/ );
Avec les functionoids, on crée d’abord une classe de base avec une méthode purement virtuelle:
Puis au lieu de trois fonctions, on crée trois classes dérivées:
Puis au lieu de passer un pointeur de fonction, on passe un Funct*
. Je vais créer un typedef
appelé FunctPtr
simplement pour que le reste du code soit similaire à l’ancienne approche:
typedef Funct* FunctPtr;void myCode(FunctPtr f){ // ... f->doit( /*...args-go-here...*/ ); // ...}
Vous pouvez créer un tableau de ceux-ci presque de la même manière :
Cela nous donne le premier indice sur le point où les functionoids sont strictement plus puissants que les function-pointers : le fait que l’approche functionoid a des arguments que vous pouvez passer aux ctors (montrés ci-dessus comme …ctor-args…) alors que la version function-pointers ne le fait pas. Pensez à un objet fonctionnoïde comme un appel de fonction lyophilisé (l’accent est mis sur le mot appel). Contrairement à un pointeur vers une fonction, un objet fonctionnoïde est (conceptuellement) un pointeur vers une fonction partiellement appelée. Imaginez pour l’instant une technologie qui vous permette de passer quelques arguments, mais pas tous, à une fonction, puis de geler cet appel (partiellement terminé). Imaginez que cette technologie vous rende une sorte de pointeur magique vers cet appel de fonction partiellement achevé et gelé. Puis, plus tard, vous passez les autres args en utilisant ce pointeur, et le système prend comme par magie vos args originaux (qui ont été gelés), les combine avec toutes les variables locales que la fonction a calculées avant d’être gelée, combine tout cela avec les args nouvellement passés, et continue l’exécution de la fonction là où elle s’est arrêtée lorsqu’elle a été gelée. Cela peut ressembler à de la science-fiction, mais c’est en fait ce que permettent de faire les objets fonctionnels. De plus, ils vous permettent de « compléter » à plusieurs reprises cette fonction lyophilisée avec différents « paramètres restants », aussi souvent que vous le souhaitez. De plus, ils vous permettent (et non pas vous obligent) à changer l’état lyophilisé lorsqu’il est appelé, ce qui signifie que les fonctionnalistes peuvent se souvenir des informations d’un appel à l’autre.
Retournons nos pieds sur terre et nous travaillerons quelques exemples pour expliquer ce que tout ce charabia signifie vraiment.
Supposons que les fonctions originales (dans le style des fonctions-pointeurs à l’ancienne) prennent des paramètres légèrement différents.
int funct1(int x, float y){ /*...code...*/ }int funct2(int x, const std::string& y, int z){ /*...code...*/ }int funct3(int x, const std::vector<double>& y){ /*...code...*/ }
Lorsque les paramètres sont différents, l’approche des pointeurs de fonction à l’ancienne est difficile à utiliser, puisque l’appelant ne sait pas quels paramètres passer (l’appelant a simplement un pointeur vers la fonction, pas le nom de la fonction ou,lorsque les paramètres sont différents, le nombre et les types de ses paramètres) (ne m’écrivez pas de courriel à ce sujet ; oui, vous pouvez le faire, mais vous devez vous tenir sur la tête et faire des choses désordonnées ; mais ne m’écrivez pas à ce sujet – utilisez plutôt des objets de fonction).
Avec les functionoids, la situation est, au moins parfois, bien meilleure. Puisqu’un functionoid peut être considéré comme un appel de fonction lyophilisé, il suffit de prendre les args non communs, comme ceux que j’ai appelés y
et/ou z
, et d’en faire desargs aux ctors correspondants. Vous pouvez également passer les args communs (dans ce cas le int
appelé x
) au ctor, mais vous n’êtes pas obligé de le faire – vous avez la possibilité de le/les passer à la méthode virtuelle pure doit()
à la place. Je vais supposer que vous voulez passer x
dans doit()
et y
et/ou z
dans les ctors:
class Funct {public: virtual int doit(int x) = 0;};
Alors au lieu de trois fonctions, vous créez trois classes dérivées:
Maintenant vous voyez que les paramètres du ctor sont gelés dans le functionoid quand vous créez le tableau de functionoids :
Donc, quand l’utilisateur invoque le doit()
sur un de ces functionoids, il fournit les args « restants », et l’appel combine conceptuellement les args originaux passés au ctor avec ceux passés dans la méthode doit()
:
array->doit(12);
Comme je l’ai déjà laissé entendre, l’un des avantages des functionoids est que vous pouvez avoir plusieurs instances de, disons, Funct1
dans votre tableau, et ces instances peuvent avoir différents paramètres lyophilisés en elles. Par exemple, array
etarray
sont tous deux de type Funct1
, mais le comportement de array->doit(12)
sera différent de celui dearray->doit(12)
puisque le comportement dépendra à la fois du 12 qui a été passé à doit()
et des args passés aux ctors.
Un autre avantage des functionoids est apparent si nous changeons l’exemple d’un tableau de functionoids à un localfunctionoid. Pour préparer le terrain, revenons à la bonne vieille approche fonction-pointeur, et imaginons que vous essayez de passer une fonction de comparaison à une routine sort()
ou binarySearch()
. La routine sort()
ou binarySearch()
s’appelle childRoutine()
et le type de pointeur de fonction de comparaison s’appelle FunctPtr
:
void childRoutine(FunctPtr f){ // ... f( /*...args...*/ ); // ...}
Alors différents appelants passeraient différents pointeurs de fonction selon ce qu’ils pensent être le mieux :
void myCaller(){ // ... childRoutine(funct1); // ...}void yourCaller(){ // ... childRoutine(funct3); // ...}
Nous pouvons facilement traduire cet exemple en un exemple utilisant des functionoids:
En prenant cet exemple comme toile de fond, nous pouvons voir deux avantages des functionoids par rapport aux function-pointers. L’avantage « ctor args « décrit ci-dessus, plus le fait que les fonctionnalistes peuvent maintenir l’état entre les appels d’une manière thread-safe.Avec les pointeurs de fonction ordinaire, les gens maintiennent normalement l’état entre les appels via des données statiques. Cependant, les données statiques ne sont pas intrinsèquement thread-safe – les données statiques sont partagées entre tous les threads. L’approche fonctionnoïde permet d’obtenir quelque chose d’intrinsèquement sûr pour les threads puisque le code se retrouve avec des données locales aux threads. L’implémentation est simple : changez la bonne vieille donnée statique en un membre de données d’instance à l’intérieur de l’objet this
du functionoid, etpoof, les données ne sont pas seulement thread-locales, mais elles sont même sûres avec les appels récursifs : chaque appel à yourCaller()
aura son propre objet Funct3
distinct avec ses propres données d’instance distinctes.
Notez que nous avons gagné quelque chose sans rien perdre. Si vous voulez des données globales au niveau du thread, les fonctionnoïdes peuvent aussi les donner aux jeunes : il suffit de les changer d’un membre de données d’instance à l’intérieur de l’objet this
du fonctionnoïde à un membre de données statiques à l’intérieur de la classe du fonctionnoïde, ou même à une donnée statique à portée locale. Vous ne seriez pas mieux qu’avec les pointeurs de fonction, mais vous ne seriez pas pire non plus.
L’approche du functionoid vous donne une troisième option qui n’est pas disponible avec l’approche à l’ancienne : le functionoid laisse les appelants décider s’ils veulent des données locales au thread ou globales au thread. Ils seraient responsables d’utiliser deselocks dans les cas où ils veulent des données thread-global, mais au moins ils auraient le choix. C’est facile:
Les functionoids ne résolvent pas tous les problèmes rencontrés lors de la réalisation de logiciels flexibles, mais ils sont strictement plus puissants que les function-pointers et ils valent au moins la peine d’être évalués. En fait, vous pouvez facilement prouver que les functionoids ne perdent aucun pouvoir sur les function-pointers, puisque vous pouvez imaginer que la vieille approche des function-pointers est équivalente à avoir un objet functionoid global( !). Puisque vous pouvez toujours faire un objet fonctionnoïde global, vous n’avez pas perdu de terrain. QED.
Pouvez-vous faire des objets fonctionnalisés plus rapidement que les appels de fonction normaux ?
Oui.
Si vous avez un petit objet fonctionnalisé, et dans le monde réel c’est plutôt commun, le coût de l’appel de fonction peut être élevé par rapport au coût du travail effectué par l’objet fonctionnalisé. Dans la FAQ précédente, les systèmes fonctionnels étaient implémentés à l’aide de fonctions virtuelles et vous coûtaient généralement un appel de fonction. Une autre approche utilise des modèles.
L’exemple suivant est similaire dans son esprit à celui de la FAQ précédente. J’ai renommé doit()
enoperator()()
pour améliorer la lisibilité du code de l’appelant et pour permettre à quelqu’un de passer un pointeur de fonction régulier:
La différence entre cette approche et celle de la FAQ précédente est que le fuctionoid est « lié » à l’appelant au moment de la compilation plutôt qu’au moment de l’exécution. Pensez-y comme le passage d’un paramètre : si vous savez au moment de la compilation le type de fuctionoid que vous voulez finalement passer, alors vous pouvez utiliser la technique ci-dessus, et vous pouvez,au moins dans des cas typiques, obtenir un avantage de vitesse en ayant le compilateurinline-expandant le code du fuctionoid dans l’appelant. Voici un exemple :
template <typename FunctObj>void myCode(FunctObj f){ // ... f( /*...args-go-here...*/ ); // ...}
Lorsque le compilateur compile ce qui précède, il pourrait inline-expandre l’appel ce qui pourrait améliorer les performances.
Voici une façon d’appeler ce qui précède :
void blah(){ // ... Funct2 x("functionoids are powerful", 42); myCode(x); // ...}
A part : comme cela a été laissé entendre dans le premier paragraphe ci-dessus, vous pouvez également passer dans les noms des fonctions normales (bien que vouspouviez encourir le coût de l’appel de fonction lorsque l’appelant les utilise) :
void myNormalFunction(int x);void blah(){ // ... myCode(myNormalFunction); // ...}
Quelle est la différence entre un fonctionnoïde et un foncteur ?
Un fonctionnoïde est un objet qui a une méthode principale. C’est fondamentalement l’extension OO d’une fonction de type C telle que asprintf(). On utiliserait un functionoid chaque fois que la fonction a plus d’un point d’entrée (c’est-à-dire plus d’une « méthode »)et/ou doit maintenir l’état entre les appels d’une manière thread-safe (l’approche de style C pour maintenir l’état entre les appels est d’ajouter une variable locale « statique » à la fonction, mais c’est horriblement dangereux dans un environnement multi-thread).
Un functor est un cas spécial d’un functionoid : c’est un functionoid dont la méthode est le « function-call operator, « operator()(). Puisqu’il surcharge l’opérateur d’appel de fonction, le code peut appeler sa méthode principale en utilisant la même syntaxe que celle utilisée pour un appel de fonction. Par exemple, si « foo » est un foncteur, pour appeler la méthode « operator()() » sur l’objet « foo », on dira « foo() ». L’avantage de cette méthode se trouve dans les modèles, puisque le modèle peut avoir un paramètre de modèle qui sera utilisé comme une fonction, et ce paramètre peut être soit le nom d’une fonction, soit un objet-facteur. Il y a un avantage de performance à ce que ce soit un objet-foncteur puisque la méthode « operator()() » peut être inlined (alors que si vous passez l’adressed’une fonction, elle doit, nécessairement, être non-inlined).
C’est très utile pour des choses comme la fonction « comparaison » sur les conteneurs triés. En C, la fonction de comparaison esttoujours passée par pointeur (par ex, voir la signature de « qsort() »), mais en C++ le paramètre peut arriver soit comme pointeur de fonction OU comme nom d’un objet-foncteur, et le résultat est que les conteneurs triés en C++ peuvent être, dans certains cas, beaucoup plus rapides (et jamais plus lents) que l’équivalent en C.
Puisque Java n’a rien de similaire aux templates, il doit utiliser la liaison dynamique pour toutes ces choses, et la liaison dynamique signifie nécessairement un appel de fonction. Normalement, ce n’est pas un gros problème, mais en C++, nous voulons permettre un code extrêmement performant. C’est-à-dire que le C++ a une philosophie « ne payez que si vous l’utilisez », ce qui signifie que le langage ne doit jamais imposer arbitrairement une surcharge par rapport à ce que la machine physique est capable de réaliser (bien sûr, un programmeur peut, facultativement, utiliser des techniques telles que la liaison dynamique qui, en général, imposera une certaine surcharge en échange de la flexibilité ou d’une autre « ilité », mais c’est au concepteur et au programmeur de décider s’ils veulent les avantages (et les coûts) de telles constructions).
Leave a Reply