Je répondais récemment à une question sur le comportement indéfini de faire p < q
en C quand p
et q
sont des pointeurs vers différents objets / tableaux. Cela m'a fait penser: C ++ a le même comportement (non défini) que <
dans ce cas, mais offre également le modèle de bibliothèque standard std::less
qui est garanti pour retourner la même chose que <
lorsque les pointeurs peuvent être comparés, et retourner un ordre cohérent lorsqu'ils ne le peuvent pas.
C offre-t-il quelque chose avec des fonctionnalités similaires qui permettrait de comparer en toute sécurité des pointeurs arbitraires (au même type)? J'ai essayé de parcourir la norme C11 et je n'ai rien trouvé, mais mon expérience en C est beaucoup plus petite qu'en C ++, donc j'aurais pu facilement manquer quelque chose.
la source
Réponses:
Sur les implémentations avec un modèle de mémoire plate (essentiellement tout), la conversion vers
uintptr_t
Just Work fonctionnera.(Mais voir Les comparaisons de pointeurs doivent-elles être signées ou non signées en 64 bits x86? Pour savoir si vous devez traiter les pointeurs comme signés ou non, y compris les problèmes de formation de pointeurs en dehors des objets qui est UB en C.)
Mais les systèmes avec des modèles de mémoire non plats existent, et de penser à leur sujet peuvent aider à expliquer la situation actuelle, comme C ++ ayant des spécifications pour
<
contrestd::less
.Une partie de l'intérêt des
<
pointeurs sur pour séparer les objets étant UB en C (ou du moins non spécifié dans certaines révisions C ++) est de permettre des machines étranges, y compris des modèles de mémoire non plats.Un exemple bien connu est le mode réel x86-16 où les pointeurs sont segment: offset, formant une adresse linéaire 20 bits via
(segment << 4) + offset
. La même adresse linéaire peut être représentée par plusieurs combinaisons seg: off différentes.C ++
std::less
sur des pointeurs sur des ISA étranges peut avoir besoin d'être coûteux , par exemple "normaliser" un segment: offset sur x86-16 pour avoir un offset <= 15. Cependant, il n'y a aucun moyen portable de l'implémenter. La manipulation requise pour normaliser unuintptr_t
(ou la représentation d'objet d'un objet pointeur) est spécifique à l'implémentation.Mais même sur des systèmes où C ++
std::less
doit être coûteux,<
cela ne doit pas l'être. Par exemple, en supposant un "grand" modèle de mémoire où un objet tient dans un segment,<
il suffit de comparer la partie décalée et de ne pas même déranger avec la partie de segment. (Les pointeurs à l'intérieur du même objet auront le même segment, et sinon c'est UB en C. C ++ 17 est devenu simplement "non spécifié", ce qui pourrait encore permettre de sauter la normalisation et de comparer simplement les décalages.) Cela suppose que tous les pointeurs de n'importe quelle partie d'un objet utilise toujours la mêmeseg
valeur, ne se normalisant jamais. C'est ce que vous attendez d'un ABI pour un modèle de mémoire "grand" par opposition à "énorme". (Voir la discussion dans les commentaires ).(Un tel modèle de mémoire peut avoir une taille d'objet maximale de 64 Ko par exemple, mais un espace d'adressage total maximal beaucoup plus grand qui a de la place pour de nombreux objets de cette taille maximale. ISO C permet aux implémentations d'avoir une limite de taille d'objet inférieure à la la valeur maximale (non signée)
size_t
peut représenter ,.SIZE_MAX
Par exemple, même sur les systèmes de modèle à mémoire plate, GNU C limite la taille maximale de l'objetPTRDIFF_MAX
pour que le calcul de la taille puisse ignorer le débordement signé.) Voir cette réponse et discussion dans les commentaires.Si vous voulez autoriser des objets plus grands qu'un segment, vous avez besoin d'un "énorme" modèle de mémoire qui doit se soucier de déborder la partie décalée d'un pointeur lorsque vous effectuez
p++
une boucle dans un tableau, ou lorsque vous effectuez une indexation / arithmétique de pointeur. Cela conduit à un code plus lent partout, mais cela signifierait probablement que celap < q
se produirait pour les pointeurs vers différents objets, car une implémentation ciblant un modèle de mémoire "énorme" choisirait normalement de garder tous les pointeurs normalisés tout le temps. Voir Quels sont les pointeurs proches, lointains et énormes? - certains compilateurs C réels pour le mode réel x86 avaient une option à compiler pour le modèle "énorme" où tous les pointeurs par défaut étaient "énormes" sauf indication contraire.La segmentation en mode réel x86 n'est pas le seul modèle de mémoire non plate possible , c'est simplement un exemple concret utile pour illustrer comment il est géré par les implémentations C / C ++. Dans la vie réelle, les implémentations ont étendu ISO C avec le concept de pointeurs
far
vs.near
, permettant aux programmeurs de choisir quand ils peuvent s'en tirer en stockant / passant simplement la partie offset 16 bits, par rapport à certains segments de données courants.Mais une implémentation ISO C pure devrait choisir entre un petit modèle de mémoire (tout sauf le code dans le même 64 Ko avec des pointeurs 16 bits) ou grand ou énorme avec tous les pointeurs étant 32 bits. Certaines boucles pouvaient être optimisées en incrémentant uniquement la partie décalée, mais les objets pointeurs ne pouvaient pas être optimisés pour être plus petits.
Si vous saviez quelle était la manipulation magique pour une implémentation donnée, vous pourriez l'implémenter en C pur . Le problème est que différents systèmes utilisent un adressage différent et les détails ne sont paramétrés par aucune macro portable.
Ou peut-être pas: cela peut impliquer de rechercher quelque chose à partir d'une table de segments spéciale ou quelque chose, par exemple comme le mode protégé x86 au lieu du mode réel où la partie segment de l'adresse est un index, pas une valeur à déplacer à gauche. Vous pouvez configurer des segments qui se chevauchent partiellement en mode protégé, et les parties de sélecteur de segment des adresses ne seraient même pas nécessairement ordonnées dans le même ordre que les adresses de base de segment correspondantes. Obtenir une adresse linéaire à partir d'un pointeur seg: off en mode protégé x86 peut impliquer un appel système, si le GDT et / ou le LDT ne sont pas mappés en pages lisibles dans votre processus.
(Bien sûr, les systèmes d'exploitation traditionnels pour x86 utilisent un modèle de mémoire plate, la base de segment est donc toujours 0 (sauf pour le stockage local par thread utilisant
fs
ougs
segments), et seule la partie "offset" 32 bits ou 64 bits est utilisée comme pointeur .)Vous pouvez ajouter manuellement du code pour diverses plates-formes spécifiques, par exemple supposer par défaut plat, ou
#ifdef
quelque chose pour détecter le mode réel x86 et le diviseruintptr_t
en moitiés 16 bits pourseg -= off>>4; off &= 0xf;
ensuite combiner ces parties en un nombre 32 bits.la source
p < q
est UB en C s'ils pointent vers des objets différents, n'est-ce pas? Je le saisp - q
.seg
valeur de cet objet et un décalage qui est> = le décalage dans le segment où cet objet commence. C oblige UB à faire beaucoup de choses entre des pointeurs vers différents objets, y compris des choses commetmp = a-b
et ensuiteb[tmp]
à accédera[0]
. Cette discussion sur l'alias de pointeur segmenté est un bon exemple de la raison pour laquelle ce choix de conception est logique.J'ai une fois essayé de trouver un moyen de contourner cela et j'ai trouvé une solution qui fonctionne pour les objets qui se chevauchent et dans la plupart des autres cas, en supposant que le compilateur fait la chose "habituelle".
Vous pouvez d'abord implémenter la suggestion dans Comment implémenter memmove en C standard sans copie intermédiaire? puis si cela ne fonctionne pas, transtypez en
uintptr
(un type d'encapsuleur pouruintptr_t
ouunsigned long long
selon qu'iluintptr_t
est disponible) et obtenez un résultat précis très probable (bien que cela n'aurait probablement pas d'importance de toute façon):la source
Non
Considérons d'abord les pointeurs d'objet . Les pointeurs de fonction apportent un tout autre ensemble de préoccupations.
2 pointeurs
p1, p2
peuvent avoir des encodages différents et pointer vers la même adresse doncp1 == p2
même si cememcmp(&p1, &p2, sizeof p1)
n'est pas 0. De telles architectures sont rares.Pourtant, la conversion de ces pointeurs vers
uintptr_t
ne nécessite pas le même résultat entier menant à(uintptr_t)p1 != (uinptr_t)p2
.(uintptr_t)p1 < (uinptr_t)p2
lui-même est un code bien légal, peut ne pas fournir les fonctionnalités espérées.Si le code a vraiment besoin de comparer des pointeurs non liés, créez une fonction d'assistance
less(const void *p1, const void *p2)
et exécutez-y du code spécifique à la plate-forme.Peut-être:
la source