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

142

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;
Jan Swart
la source
9
Si la fonction de rappel est connue au moment de la compilation, envisagez plutôt un modèle.
Baum mit Augen
4
Lors de l' implémentation d' une fonction de rappel, vous devez faire tout ce dont l'appelant a besoin. Si votre question porte vraiment sur la conception d' une interface de rappel, il n'y a pas assez d'informations ici pour y répondre. Que voulez-vous que le destinataire de votre rappel fasse? Quelles informations devez-vous transmettre au destinataire? Quelles informations le destinataire doit-il vous transmettre à la suite de l'appel?
Pete Becker

Réponses:

171

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::functionpour 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

leèmes
la source
9
les pointeurs de fonction ont une surcharge d'appel par rapport aux paramètres de modèle. les paramètres de modèle facilitent l'inlining, même si vous êtes passé de plus de 16 niveaux, car le code en cours d'exécution est décrit par le type du paramètre et non par la valeur. Et les objets de fonction de modèle stockés dans des types de retour de modèle sont un modèle commun et utile (avec un bon constructeur de copie, vous pouvez créer la fonction de modèle efficace invocable qui peut être convertie en std::functionune fonction effacée si vous devez la stocker en dehors de immédiatement appelé contexte).
Yakk - Adam Nevraumont
1
@tohecz Je mentionne maintenant si cela nécessite C ++ 11 ou non.
leemes
1
@Yakk Oh bien sûr, j'ai oublié ça! Ajouté, merci.
leemes
1
@MooingDuck Bien sûr, cela dépend de l'implémentation. Mais si je me souviens bien, en raison du fonctionnement de l'effacement de type, une autre indirection est-elle en cours? Mais maintenant que j'y repense, je suppose que ce n'est pas le cas si vous lui attribuez des pointeurs de fonction ou des lambdas sans capture ... (comme une optimisation typique)
leemes
1
@leemes: Oui, pour les pointeurs de fonction ou les lambdas sans capture, il devrait avoir la même surcharge qu'un c-func-ptr. Ce qui est toujours un blocage du pipeline + pas anodinement intégré.
Mooing Duck
25

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::functionimplé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::functionsauf 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 delegatedé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::functionfonctionne raisonnablement bien.

Yakk - Adam Nevraumont
la source
1
D'où cela 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.
Nikos du
@ nik-lz Je ne sais pas comment je vous apprendrais l'utilisation et l'historique des rappels en C dans un commentaire. Ou la philosophie de la programmation procédurale par opposition à la programmation fonctionnelle. Donc, vous repartirez vide.
Yakk - Adam Nevraumont
J'ai oublié this. Est-ce parce qu'il faut tenir compte du cas d'une fonction membre appelée, alors nous avons besoin du thispointeur 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.
Nikos le
@ Les fonctions membres de Nik-Lz ne sont pas des fonctions. Les fonctions n'ont pas d'état (d'exécution). Les rappels prennent un 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".
Yakk - Adam Nevraumont
Ouais, c'est ce dont je parlais. L'état d'exécution est essentiellement l'adresse de l'objet appelé (car il change entre les exécutions). Il en est encore this. C'est ce que je voulais dire. D'accord merci quand même.
Nikos le
17

Utilisez std::functionpour 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_contextargument 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.

Mike Seymour
la source
Quel est le type de p ici? sera-ce un type std :: function? void f () {}; auto p = f; p ();
3h51
14

La seule raison à éviter std::functionest 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::functiondonne à 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.

dasblinkenlight
la source
1

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

Vladon
la source
3
Pouvez-vous expliquer ce qu'est ce VMT?
Gupta