web-dev-qa-db-fra.com

Pourquoi les pointeurs de fonction et les pointeurs de données sont-ils incompatibles en C / C ++?

J'ai lu que la conversion d'un pointeur de fonction en un pointeur de données et vice versa fonctionne sur la plupart des plates-formes mais n'est pas garantie de fonctionner. pourquoi est-ce le cas? Les deux ne devraient-ils pas être simplement des adresses dans la mémoire principale et donc être compatibles?

127
gexicide

Une architecture n'a pas besoin de stocker du code et des données dans la même mémoire. Avec une architecture Harvard, le code et les données sont stockés dans une mémoire complètement différente. La plupart des architectures sont des architectures Von Neumann avec du code et des données dans la même mémoire, mais C ne se limite pas à certains types d'architectures si possible.

169
Dirk Holsopple

Certains ordinateurs ont (avaient) des espaces d'adressage séparés pour le code et les données. Sur un tel matériel, cela ne fonctionne tout simplement pas.

Le langage est conçu non seulement pour les applications de bureau actuelles, mais pour lui permettre d'être implémenté sur un large éventail de matériel.


Il semble que le comité du langage C n'ait jamais voulu void* pour être un pointeur sur la fonction, ils voulaient juste un pointeur générique sur les objets.

La justification C99 dit:

6.3.2.3 Pointeurs
C a maintenant été implémenté sur un large éventail d'architectures. Alors que certaines de ces architectures comportent des pointeurs uniformes qui sont de la taille d'un type entier, le code portable au maximum ne peut pas supposer une correspondance nécessaire entre différents types de pointeurs et les types entiers. Sur certaines implémentations, les pointeurs peuvent même être plus larges que n'importe quel type entier.

L'utilisation de void* ("Pointeur vers void") en tant que type de pointeur d'objet générique est une invention du comité C89. L'adoption de ce type a été stimulée par le désir de spécifier des arguments de prototype de fonction qui convertissent silencieusement des pointeurs arbitraires (comme dans fread) ou se plaignent si le type d'argument ne correspond pas exactement (comme dans strcmp) . Rien n'est dit sur les pointeurs vers les fonctions, qui peuvent être sans commune mesure avec les pointeurs d'objets et/ou les entiers.

Remarque Rien n'est dit sur les pointeurs vers les fonctions dans le dernier paragraphe. Ils pourraient être différents des autres pointeurs, et le comité en est conscient.

37
Bo Persson

Pour ceux qui se souviennent de MS-DOS, Windows 3.1 et plus, la réponse est assez simple. Tous ces éléments prenaient en charge plusieurs modèles de mémoire différents, avec différentes combinaisons de caractéristiques pour les pointeurs de code et de données.

Ainsi, par exemple pour le modèle Compact (petit code, grandes données):

sizeof(void *) > sizeof(void(*)())

et inversement dans le modèle Medium (gros code, petites données):

sizeof(void *) < sizeof(void(*)())

Dans ce cas, vous n'aviez pas de stockage séparé pour le code et la date, mais vous ne pouviez toujours pas convertir entre les deux pointeurs (à moins d'utiliser des modificateurs __near et __far non standard).

De plus, il n'y a aucune garantie que même si les pointeurs sont de la même taille, ils pointent vers la même chose - dans le modèle de mémoire DOS Small, le code et les données sont utilisés à proximité des pointeurs, mais ils pointent vers des segments différents. Ainsi, la conversion d'un pointeur de fonction en un pointeur de données ne vous donnerait pas un pointeur ayant une quelconque relation avec la fonction, et donc il n'y avait aucune utilité pour une telle conversion.

30
Tomek

Les pointeurs à annuler sont censés pouvoir accueillir un pointeur vers n'importe quel type de données - mais pas nécessairement un pointeur vers une fonction. Certains systèmes ont des exigences différentes pour les pointeurs vers les fonctions que les pointeurs vers les données (par exemple, il existe des DSP avec un adressage différent pour les données par rapport au code, le modèle moyen sur MS-DOS utilise des pointeurs 32 bits pour le code mais seulement des pointeurs 16 bits pour les données) .

23
Jerry Coffin

En plus de ce qui est déjà dit ici, il est intéressant de regarder POSIX dlsym() :

La norme ISO C n'exige pas que les pointeurs vers les fonctions puissent être déplacés d'avant en arrière vers les pointeurs vers les données. En effet, la norme ISO C n'exige pas qu'un objet de type void * puisse contenir un pointeur sur une fonction. Cependant, les implémentations prenant en charge l'extension XSI nécessitent qu'un objet de type void * puisse contenir un pointeur sur une fonction. Le résultat de la conversion d'un pointeur vers une fonction en un pointeur vers un autre type de données (sauf void *) n'est pas encore défini, cependant. Notez que les compilateurs conformes à la norme ISO C sont requis pour générer un avertissement si une conversion d'un pointeur void * vers un pointeur de fonction est tentée comme dans:

 fptr = (int (*)(int))dlsym(handle, "my_function");

En raison du problème noté ici, une future version peut soit ajouter une nouvelle fonction pour renvoyer des pointeurs de fonction, soit l'interface actuelle peut être déconseillée au profit de deux nouvelles fonctions: l'une qui renvoie des pointeurs de données et l'autre qui renvoie des pointeurs de fonction.

12
Maxim Egorushkin

C++ 11 a une solution à l'inadéquation de longue date entre C/C++ et POSIX en ce qui concerne dlsym(). On peut utiliser reinterpret_cast pour convertir un pointeur de fonction vers/depuis un pointeur de données tant que l'implémentation prend en charge cette fonctionnalité.

De la norme, 5.2.10 par. 8, "la conversion d'un pointeur de fonction en un type de pointeur d'objet ou vice versa est prise en charge sous condition." 1.3.5 définit le "support conditionnel" comme une "construction de programme qu'une implémentation n'est pas requise pour supporter".

9
David Hammen

Selon l'architecture cible, le code et les données peuvent être stockés dans des zones de mémoire fondamentalement incompatibles et physiquement distinctes.

7
Graham Borland

undefined ne signifie pas nécessairement non autorisé, cela peut signifier que l'implémenteur du compilateur a plus de liberté pour le faire comme il le souhaite.

Par exemple, cela peut ne pas être possible sur certaines architectures - undefined leur permet d'avoir toujours une bibliothèque 'C' conforme même si vous ne pouvez pas le faire.

5
Martin Beckett

Ils peuvent être de types différents avec des besoins d'espace différents. L'affectation à un peut trancher irréversiblement la valeur du pointeur de sorte que l'attribution en retour entraîne quelque chose de différent.

Je pense qu'ils peuvent être de types différents car la norme ne veut pas limiter les implémentations possibles qui économisent de l'espace quand elles ne sont pas nécessaires ou lorsque la taille peut obliger le CPU à faire des conneries supplémentaires pour l'utiliser, etc ...

5
Edward Strange

Une autre solution:

En supposant que POSIX garantit que la fonction et les pointeurs de données auront la même taille et la même représentation (je ne trouve pas le texte pour cela, mais l'exemple OP cité suggère qu'ils au moins destiné pour faire cette exigence), ce qui suit devrait marcher:

double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);

Cela évite de violer les règles d'alias en passant par le char [] représentation, qui est autorisée à alias tous les types.

Encore une autre approche:

union {
    double (*fptr)(double);
    void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;

Mais je recommanderais l'approche memcpy si vous voulez un C. 100% correct.

4
R..

La seule solution vraiment portable est de ne pas utiliser dlsym pour les fonctions, et d'utiliser à la place dlsym pour obtenir un pointeur sur les données qui contiennent des pointeurs de fonction. Par exemple, dans votre bibliothèque:

struct module foo_module = {
    .create = create_func,
    .destroy = destroy_func,
    .write = write_func,
    /* ... */
};

puis dans votre candidature:

struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */

Soit dit en passant, il s'agit de toute façon d'une bonne pratique de conception, qui facilite la prise en charge du chargement dynamique via dlopen et de la liaison statique de tous les modules sur les systèmes qui ne prennent pas en charge la liaison dynamique, ou lorsque l'intégrateur utilisateur/système ne veut pas pour utiliser la liaison dynamique.

2
R..

Sur la plupart des architectures, les pointeurs vers tous les types de données normaux ont la même représentation, donc la conversion entre les types de pointeurs de données est un no-op.

Cependant, il est concevable que les pointeurs de fonction puissent nécessiter une représentation différente, peut-être qu'ils sont plus grands que les autres pointeurs. Si void * pouvait contenir des pointeurs de fonction, cela signifierait que la représentation de void * devrait être de plus grande taille. Et tous les transtypages de pointeurs de données vers/depuis void * devraient effectuer cette copie supplémentaire.

Comme quelqu'un l'a mentionné, si vous en avez besoin, vous pouvez l'obtenir en utilisant un syndicat. Mais la plupart des utilisations de void * sont uniquement pour les données, il serait donc coûteux d'augmenter toute leur utilisation de la mémoire au cas où un pointeur de fonction devrait être stocké.

2
Barmar

Un exemple moderne où les pointeurs de fonction peuvent différer en taille des pointeurs de données: Pointeurs de fonction membres de classe C++

Citation directe de https://blogs.msdn.Microsoft.com/oldnewthing/20040209-00/?p=40713/

class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2 { int d; void DerivedMethod(); };

Il existe maintenant deux pointeurs this possibles.

Un pointeur vers une fonction membre de Base1 peut être utilisé comme pointeur vers une fonction membre de Derived, car ils utilisent tous les deux le même pointeur this. Mais un pointeur vers une fonction membre de Base2 ne peut pas être utilisé tel quel comme pointeur vers une fonction membre de Derived, car le pointeur this doit être ajusté.

Il existe de nombreuses façons de résoudre ce problème. Voici comment le compilateur Visual Studio décide de le gérer:

Un pointeur sur une fonction membre d'une classe héritée par multiplication est vraiment une structure.

[Address of function]
[Adjustor]

La taille d'une fonction pointeur-sur-membre d'une classe qui utilise l'héritage multiple est la taille d'un pointeur plus la taille d'un size_t.

tl; dr: lors de l'utilisation de l'héritage multiple, un pointeur vers une fonction membre peut (en fonction du compilateur, de la version, de l'architecture, etc.) être réellement stocké en tant que

struct { 
    void * func;
    size_t offset;
}

qui est évidemment plus grand qu'un void *.

1
Andrew Sun