Lors de l'implémentation d'une fonction de rappel en C ++, dois-je toujours utiliser le pointeur de fonction de style C:
void (*callbackFunc)(int);
Ou devrais-je utiliser std :: function:
std::function< void(int) > callbackFunc;
Lors de l'implémentation d'une fonction de rappel en C ++, dois-je toujours utiliser le pointeur de fonction de style C:
void (*callbackFunc)(int);
Ou devrais-je utiliser std :: function:
std::function< void(int) > callbackFunc;
Réponses:
En bref, à utiliserstd::function
sauf si vous avez une raison de ne pas le faire.
Les pointeurs de fonction ont l'inconvénient de ne pas pouvoir capturer un certain contexte. Vous ne pourrez par exemple pas passer une fonction lambda comme rappel qui capture certaines variables de contexte (mais cela fonctionnera si elle n'en capture aucune). L'appel d'une variable membre d'un objet (c'est-à-dire non statique) n'est donc pas non plus possible, car l'objet ( this
-pointer) doit être capturé. (1)
std::function
(depuis C ++ 11) consiste principalement à stocker une fonction (la transmettre ne nécessite pas qu'elle soit stockée). Par conséquent, si vous souhaitez stocker le rappel par exemple dans une variable membre, c'est probablement votre meilleur choix. Mais aussi si vous ne le stockez pas, c'est un bon "premier choix" bien qu'il ait l'inconvénient d'introduire une (très petite) surcharge lors de l'appel (donc dans une situation très critique pour les performances, cela peut être un problème, mais dans la plupart ça ne devrait pas). C'est très «universel»: si vous vous souciez beaucoup du code cohérent et lisible et que vous ne voulez pas penser à chaque choix que vous faites (c'est-à-dire que vous voulez garder les choses simples), utilisez std::function
pour chaque fonction que vous transmettez.
Pensez à une troisième option: si vous êtes sur le point d'implémenter une petite fonction qui rapporte quelque chose via la fonction de rappel fournie, considérez un paramètre de modèle , qui peut alors être n'importe quel objet appelable , c'est-à-dire un pointeur de fonction, un foncteur, un lambda a std::function
, ... L'inconvénient ici est que votre fonction (externe) devient un modèle et doit donc être implémentée dans l'en-tête. D'un autre côté, vous obtenez l'avantage que l'appel au callback peut être intégré, car le code client de votre fonction (externe) "voit" l'appel au callback, les informations de type exactes étant disponibles.
Exemple pour la version avec le paramètre template (écrire &
au lieu de &&
pour pré-C ++ 11):
template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
...
callback(...);
...
}
Comme vous pouvez le voir dans le tableau suivant, tous ont leurs avantages et leurs inconvénients:
+-------------------+--------------+---------------+----------------+
| | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture | no(1) | yes | yes |
| context variables | | | |
+-------------------+--------------+---------------+----------------+
| no call overhead | yes | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be inlined | no | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be stored | yes | yes | no(2) |
| in class member | | | |
+-------------------+--------------+---------------+----------------+
| can be implemented| yes | yes | no |
| outside of header | | | |
+-------------------+--------------+---------------+----------------+
| supported without | yes | no(3) | yes |
| C++11 standard | | | |
+-------------------+--------------+---------------+----------------+
| nicely readable | no | yes | (yes) |
| (my opinion) | (ugly type) | | |
+-------------------+--------------+---------------+----------------+
(1) Des solutions de contournement existent pour surmonter cette limitation, par exemple en passant les données supplémentaires en tant que paramètres supplémentaires à votre fonction (externe): myFunction(..., callback, data)
appellera callback(data)
. C'est le "rappel avec arguments" de style C, qui est possible en C ++ (et d'ailleurs fortement utilisé dans l'API WIN32) mais qui doit être évité car nous avons de meilleures options en C ++.
(2) Sauf si nous parlons d'un modèle de classe, c'est-à-dire que la classe dans laquelle vous stockez la fonction est un modèle. Mais cela signifierait que du côté client, le type de la fonction décide du type de l'objet qui stocke le rappel, ce qui n'est presque jamais une option pour les cas d'utilisation réels.
(3) Pour pré-C ++ 11, utilisez boost::function
std::function
une fonction effacée si vous devez la stocker en dehors de immédiatement appelé contexte).
void (*callbackFunc)(int);
peut être une fonction de rappel de style C, mais c'est une fonction horriblement inutilisable de mauvaise conception.
Un rappel de style C bien conçu ressemble à void (*callbackFunc)(void*, int);
- il a un void*
pour permettre au code qui effectue le rappel de maintenir l'état au-delà de la fonction. Ne pas faire cela oblige l'appelant à stocker l'état globalement, ce qui est impoli.
std::function< int(int) >
finit par être légèrement plus cher que l' int(*)(void*, int)
invocation dans la plupart des implémentations. Il est cependant plus difficile pour certains compilateurs de se connecter. Il existe des std::function
implémentations de clones qui rivalisent avec les frais généraux d'invocation de pointeur de fonction (voir «délégués les plus rapides possibles», etc.) qui peuvent se frayer un chemin dans les bibliothèques.
Désormais, les clients d'un système de rappel ont souvent besoin de configurer des ressources et de les éliminer lorsque le rappel est créé et supprimé, et d'être conscients de la durée de vie du rappel. void(*callback)(void*, int)
ne fournit pas cela.
Parfois, cela est disponible via la structure de code (le rappel a une durée de vie limitée) ou via d'autres mécanismes (annulation de l'enregistrement des rappels et autres).
std::function
fournit un moyen de gestion de durée de vie limitée (la dernière copie de l'objet disparaît lorsqu'elle est oubliée).
En général, j'utiliserais un std::function
sauf si les problèmes de performances se manifestent. S'ils le faisaient, je chercherais d'abord les changements structurels (au lieu d'un rappel par pixel, que diriez-vous de générer un processeur de ligne de balayage basé sur le lambda que vous me passez? Ce qui devrait être suffisant pour réduire la surcharge des appels de fonction à des niveaux triviaux. ). Ensuite, si cela persiste, j'écrirais un delegate
délégué le plus rapide possible et je verrais si le problème de performances disparaît.
Je n'utiliserais principalement que des pointeurs de fonction pour les API héritées ou pour créer des interfaces C pour communiquer entre différents codes générés par les compilateurs. Je les ai également utilisés comme détails d'implémentation interne lorsque j'implémente des tables de sauts, l'effacement de types, etc.: lorsque je les produis et les consomme et que je ne les expose pas en externe pour un code client à utiliser, et que les pointeurs de fonction font tout ce dont j'ai besoin .
Notez que vous pouvez écrire des wrappers qui transforment un std::function<int(int)>
en un int(void*,int)
rappel de style, en supposant qu'il existe une infrastructure de gestion de la durée de vie des rappels appropriée. Donc, en tant que test de fumée pour tout système de gestion de la durée de vie des rappels de style C, je m'assurerais que l'emballage d'un std::function
fonctionne raisonnablement bien.
void*
vient-il? Pourquoi voudriez-vous maintenir l'état au-delà de la fonction? Une fonction doit contenir tout le code dont elle a besoin, toutes les fonctionnalités, il suffit de lui transmettre les arguments souhaités et de modifier et de renvoyer quelque chose. Si vous avez besoin d'un état externe, pourquoi un functionPtr ou un rappel porterait-il ce bagage? Je pense que le rappel est inutilement complexe.
this
. Est-ce parce qu'il faut tenir compte du cas d'une fonction membre appelée, alors nous avons besoin du this
pointeur pour pointer vers l'adresse de l'objet? Si je me trompe, pourriez-vous me donner un lien vers où je peux trouver plus d'informations à ce sujet, car je ne trouve pas grand chose à ce sujet. Merci d'avance.
void*
pour permettre la transmission de l'état d'exécution. Un pointeur de fonction avec un void*
et un void*
argument peut émuler un appel de fonction membre à un objet. Désolé, je ne connais pas de ressource qui explique "la conception de mécanismes de rappel C 101".
this
. C'est ce que je voulais dire. D'accord merci quand même.
Utilisez std::function
pour stocker des objets appelables arbitraires. Il permet à l'utilisateur de fournir le contexte nécessaire pour le rappel; un pointeur de fonction simple ne le fait pas.
Si vous avez besoin d'utiliser des pointeurs de fonction simples pour une raison quelconque (peut-être parce que vous voulez une API compatible C), alors vous devez ajouter un void * user_context
argument afin qu'il soit au moins possible (bien que peu pratique) pour qu'il accède à un état qui n'est pas directement passé au fonction.
La seule raison à éviter std::function
est la prise en charge des compilateurs hérités qui ne prennent pas en charge ce modèle, qui a été introduit dans C ++ 11.
Si la prise en charge du langage pré-C ++ 11 n'est pas une exigence, utiliser std::function
donne à vos appelants plus de choix dans l'implémentation du rappel, ce qui en fait une meilleure option par rapport aux pointeurs de fonction "simples". Il offre aux utilisateurs de votre API plus de choix, tout en faisant abstraction des spécificités de leur implémentation pour votre code qui effectue le rappel.