web-dev-qa-db-fra.com

Devrais-je utiliser std :: function ou un pointeur de fonction en C++?

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;
118
worker11811

_ {En bref, utilisez std::function à moins que vous n'ayez une raison de ne pas le faire.

Les pointeurs de fonction ont l'inconvénient de ne pas être capable de capturer un peu de contexte. Vous ne pourrez pas, par exemple, passer une fonction lambda en tant que 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) est donc également impossible, car l'objet (this- pointeur) doit être capturé.(1)

std::function (depuis C++ 11) est principalement destiné à stocker une fonction (sa transmission ne nécessite pas son stockage). 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 présente l'inconvénient d'introduire des frais généraux (très minimes) lors de l'appel (ce qui peut poser problème dans une situation très critique en termes de performances, mais dans la plupart des cas). ça ne devrait pas). Il est très "universel": si vous tenez à un code cohérent et lisible et que vous ne voulez pas penser à tous les choix que vous faites (c’est-à-dire que vous voulez rester simple), 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 ensuite quelque chose via la fonction de rappel fournie, considérez un paramètre de modèle, qui peut alors être tout objet appelable, c'est-à-dire un pointeur de fonction, un foncteur, un lambda, un std::function, ... Le problème est que votre fonction (extérieure) devient un modèle et doit donc être implémentée dans l'en-tête. D'autre part, vous avez l'avantage que l'appel au rappel peut être en ligne, car le code client de votre fonction (extérieure) "voit" l'appel au rappel, l'information de type exacte sera disponible.

Exemple pour la version avec le paramètre template (write & au lieu de && pour les versions antérieures à C++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

Comme vous pouvez le constater dans le tableau ci-dessous, tous présentent des avantages et des 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 transmettant les données supplémentaires en tant que paramètres supplémentaires à votre fonction (extérieure): myFunction(..., callback, data) appellera callback(data). C'est le "callback avec arguments" de style C, qui est possible en C++ (et d'ailleurs très utilisé dans l'API WIN32), mais qui devrait être évité car nous avons de meilleures options en C++.

(2) Sauf s’il s’agit 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é du 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 les versions antérieures à C++ 11, utilisez boost::function

148
leemes

void (*callbackFunc)(int); est peut-être une fonction de rappel de style C, mais elle est terriblement inutilisable et de conception médiocre.

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 conserver un état au-delà de la fonction. Ne pas le faire oblige l'appelant à stocker l'état global, ce qui est impoli.

std::function< int(int) > finit par être légèrement plus cher que int(*)(void*, int) dans la plupart des implémentations. Il est cependant plus difficile pour certains compilateurs de s’inscrire en ligne. Il existe des implémentations de clonage std::function qui rivalisent avec le temps système d'invocation de pointeur de fonction (voir 'Délégués les plus rapides possibles', etc.) pouvant entrer 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 de connaître 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 callback a une durée de vie limitée) ou via d'autres mécanismes (désinscription des callbacks, etc.).

std::function fournit un moyen de gestion de la durée de vie limitée (la dernière copie de l'objet disparaît quand il est oublié).

En général, j'utiliserais un std::function à moins que le rendement ne concerne le manifeste. S'ils le faisaient, je commencerais par rechercher des modifications structurelles (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 transmettez?), Ce qui devrait être suffisant pour réduire la surcharge de l'appel de fonction à des niveaux triviaux. ). Ensuite, s’il persiste, j’écrirai une delegate sur la base des délégués les plus rapides possibles et verrai si le problème de performances disparaît.

Je n'utiliserais généralement que des pointeurs de fonction pour les API héritées, ou pour créer des interfaces C pour la communication entre le code généré par les différents compilateurs. Je les ai également utilisées en tant que détails d'implémentation internes lors de l'implémentation de tables de saut, d'effacement de type, etc.: lorsque je le produis et le consomme, et que je ne l'expose pas en externe pour aucun code client, les pointeurs de fonction font tout ce dont j'ai besoin. .

Notez que vous pouvez écrire des wrappers qui transforment une std::function<int(int)> en un rappel de style int(void*,int), en supposant qu'il existe une infrastructure de gestion de la durée de vie du rappel appropriée. Donc, en tant que test de fumée pour tout système de gestion de durée de vie de rappel de style C, je m'assurerais que le wrapping d'un std::function fonctionne raisonnablement bien.

22

Utilisez std::function pour stocker des objets appelables arbitrairement. Il permet à l'utilisateur de fournir le contexte nécessaire au 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 souhaitez une API compatible C), vous devez ajouter un argument void * user_context afin qu'il soit au moins possible (bien que gênant) d'accéder à un état non directement transmis la fonction.

16
Mike Seymour

La seule raison d'éviter std::function est la prise en charge des compilateurs hérités qui ne prennent pas en charge ce modèle, introduit en C++ 11.

Si la prise en charge du langage pré-C++ 11 n'est pas requise, l'utilisation de std::function offre à vos appelants davantage de choix pour la mise en œuvre 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 résumant les spécificités de leur implémentation pour votre code effectuant le rappel.

13
dasblinkenlight

std::function peut amener VMT au code dans certains cas, ce qui a un impact sur les performances.

0
vladon