Disons que j'ai une fonction qui accepte un void (*)(void*)
pointeur de fonction à utiliser comme rappel:
void do_stuff(void (*callback_fp)(void*), void* callback_arg);
Maintenant, si j'ai une fonction comme celle-ci:
void my_callback_function(struct my_struct* arg);
Puis-je faire cela en toute sécurité?
do_stuff((void (*)(void*)) &my_callback_function, NULL);
J'ai regardé cette question et j'ai examiné certaines normes C qui disent que vous pouvez convertir en «pointeurs de fonction compatibles», mais je ne trouve pas de définition de ce que signifie «pointeur de fonction compatible».
c
function-pointers
Mike Weller
la source
la source
void (*func)(void *)
signifie qu'ilfunc
s'agit d'un pointeur vers une fonction avec une signature de type telle quevoid foo(void *arg)
. Alors oui, tu as raison.Réponses:
En ce qui concerne la norme C, si vous transtypez un pointeur de fonction vers un pointeur de fonction d'un type différent et que vous l'appelez ensuite, il s'agit d' un comportement non défini . Voir l'annexe J.2 (informative):
Le paragraphe 8 de la section 6.3.2.3 se lit comme suit:
Donc, en d'autres termes, vous pouvez convertir un pointeur de fonction en un type de pointeur de fonction différent, le renvoyer à nouveau et l'appeler, et les choses fonctionneront.
La définition de compatible est quelque peu compliquée. Il se trouve dans la section 6.7.5.3, paragraphe 15:
Les règles pour déterminer si deux types sont compatibles sont décrites dans la section 6.2.7, et je ne les citerai pas ici car elles sont assez longues, mais vous pouvez les lire sur le projet de norme C99 (PDF) .
La règle pertinente ici est dans la section 6.7.5.1, paragraphe 2:
Par conséquent, comme a
void*
n'est pas compatible avec astruct my_struct*
, un pointeur de fonction de typevoid (*)(void*)
n'est pas compatible avec un pointeur de fonction de typevoid (*)(struct my_struct*)
, donc cette conversion de pointeurs de fonction est un comportement techniquement indéfini.Dans la pratique, cependant, vous pouvez vous en tirer en toute sécurité avec des pointeurs de fonction de diffusion dans certains cas. Dans la convention d'appel x86, les arguments sont poussés sur la pile et tous les pointeurs ont la même taille (4 octets en x86 ou 8 octets en x86_64). Appeler un pointeur de fonction revient à pousser les arguments sur la pile et à faire un saut indirect vers la cible du pointeur de fonction, et il n'y a évidemment pas de notion de types au niveau du code machine.
Ce que vous ne pouvez certainement pas faire:
stdcall
convention d' appel (que les macrosCALLBACK
,PASCAL
etWINAPI
tout développer à). Si vous passez un pointeur de fonction qui utilise la convention d'appel standard C (cdecl
), il en résultera un mauvais résultat.this
paramètre masqué , et si vous transtypez une fonction membre en fonction régulière, il n'y a pas d'this
objet à utiliser, et encore une fois, il en résultera beaucoup de mauvais.Une autre mauvaise idée qui peut parfois fonctionner mais qui est également un comportement indéfini:
void (*)(void)
en avoid*
). Les pointeurs de fonction ne sont pas nécessairement de la même taille que les pointeurs réguliers, car sur certaines architectures, ils peuvent contenir des informations contextuelles supplémentaires. Cela fonctionnera probablement bien sur x86, mais rappelez-vous que ce comportement n'est pas défini.la source
void*
qu'ils sont compatibles avec n'importe quel autre pointeur? Il ne devrait y avoir aucun problème à convertir astruct my_struct*
en avoid*
, en fait, vous ne devriez même pas avoir à lancer, le compilateur devrait simplement l'accepter. Par exemple, si vous passez astruct my_struct*
à une fonction qui prend avoid*
, aucune conversion n'est requise. Qu'est-ce que je manque ici qui les rend incompatibles?void*
que pour les types de pointeurs de fonction, voir la spécification .void *
n'est "compatible avec" que tout autre pointeur (non fonctionnel) de manière très précise (qui n'est pas liée à ce que le standard C signifie avec le mot "compatible" dans ce cas). C permetvoid *
à a d'être plus grand ou plus petit que astruct my_struct *
, ou d'avoir les bits dans un ordre différent ou inversé ou autre. Doncvoid f(void *)
etvoid f(struct my_struct *)
peut être incompatible avec ABI . C convertira les pointeurs eux-mêmes pour vous si nécessaire, mais il ne peut pas et parfois ne peut pas convertir une fonction pointée pour prendre un type d'argument éventuellement différent.J'ai récemment posé des questions sur ce même problème concernant du code dans GLib. (GLib est une bibliothèque principale pour le projet GNOME et écrite en C.) On m'a dit que tout le framework slots'n'signals en dépendait.
Tout au long du code, il existe de nombreuses instances de conversion du type (1) vers (2):
typedef int (*CompareFunc) (const void *a, const void *b)
typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
Il est courant de passer en chaîne avec 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 par vous-même ici sur
g_array_sort()
: http://git.gnome.org/browse/glib/tree/glib/garray.cLes réponses ci-dessus sont détaillées et probablement correctes - si vous faites partie du comité des normes. Adam et Johannes méritent d'être félicités pour leurs réponses bien documentées. Cependant, dans la nature, vous trouverez que ce code fonctionne très bien. Controversé? Oui. Considérez ceci: GLib compile / travaille / teste sur un grand nombre de plates-formes (Linux / Solaris / Windows / OS X) avec une grande variété de compilateurs / linkers / chargeurs de noyau (GCC / CLang / MSVC). Les normes soient damnées, je suppose.
J'ai passé du temps à réfléchir à ces réponses. Voici ma conclusion:
En réfléchissant plus profondément après avoir écrit cette réponse, je ne serais pas surpris si le code des compilateurs C utilise cette même astuce. Et puisque (la plupart / tous?) Les compilateurs C modernes sont bootstrapés, cela impliquerait que l'astuce est sûre.
Une question plus importante à rechercher: quelqu'un peut-il trouver une plate-forme / compilateur / éditeur de liens / chargeur où cette astuce ne fonctionne pas ? Points de brownie majeurs pour celui-là. Je parie que certains processeurs / systèmes embarqués n'aiment pas ça. Cependant, pour l'informatique de bureau (et probablement mobile / tablette), cette astuce fonctionne probablement toujours.
la source
Le point n'est vraiment pas de savoir si vous 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érera du code pour my_callback_helper que si c'est vraiment nécessaire, auquel cas vous seriez heureux de l'avoir fait.
la source
my_callback_helper
, à moins qu'il ne soit toujours en ligne. Ce n'est certainement pas nécessaire, car la seule chose que cela a tendance à faire estjmp my_callback_function
. Le compilateur veut probablement s'assurer que les adresses des fonctions sont différentes, mais malheureusement, il le fait même lorsque la fonction est marquée avec C99inline
(c'est-à-dire "ne se soucie pas de l'adresse").void *
peut même être de taille différente de astruct *
(je pense que c'est faux, car autrementmalloc
serait cassé, mais ce commentaire a 5 votes positifs, donc je lui donne un peu de crédit. Si @mtraceur a raison, la solution que vous avez écrite ne serait pas correcte.void*
doit encore fonctionner. En bref, ilvoid*
peut y avoir plus de bits, mais si vous lancez unstruct*
survoid*
ces bits supplémentaires, il peut s'agir de zéros et le renvoi peut simplement annuler ces zéros à nouveau.void *
pouvait (en théorie) être si différent d'unstruct *
. J'implémente une vtable en C et j'utilise unthis
pointeur C ++ - ish comme premier argument des fonctions virtuelles. De toute évidence,this
doit être un pointeur vers la structure "courante" (dérivée). Ainsi, les fonctions virtuelles ont besoin de différents prototypes en fonction de la structure dans laquelle elles sont implémentées. Je pensais que l'utilisation d'unvoid *this
argument résoudrait tout, mais maintenant j'ai appris que son comportement n'est pas défini ...Vous avez un type de fonction compatible si le type de retour et les types de paramètres sont compatibles - en gros (c'est plus compliqué en réalité :)). La compatibilité est la même que celle du "même type", juste plus laxiste pour permettre d'avoir des types différents tout en ayant une certaine forme de dire "ces types sont presque les mêmes". Dans C89, par exemple, deux structures étaient compatibles si elles étaient par ailleurs identiques mais leur nom était différent. C99 semble avoir changé cela. Citant le document de justification c (lecture hautement recommandée, d'ailleurs!):
Cela dit - oui, c'est strictement un comportement indéfini, car votre fonction do_stuff ou quelqu'un d'autre appellera votre fonction avec un pointeur de fonction ayant
void*
comme paramètre, mais votre fonction a un paramètre incompatible. Mais néanmoins, je m'attends à ce que tous les compilateurs le compilent et l'exécutent sans gémir. Mais vous pouvez faire un nettoyage en ayant une autre fonction prenant unvoid*
(et en l'enregistrant comme fonction de rappel) qui appellera alors votre fonction réelle.la source
Comme le code C se compile en instructions qui ne se soucient pas du tout des types de pointeurs, il est tout à fait correct d'utiliser le code que vous mentionnez. Vous rencontriez des problèmes lorsque vous exécutiez do_stuff avec votre fonction de rappel et votre pointeur vers autre chose que la structure my_struct comme argument.
J'espère pouvoir clarifier les choses 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
Fondamentalement, vous pouvez lancer des pointeurs sur ce que vous voulez, tant que les données continuent à avoir un sens au moment de l'exécution.
la source
Si vous pensez à la façon dont les appels de fonction fonctionnent en C / C ++, ils poussent certains éléments sur la pile, sautent au nouvel emplacement de code, s'exécutent, puis font apparaître la pile au retour. Si vos pointeurs de fonction décrivent des fonctions avec le même type de retour et le même nombre / taille d'arguments, vous devriez être d'accord.
Ainsi, je pense que vous devriez pouvoir le faire en toute sécurité.
la source
struct
-pointers etvoid
-pointers ont des représentations de bits compatibles; ce n'est pas garanti que ce soit le casLes pointeurs vides sont compatibles avec d'autres types de pointeurs. C'est l'épine dorsale du fonctionnement de malloc et des fonctions mem (
memcpy
,memcmp
). En règle générale, en C (plutôt qu'en C ++)NULL
est une macro définie comme((void *)0)
.Regardez 6.3.2.3 (Item 1) dans C99:
la source