Question

Disons que j'ai une fonction qui accepte un pointeur de fonction void (*)(void*) pour être utilisé comme un rappel:

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

Maintenant, si j'ai une fonction comme ceci:

void my_callback_function(struct my_struct* arg);

Puis-je faire en toute sécurité?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

J'ai regardé cette question et je l'ai regardé certaines normes C qui disent que vous pouvez lancer à « pointeurs de fonctions compatibles », mais je ne peux pas trouver une définition de « pointeur de fonction compatible » signifie.

Était-ce utile?

La solution

En ce qui concerne la norme C est concerné, si vous lancez un pointeur de fonction à un pointeur de fonction d'un type différent, puis appelez cela, il est un comportement non défini . Voir l'annexe J.2 (informative):

  

Le comportement est indéfini dans les circonstances suivantes:

     
      
  • Un pointeur est utilisé pour appeler une fonction dont le type est pas compatible avec la pointe à   Type (6.3.2.3).
  •   

Section 6.3.2.3, paragraphe 8 se lit comme suit:

  

Un pointeur vers une fonction d'un type peut être converti en un pointeur sur une fonction d'un autre   type et à nouveau; le résultat comparera égal au pointeur d'origine. Si un ancien   pointeur est utilisé pour appeler une fonction dont le type n'est pas compatible avec le type de pointe,   le comportement est indéfini.

En d'autres termes, vous pouvez jeter un pointeur de fonction à un type de pointeur de fonction différente, jeter à nouveau, et l'appeler, et les choses vont.

La définition de compatible est un peu compliquée. Il se trouve à la section 6.7.5.3, paragraphe 15:

  

Pour deux types de fonction soient compatibles, les deux doivent spécifier les types de retour compatibles 127 .

     

En outre, les listes de type de paramètre, si les deux sont présents, sont d'accord sur le nombre de   paramètres et à l'utilisation de la terminaison de l'ellipse; les paramètres correspondants doivent être munis   types compatibles. Si un type a une liste de type de paramètre et l'autre type est spécifié par un   déclarateur de fonction qui ne fait pas partie d'une définition de fonction et qui contient un vide   liste d'identifiants, la liste des paramètres ne doit pas avoir une terminaison de l'ellipse et le type de chaque   paramètre doit être compatible avec le type qui résulte de l'application de la   promotions argument par défaut. Si un type a une liste de type de paramètre et l'autre type est   spécifiée par la définition d'une fonction qui contient une liste d'identifiants (éventuellement vide), les deux doivent   d'accord dans le nombre de paramètres et le type de chaque paramètre prototype doit être   compatible avec le type qui résulte de l'application de l'argument par défaut   promotions au type de l'identifiant correspondant. (Dans la détermination du type   compatibilité et d'un type composite, chaque paramètre déclaré avec la fonction ou le tableau   type est considéré comme ayant le type ajusté et chaque paramètre déclaré de type qualifié   est considéré comme ayant la version non qualifiée de son type déclaré.)

     

127) Si les deux types de fonction sont « « old style » », types de paramètres ne sont pas comparés.

Les règles pour déterminer si deux types sont compatibles sont décrits dans la section 6.2.7, et je ne vais pas les citer ici, car ils sont assez longues, mais vous pouvez les lire sur la page projet de la norme C99 (PDF) .

La règle pertinente ici est à la section 6.7.5.1, paragraphe 2:

  

Pour deux types de pointeurs soient compatibles, les deux doivent être identiques qualifiés et les deux sont des pointeurs vers des types compatibles.

Par conséquent, étant donné qu'un href="https://stackoverflow.com/questions/11647220/are-void-pointer-and-pointer-to-some-structure-layout-compatible"> void* avec un struct my_struct*, un pointeur de fonction de type void (*)(void*) est pas compatible avec un pointeur de fonction de type void (*)(struct my_struct*), cette coulée de pointeurs de fonction est un comportement techniquement indéfini.

Dans la pratique, cependant, vous pouvez en toute sécurité sortir avec des pointeurs de fonction de coulée dans certains cas. Dans la convention d'appel x86, les arguments sont poussés sur la pile, et tous les pointeurs sont de la même taille (4 octets x86 ou 8 octets x86_64). Appeler un pointeur de fonction se résume à pousser les arguments sur la pile et de faire un saut indirect à la fonctionpointeur cible, et il n'y a évidemment aucune notion de types au niveau du code machine.

Les choses que vous certainement ne peut pas faire:

  • Cast entre les pointeurs de fonction des différentes conventions d'appel. Vous gâchera la pile et au mieux, accident, au pire, réussir en silence avec un énorme trou de sécurité béant. Dans la programmation Windows, vous passez souvent des pointeurs de fonction autour. Win32 attend toutes les fonctions de rappel à utiliser la convention d'appel de stdcall (qui les macros CALLBACK, PASCAL et WINAPI tout développer à). Si vous passez un pointeur de fonction qui utilise la convention d'appel standard C (cdecl), badness entraînera.
  • En C ++, entre les pointeurs de lancer la fonction de membre de classe et des pointeurs de fonction régulière. Intervient souvent les débutants C +. Les fonctions membres de classe ont un paramètre this caché, et si vous lancez une fonction de membre à une fonction régulière, il n'y a aucun objet this à utiliser, et encore, beaucoup badness résulteront.

Une autre mauvaise idée qui pourrait parfois fonctionner, mais est aussi un comportement non défini:

  • coulée entre des pointeurs de fonction et des pointeurs réguliers (par exemple une coulée void (*)(void) à un void*). pointeurs de fonction ne sont pas nécessairement la même taille que des pointeurs réguliers, étant donné que sur certaines architectures ils pourraient contenir des informations contextuelles supplémentaires. Cela fonctionnera probablement ok sur x86, mais rappelez-vous que c'est un comportement non défini.

Autres conseils

J'ai demandé à cette même question exacte au sujet du code dans GLib récemment. (GLib est une bibliothèque de base pour le projet GNOME et écrit en C.) On m'a dit que l'ensemble slots'n'signals cadre dépend.

Tout au long de ce code, il existe de nombreux cas de la coulée à partir du type (1) à (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

Il est courant de chaîne à travers des appels comme celui-ci:

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

Voyez vous-même ici à g_array_sort(): http: //git.gnome .org / browse / glib / arbre / glib / garray.c

Les réponses ci-dessus sont détaillées et probablement correcte - si vous êtes assis sur le comité des normes. Adam et Johannes mérite de leurs réponses bien documentées. Cependant, dans la nature, vous trouverez ce code fonctionne très bien. Controversé? Oui. Considérez ceci: GLib compile / travaux / essais sur un grand nombre de plates-formes (Linux / Solaris / Windows / OS X) avec une grande variété de compilateurs / linkers / chargeurs du noyau (GCC / Clang / MSVC). Normes être damnés, je suppose.

J'ai passé quelque temps à penser à ces réponses. Voici ma conclusion:

  1. Si vous écrivez une bibliothèque de rappel, cela pourrait être OK. Caveat emptor -. Utiliser à vos propres risques
  2. Sinon, ne le font pas.

Penser plus profond après avoir écrit cette réponse, je ne serais pas surpris si le code pour les compilateurs C utilise cette même astuce. Et depuis (plus / all?) Compilateurs C modernes sont amorcés, cela impliquerait l'affaire est en sécurité.

Une question plus importante à la recherche: Quelqu'un peut-il trouver une plate-forme / compilateur / éditeur de liens / chargeur où cette astuce ne pas travail? Les principaux points de brownie pour celui-là. Je parie qu'il ya des processeurs / systèmes embarqués qui ne l'aiment pas. Cependant, pour le calcul de bureau (et probablement mobile / tablette), cette astuce fonctionne probablement encore.

Le point est vraiment pas si vous le pouvez. La solution triviale est

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

Un bon compilateur ne générer du code pour my_callback_helper si elle est vraiment nécessaire, dans ce cas, vous seriez content que ce soit fait.

Vous avez un type de fonction compatible si le type de retour et les types de paramètres sont compatibles - essentiellement (il est plus compliqué en réalité :)). La compatibilité est le même que « même type » un peu plus laxiste pour permettre d'avoir différents types mais qui ont encore une certaine forme de dire « ces types sont presque les mêmes ». En C89, par exemple, deux struct étaient compatibles si elles étaient identiques mais juste leur nom était différent. C99 semble avoir changé. Je cite le document raison c (lecture fortement recommandé, btw!):

  

Structure, union ou déclarations de type de recensement en deux unités de traduction différents ne déclarent pas officiellement le même type, même si le texte de ces déclarations proviennent du même fichier include, étant donné que les unités de traduction sont eux-mêmes disjoints. La norme spécifie donc les règles de compatibilité supplémentaires pour ces types, de sorte que si deux de ces déclarations sont suffisamment semblables, ils sont compatibles.

Cela dit - oui strictement ce comportement est indéfini, parce que votre fonction do_stuff ou quelqu'un d'autre appelleront votre fonction avec un pointeur de fonction ayant void* en tant que paramètre, mais votre fonction a un paramètre incompatible. Mais néanmoins, je pense tous les compilateurs pour compiler et exécuter sans gémissant. Mais vous pouvez faire plus propre en ayant une autre fonction qui prend une void* (et l'enregistrement en tant que fonction de rappel) qui suffit d'appeler votre fonction réelle alors.

code C compile à l'instruction qui ne se soucient pas du tout sur les types de pointeur, il est tout à fait bon d'utiliser le code que vous mentionnez. Vous auriez des problèmes lors de l'exécution que vous souhaitez exécuter do_stuff avec votre fonction de rappel et pointeur vers quelque chose d'autre structure de ma_struct alors comme argument.

J'espère pouvoir le rendre plus clair en montrant ce qui ne fonctionnerait pas:

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

ou ...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

En gros, vous pouvez lancer des pointeurs sur ce que vous voulez, tant que les données continuent de donner un sens à l'exécution.

Si vous pensez à la fonction façon appelle travailler en C / C ++, ils poussent certains éléments sur la pile, sauter vers le nouvel emplacement de code, exécutez, puis pop la pile sur le retour. Si vos pointeurs de fonction décrivent les fonctions avec le même type de retour et le même nombre / taille des arguments, vous devriez être bien.

Ainsi, je pense que vous devriez être en mesure de le faire en toute sécurité.

pointeurs Void sont compatibles avec d'autres types de pointeur. Il est l'épine dorsale de la façon dont malloc et les fonctions mem (memcpy, memcmp) travail. Typiquement, dans C (plutôt que C ++) NULL est une macro définie comme ((void *)0).

Regardez 6.3.2.3 (1 °) en C99:

  

Un pointeur à vide peut être converti en ou à partir d'un pointeur vers un type incomplet ou objet

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top