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 comportement n'est pas défini dans les circonstances suivantes:
- Un pointeur est utilisé pour appeler une fonction dont le type n'est pas compatible avec le type pointé (6.3.2.3).
Le paragraphe 8 de la section 6.3.2.3 se lit comme suit:
Un pointeur vers une fonction d'un type peut être converti en un pointeur vers une fonction d'un autre type et inversement; le résultat doit être comparé égal au pointeur d'origine. Si un pointeur converti est utilisé pour appeler une fonction dont le type n'est pas compatible avec le type pointé, le comportement n'est pas défini.
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:
Pour que deux types de fonctions soient compatibles, les deux doivent spécifier des types de retour compatibles 127 .
De plus, les listes de types de paramètres, si les deux sont présentes, doivent correspondre au nombre de paramètres et à l'utilisation du terminateur d'ellipse; les paramètres correspondants doivent avoir des types compatibles. Si un type a une liste de types de paramètres 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 une liste d'identificateurs vide, la liste de paramètres ne doit pas avoir de terminateur d'ellipse et le type de chaque paramètre doit être compatible avec le type qui résulte de l'application des promotions d'argument par défaut. Si un type a une liste de types de paramètres et l'autre type est spécifié par une définition de fonction qui contient une liste d'identificateurs (éventuellement vide), les deux doivent s'accorder sur 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 de la compatibilité de type et d'un type composite, chaque paramètre déclaré avec le type de fonction ou de tableau est considéré comme ayant le type ajusté et chaque paramètre déclaré avec le type qualifié est considéré comme ayant la version non qualifiée de son type déclaré.)
127) Si les deux types de fonctions sont de «l'ancien style», les types de paramètres ne sont pas comparés.
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:
Pour que deux types de pointeurs soient compatibles, les deux doivent être qualifiés de la même manière et les deux doivent être des pointeurs vers des types compatibles.
Par conséquent, comme a void*
n'est pas compatible avec a struct my_struct*
, un pointeur de fonction de type void (*)(void*)
n'est pas compatible avec un pointeur de fonction de type void (*)(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:
- Conversion entre les pointeurs de fonction de différentes conventions d'appel. Vous allez gâcher la pile et, au mieux, vous écraser, au pire, réussir silencieusement avec un énorme trou de sécurité béant. Dans la programmation Windows, vous passez souvent des pointeurs de fonction. Win32 attend toutes les fonctions de rappel à utiliser la
stdcall
convention d' appel (que 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
), il en résultera un mauvais résultat.
- En C ++, transtyper entre les pointeurs de fonction de membre de classe et les pointeurs de fonction standard. Cela trébuche souvent les débutants C ++. Les fonctions membres de classe ont un
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:
- Conversion entre les pointeurs de fonction et les pointeurs réguliers (par exemple, conversion de a
void (*)(void)
en a void*
). 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.