12
votes

Magie de modèle pour l'emballage des rappels C qui prenez des paramètres vides *?

dire que j'utilise une API C qui vous permet d'enregistrer des rappels qui prennent un Void * fermeture: xxx

en C ++, il est agréable d'avoir des types plus puissants que VOID * Donc, je veux créer une enveloppe qui me permet de me contenter de vous enregistrer des rappels C ++ frappés à la place: xxx

Cela fonctionne bien. Une belle propriété de cette solution est qu'elle peut aligner mon rappel dans le wrapper. Ce jeu d'emballage a donc zéro surcharge. Je considère cela une exigence.

Mais ce serait bien que je puisse faire ressembler l'API plus comme ceci: xxx

J'espère que je peux atteindre le ci-dessus en inféré des paramètres de modèle. Mais je ne peux pas comprendre comment le faire fonctionner. Ma tentative jusqu'à présent est la suivante: xxx

mais cela ne fonctionne pas. Toute personne a une incantation magique qui rendra F2 () travail ci-dessus, tout en conservant la caractéristique de performance de surcharge zéro? Je veux quelque chose qui fonctionnera en C ++ 98.


15 commentaires

J'ai du mal à voir le point de cela. Quel avantage est un wrapper si cela devient coulé sur vide * de toute façon?


L'emballage enregistre la fonction C ++ de devoir faire une statique_cast.


Il peut également taper pour vous assurer que la fermeture que vous passez lorsque vous enregistrez, le rappel est le même type que le rappel prend comme paramètre.


En fait, cela n'est pas défini. Votre wrapper utilise un ABI C ++. Le rappel C comme tout code C utilise uniquement un C ABI. Si cela fonctionne, il vous suffit de gagner de la chance que l'ABI soit alignée.


@Lokiastari Le problème tiendrait toujours si, au lieu d'une API C littéral C, l'OP était confronté à un API C-style C .


Vous cherchez quelque chose comme Boost :: Tout? Il peut au moins ajouter un certain niveau de santé mentale - vous pouvez vous assurer que le type est correct lors de la conversion d'un vide *.


@Lokiastari: Qu'est-ce qui est indéfini à ce sujet? Je le convertissez en un vide * pour passer à C, C réussit-le à C ++ en tant que vide * que j'ai ensuite static_cast <> à un type de pointeur plus spécifique. Rien d'indéfini à ce sujet.


@Joshhaberman: C'est en dessous du niveau de langue. L'ABI définit lorsque des paramètres et des résultats sont mis. Comment le cadre de pile est nettoyé. Ce qui est sur le cadre de la pile pour la manipulation des exceptions. c etc ,c etc. Votre code passe une fonction C ++ avec un ABI C ++ sur une fonction qui s'attend à une fonction avec un ABI C. Vous venez de gagner de la chance que cela fonctionne.


@Lokiastari: Je sais à propos de Abis, mais une fonction déclarée "externe c" suit un abdi. La capacité de C ++ à appeler les fonctions «externe C» est bien établie.


@JoshABERMAN: Malheureusement, les fonctions de modèles ne peuvent pas être déclarées externes "C". Je ne vois pas non plus aucun externe "c" déclarations ci-dessus.


@Joshaberman une déclaration de fonction par ex. Un bloc de fonctionnement du paramètre de pointeur de fonction est déclaré pour accepter un pointeur sur une fonction de liaison de langue C, que callbackwrapper n'est pas.


@Lucdanton: Vous dites qu'une fonction C ++ régulière (c.-à-d. Avec une liaison C ++) ne peut jamais être transmise en tant que pointeur de fonction sur une fonction C? Je n'ai jamais entendu parler de cela, et je ne peux en trouver aucune mention dans la norme. Avez-vous une référence pour cela?


@Joshaberman Eh bien, un programme C ++ peut déclarer par ex. Typedef void callback_type (); externe "C" vide F (Callback_Type * FUNC, VOID * DATA); , garantissant ainsi que f est une fonction de liaison C en prenant un pointeur sur une fonction C ++. Comme vous pouvez probablement dire de l'expérience, cela n'est généralement pas fait. (La liaison linguistique est en 7.5 pour la standard.)


@Joshhaberman: C'est parce que c et C ++ sont des langues différentes. Vous ne pouvez pas montrer (prouver) un négatif. C'est pourquoi la norme définit ce que vous pouvez faire. Pas une liste de choses qu'il ne peut pas faire. Ce dont vous avez besoin est une chose dans la norme qui dit que c a été rendu binaire compatible avec C ++ pour que ce code fonctionne. Il n'y a pas de clause de ce type dans la norme.


@Joshhaberman: Vous pouvez voir Cette question pour une discussion sur ce numéro.


3 Réponses :


-3
votes

Pourquoi ne pas rendre votre fermeture une véritable fermeture (en incluant un état de dactylographié réel).

class CB
{
    public:
        virtual ~CB() {}
        virtual void action() = 0;
};

extern "C" void CInterface(void* data)
{
    try
    {
        reinterpret_cast<CB*>(data)->action();
    }
    catch(...){}
    // No gurantees about throwing exceptions across a C ABI.
    // So you need to catch all exceptions and drop them
    // Or probably log them
}

void RegisterAction(CB& action)
{
    register_callback(CInterface, &action);
}


30 commentaires

@KennyTM: Non, ça ne le fait pas. En fait, je suppose qu'il ne peut pas modifier cette fonction (car elle fait partie d'une bibliothèque). Sinon, nous ne serions pas à travers ces processus, nous allions simplement changer la bibliothèque sous-jacente à C ++ :-)


Cette solution impose une fonction de fonction virtuelle. J'ai besoin de quelque chose qui peut être inliné (j'ai spécifié cela comme une exigence).


@Joshhaberman: Oui tu as fait. Mais votre code n'enlyse pas non plus. Il ne semble donc pas juste d'ajouter d'autres contraintes que vous n'appliquez pas vraiment sur votre propre code. De plus, le coût d'une fonction virtuelle appeler sur un appel de fonction normal est insignifiant. Essayez de le calmer. Donc, le coût de cela est exactement le même que votre idée de wrapper. La différence est que cela fonctionne.


Mon code est Inlinge MyCallback () Inside Callbackwrapper. J'ai vérifié cela en regardant la sortie de la langue d'assemblage. Je veux quelque chose qui continuera à le faire avec une syntaxe plus agréable.


Aussi le coût n'est pas "exactement identique". Un appel de fonction virtuelle a un coût et je l'ai mesuré, et dans ma demande, il est important. Il est très aggravant lorsque les gens répondent à votre question en vous disant qu'ils connaissent mieux vos besoins que vous.


Oui, vous êtes correct, il y a un coût théorique. Mais avec toutes les autres choses, une CPU fait dans un système d'exploitation moderne, il est très difficile de faire du temps et de mesurer. Dans la plupart des situations, vous ne pouvez pas le faire le temps. Si vous prétendez avoir programmé la différence entre un appel de fonction normal et une fonction de fonction virtuelle et vue une différence, veuillez me montrer le code qui produit ces résultats. J'aimerais beaucoup le voir.


Ce que je trouve aggravant, c'est que les gens écrivent du code qui ne fonctionne pas (de manière portant) mais qui devoir faire face à un code de sous-standard pendant une longue période vous fait agacer les plus petites choses afin que je puisse être très ennuyeux. :-)


C'est très facile à montrer; Voici un repère que j'ai fouillé dans 10 minutes qui montre un ralentissement de 20% avec des fonctions virtuelles: Gist.github.com / Anonymous / 5597659


Aussi, vous avez tort de dire que MyCallback n'est pas inlincé dans mon exemple. Ici, je démontre que c'est: Gist.github.com/anonymous/5597712


@Joshaberman: Comme je soupçonnais que votre timing est faux. Vous comparez 1 appel de fonctions VS 1 Appel de fonction et 1 appel de fonction virtuelle. Donc, pas une comparaison des pommes à pommes. Donc, lorsque vous corrigez pour cela, les temps sont les mêmes.


@JoshABERMAN: Oui, il semble être inliné. D'ACCORD. Je m'excuse, je ne pensais pas que l'analyse statique fonctionnerait car vous passez un pointeur de fonction (plutôt qu'un foncteur). Mais ça fait. Donc, vous obtenez maintenant cette augmentation de la performance (mais pas parce que l'appel est virtuel) parce que vous faites un appel moins.


@Joshhaberman mais ne change toujours pas le fait que le code est cassé. Si rapide, le code brisé n'est toujours pas très utilisé.


Vous êtes Toujours mal sur le timing. Dans mon premier exemple, je n'ai pas créé de fonction distincte pour l'appel de la fonction directe, car il est évident qu'il pourrait être inlincé. Mais si cela n'est pas évident pour vous, j'ai créé cette version, qui montre exactement les mêmes résultats. Notez comment dans l'assemblage CMP et ICMP sont exactement les mêmes: gist.github.com/anonymous/5599803


Vous l'avez corrigé et ceux-ci courent maintenant en même temps aucune différence. Mais j'ai toujours raison dans mon assertion. Il n'y a pas de différence mesurable entre un appel de fonction normal et une fonction de fonction virtuelle en termes de coût. Si vous courez sur un appareil intégré sans OS, vous pourriez peut-être le voir potentiellement (en réalité le temps).


Le gist que j'ai lié montre une différence de 20% entre les appels de fonction virtuelle et non virtuelle sur X86-64. Pourquoi dites-vous qu'il n'y a "aucune différence mesurable"?


Parce que j'ai compilé couru et j'ai chronométré. Vous êtes probablement toujours chronométré votre code d'origine. Qui a une différence de 20% en raison de la différence d'appels. Dans votre code d'origine, vous avez eu une différence de 20%. Vous avez ajouté un appel de fonction et voyez toujours une différence de 20% afin que vous assumez que l'appel de la fonction est maintenant gratuit!


Je pense que vous mesurez probablement la mauvaise chose. J'ai effondré cela dans une seule référence qui teste les deux et imprime les résultats. Si vous exécutez ce programme (correctement optimisé) sur X86-64, je serai très surpris s'il imprime quelque chose de moins de 10% de fonction de fonction virtuelle (pour moi, il imprime 26%): gist.github.com/anonymous/5602346


Sighh. En discutant toujours de cela! Oui, vous obtenez une augmentation de 15% de la vitesse (si vous supprimez l'appel de la fonction). J'ai ajouté l'assemblage approprié à la gist afin de pouvoir valider. S'il vous plaît vérifier votre montage, je suis sûr que vous verrez exactement les mêmes résultats. Ce n'est pas ce que j'ai prétendu.


Je vois ce que vous dites maintenant, vous dites que le test est injuste car l'appel de fonction directe peut être inlidé. Mais le fait que les appels virtuels ne puissent pas être inlinés fait partie de leur coût et une partie de la raison pour laquelle vous avez tort de dire que le coût est "exactement le même". Mais même si nous oublions et empêchez l'appel direct d'être inliné, vous êtes toujours mal: gist.github.com/haberman/5621682


Si je sonne grincheuse, c'est parce que vous affirmez à plusieurs reprises quelque chose que je sais être faux.


@JOSHABERMAN: OK, je suis d'accord que l'inlinage rend la fonction appelle plus rapidement. Et c'est un avantage dans ce type de situation où la fonction est faible et que le coût de l'appel est supérieur au coût de la fonction.


@JOSHHABERMAN: Je ne suis pas d'accord (ce n'est pas faux) qu'un appel de fonction normal est plus cher (de manière mesurable), puis une fonction de fonction virtuelle. Le coût d'une recherche offset sur la page qui est plus que susceptible d'être dans le cache de niveau-0 (les vtables sont souvent accessibles) est extrêmement rapide (dans l'ordre des cycles). Mesurer cette différence dans un système d'exploitation réel est à côté de l'impossibilité, car des problèmes tels que les défauts de page entraîneront une telle variance dans le calendrier que les différences de cycle sont noyées.


@Joshhaberman: le fait que vous ne puissiez pas me montrer un exemple de travail qui a une différence mesurable est ce que j'ai vu à travers ma carrière. Les gens prétendent que les appels virtuels sont beaucoup plus lents (parce que cela semble si évident). Mais lorsqu'il est contesté de produire un peu de code qui montre cela. Ils ne peuvent jamais. Si c'est faux puis en tant qu'ingénieur, vous devriez pouvoir me montrer la preuve. Sachant que c'est faux parce que vous avez un sentiment n'est pas valide. Sachant que c'est faux parce que vous l'avez mesuré. Ensuite, nous avons quelque chose à discuter.


Je viens de vous montrer la preuve. Rien de mon argument est basé sur des sentiments. Mon dernier gist empêche le compilateur d'inliquer l'appel de la fonction directe. Vous ne pouviez pas obtenir une comparaison plus pépendant à pommes. Regardez l'assemblée vous-même. L'appel de la fonction virtuelle a toujours une surcharge mesurée de 10%.


Le résultat est également systématiquement 10% sur ma machine, ce n'est pas du tout très bruyant.


En outre, le fardeau de la preuve est sur vous de dire que deux choses sont exactement la même malgré le fait que l'on doit faire strictement plus de travail. L'hypothèse naturelle est que celle qui doit faire plus de travail sera plus lente.


@Joshhaberman: D'accord. L'appel virtuel prend un (ou peut-être un couple sur du matériel) plus d'instruction. Je dis le coût de cette instruction (puisqu'il accède à une page mise en cache) est perdu dans le bruit d'un système d'exploitation en cours d'exécution. Si nous étions sur un appareil intégré sans OS ou interruption, un appel virtuel serait différent de manière mesurable. Vous avez seulement montré ce que je suis déjà d'accord est vrai. Que l'inlinage peut être utile.


En outre, il est bien établi dans la littérature que la répartition de la fonction virtuelle a un coût direct mesurable (5% selon ce document: cs.ucsb.edu/dirs/oocsb/papers/oopsla96.ps )


Il y a une seconde, vous avez dit "mais lorsqu'il est contesté de produire un peu de code qui montre cela. Ils ne peuvent jamais." Ensuite, je vous ai montré certains. Mais vous l'ignorez - peut-être que vous l'ignorez tout au long de votre carrière? Ensuite, vous avez dit que c'était encore juste à propos de l'inliction, même s'il n'y a pas d'inlinage dans ma dernière référence. Il est clair qu'aucune preuve ne changera vos croyances.


@JoshABERMAN: raté le deuxième gist, je vais regarder et lire le papier. Vous avez vérifié que le code n'était pas inliné, n'est-ce pas?



4
votes

Cette fonction de modèle améliore la syntaxe marginalement.

#ifdef HAS_EXCEPTIONS
# define BEGIN_TRY try {
# define END_TRY } catch (...) {}
#else
# define BEGIN_TRY
# define END_TRY
#endif

template <typename CB>
void CallbackWrapper(void *p) {
    BEGIN_TRY
    return (*static_cast<CB*>(p))();
    END_TRY
}

struct MyCallback {
    MyCallback () {}
    void operator () () {}
};

template <typename CB>
void RegisterCallback (CB &x) {
    register_callback(CallbackWrapper<CB>, &x);
}

MyCallback cb;
RegisterCallback(cb);


18 commentaires

Merci, à vous, à la fois pour cette suggestion et pour la référence au problème ABI. Je pense qu'il est regrettable que la norme le dit, puisque dans la pratique, les ABIS ne diffèrent jamais et des tonnes de code reposent sur ceci (par exemple, en passant une fonction C ++ à pthread_create (). De manière réaliste, je ne pense pas que quiconque sera jamais capable de faire respecter cette règle. Mais je suis toujours heureux de savoir à ce sujet maintenant.


Aucune autre idée de faire appeler le modèle n'appelle plus jolie tout en laissant toujours l'inlinage?


@JOSHHABERMAN: Je suis incapable de comprendre un moyen de tirer le type de paramètre comme trait de la fonction, de sorte que les deux arguments de modèle sont nécessaires. J'avais initialement l'intention de permettre que le nom de la fonction soit passé, mais a échoué.


Vous ne pouvez pas au moins basculer les deux arguments de modèle et utiliser la déduction automatique pour la seconde ( t ): modèle ... < / Code> -> registreCallback (& x); ? Attendez, ne fonctionne pas, car void f (t *) ne saurait pas savoir t alors.


@Christianrau: Je pensais que c'était une fonctionnalité C ++ 11. Étais-je trompé?


@ user315052 non, la déduction automatique de l'argument de modèle automatique a toujours fonctionné (c'est ce que c'est comme ça comme std :: make_pair sont pour), mais la façon dont j'ai décrit ne fonctionnerait de toute façon pas, car f (t * ) doit savoir sur t à l'avance.


@Christianrau: Ah, c'est ce que tu voulais dire.


@Joshhaberman: J'ai mis à jour la réponse avec quelque chose qui devrait rendre les choses plus simples, mais vous définiriez vos rappels comme des foncteurs.


Merci, vos suggestions ont été très utiles!


@JOSHHABERMAN: Passer une fonction C ++ à pthread_create () . Oui cela arrive beaucoup. Mais lorsque vous travaillez à un endroit avec des programmeurs expérimentés, ils giftent votre main au temps de la révision du code et vous disent de partir et faites-la correctement. Les gens apprennent généralement après leur première fois et arrêtent de le faire. Le problème est qu'il brise beaucoup de systèmes là-bas. Votre expérience est limitée à un système unique, il ne casse pas. Mais dans mon expérience, cela rompt dans 50% des systèmes lorsque je travaillais à Veritas. Il y a beaucoup plus de compilateurs que clang / g ++ et plus d'os que vous pouvez secouer un bâton à


Ps. Vous devriez probablement attraper des exceptions pour éviter qu'ils soient propagés à travers des cadres de pile qui ne peuvent pas gérer des exceptions dans les bibliothèques C.


@Lokiastari: S'il vous plaît dites-moi quels systèmes vous savez de ce programme pour exécuter ce programme: gist.github.com/ Anonymous / 5599924


Ps. Le code que vous avez posté devrait fonctionner partout. Cela ne montre pas le point dont nous parlions. Dans votre code posté exécuté () sait que la fonction transcédée est une fonction C ++, même s'il s'agit d'une fonction C (car elle est compilée à l'intérieur d'une unité de compilation C ++). Ce que vous vouliez dire, c'est de mettre le extern "c" int horaires2 (int x) {retour x * 2; } .


@Lokiastari: La plupart des plates-formes spécifient une ABI combinée C et C ++. Je veux savoir s'il y a en fait une bonne raison d'avoir une ABI séparée, ou si cela se produit jamais dans la pratique. Si aucun de ceux-ci n'est vrai, et si un grand corps de code s'appuie sur eux étant la même (ce qu'il fait), alors je pense qu'il est raisonnablement probable qu'une version future de la norme considère celle-ci un défaut et de le corriger. Je déciderai toujours de savoir si je considère que c'est un pari sûr.


@Lokiastari: Je pense que mon exemple illustre le point: Peu importe ce que le compilateur "sait", si les types de fonctions sont différents, appelant une fonction C ++ à partir d'une fonction "externe C" est un comportement non défini. Le compilateur ne sait pas si cette fonction sera également appelée directement à partir de C avec un pointeur de fonction C.


Je suis personnellement d'accord, il n'y a pas de bonne raison d'avoir différents ABI pour la langue.


Le problème est qu'ils existent. Habituellement, le C ++ ABI est défini par Compiler Vender (et il peut par multiples sur un système). Le C ABI est généralement défini par le fournisseur de matériel (avec entrée d'OS) s'ils disposent d'un compilateur La version C ++ / C fonctionnera généralement bien ensemble. Mais cela ne signifie pas que les fournisseurs de tiers C ++ vont se conformer aux fournisseurs matériels C ++ ABI (ils sont libres de choisir leur propre (ils pourraient avoir de meilleures optimisations en raison de leurs améliorations abi)). Le C ABI est-il spécifique à la combinaison «Matériel / OS».


Ps. Votre code est faux (bien correct). Il devrait fonctionner partout parce que vous passez un pointeur de fonction C ++ sur une fonction d'attente d'un pointeur de fonction C ++. Il sait créer le bon appel à une fonction C ++. Comme je l'ai souligné ci-dessus. Si vous mettez le "extern" c " sur fois2 () alors vous obtenez l'incompatibilité.



1
votes

J'ai découvert une meilleure réponse à cette question que les autres réponses qui m'ont donné ici! (En réalité, c'était un autre ingénieur à l'intérieur de Google qui l'a suggéré).

Vous devez répéter le nom de la fonction deux fois, mais qui peut être résolu avec une macro. P>

Le modèle de base est le suivant: P>

// Func1, Func2, Func3: Template classes representing a function and its
// signature.
//
// Since the function is a template parameter, calling the function can be
// inlined at compile-time and does not require a function pointer at runtime.
// These functions are not bound to a handler data so have no data or cleanup
// handler.
template <class R, class P1, R F(P1)>
struct Func1 {
  typedef R Return;
  static R Call(P1 p1) { return F(p1); }
};

// ...

// FuncSig1, FuncSig2, FuncSig3: template classes reflecting a function
// *signature*, but without a specific function attached.
//
// These classes contain member functions that can be invoked with a
// specific function to return a Func/BoundFunc class.
template <class R, class P1>
struct FuncSig1 {
  template <R F(P1)>
  Func1<R, P1, F> GetFunc() { return Func1<R, P1, F>(); }
};

// ...

// Overloaded template function that can construct the appropriate FuncSig*
// class given a function pointer by deducing the template parameters.
template <class R, class P1>
inline FuncSig1<R, P1> MatchFunc(R (*f)(P1)) {
  (void)f;  // Only used for template parameter deduction.
  return FuncSig1<R, P1>();
}

// ...

// Function that casts the first parameter to the given type.
template <class R, class P1, R F(P1)>
R CastArgument(void *c) {
  return F(static_cast<P1>(c));
}

template <class F>
struct WrappedFunc;

template <class R, class P1, R F(P1)>
struct WrappedFunc<Func1<R, P1, F> > {
  typedef Func1<R, void*, CastArgument<R, P1, F> > Func;
};

template <class T>
generic_func_t *GetWrappedFuncPtr(T func) {
  typedef typename WrappedFunc<T>::Func Func;
  return Func().Call;
}

// User code:

#include <iostream>

typedef void (generic_func_t)(void*);

void StronglyTypedFunc(int *x) {
  std::cout << "value: " << *x << "\n";
}

int main() {
  generic_func_t *f = GetWrappedFuncPtr(
      MatchFunc(StronglyTypedFunc).GetFunc<StronglyTypedFunc>());
  int x = 5;
  f(&x);
}
  • L'utilisateur doit écrire fortlytypedfunc () prendre un pointeur à une chose spécifique. Li>
  • Cette fonction peut être appelée avec un argument annulé *. Li>
  • Il n'y a pas de fonctions virtuelles aériennes ou indirectes. Li> ul> p>


1 commentaires

generic_func_t compte pour les problèmes potentiels de compatibilité C-C ++ ABI discutés dans les autres commentaires?