C a-t-il un équivalent de std :: less de C ++?

26

Je répondais récemment à une question sur le comportement indéfini de faire p < qen C quand pet qsont 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::lessqui 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.

Angew n'est plus fier de SO
la source
1
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew

Réponses:

20

Sur les implémentations avec un modèle de mémoire plate (essentiellement tout), la conversion vers uintptr_tJust 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 <contre std::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::lesssur 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 un uintptr_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::lessdoit ê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ême segvaleur, 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_tpeut représenter ,. SIZE_MAXPar exemple, même sur les systèmes de modèle à mémoire plate, GNU C limite la taille maximale de l'objet PTRDIFF_MAXpour 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 cela p < qse 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 farvs. 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 fsou gssegments), 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 #ifdefquelque chose pour détecter le mode réel x86 et le diviser uintptr_ten moitiés 16 bits pour seg -= off>>4; off &= 0xf;ensuite combiner ces parties en un nombre 32 bits.

Peter Cordes
la source
Pourquoi serait-il UB si le segment n'est pas égal?
Acorn
@Acorn: Signifie que c'est l'inverse; fixé. les pointeurs vers le même objet auront le même segment, sinon UB.
Peter Cordes
Mais pourquoi pensez-vous que c'est UB en tout cas? (logique inversée ou non, en fait je n'ai pas remarqué non plus)
Acorn
p < qest UB en C s'ils pointent vers des objets différents, n'est-ce pas? Je le sais p - q.
Peter Cordes
1
@Acorn: Quoi qu'il en soit, je ne vois pas de mécanisme qui générerait des alias (segment différent: désactivé, même adresse linéaire) dans un programme sans UB. Ce n'est donc pas comme si le compilateur devait faire tout son possible pour éviter cela; chaque accès à un objet utilise la segvaleur 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 comme tmp = a-bet ensuite b[tmp]à accéder a[0]. Cette discussion sur l'alias de pointeur segmenté est un bon exemple de la raison pour laquelle ce choix de conception est logique.
Peter Cordes
17

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 pour uintptr_tou unsigned long longselon qu'il uintptr_test disponible) et obtenez un résultat précis très probable (bien que cela n'aurait probablement pas d'importance de toute façon):

#include <stdint.h>
#ifndef UINTPTR_MAX
typedef unsigned long long uintptr;
#else
typedef uintptr_t uintptr;
#endif

int pcmp(const void *p1, const void *p2, size_t len)
{
    const unsigned char *s1 = p1;
    const unsigned char *s2 = p2;
    size_t l;

    /* Check for overlap */
    for( l = 0; l < len; l++ )
    {
        if( s1 + l == s2 || s1 + l == s2 + len - 1 )
        {
            /* The two objects overlap, so we're allowed to
               use comparison operators. */
            if(s1 > s2)
                return 1;
            else if (s1 < s2)
                return -1;
            else
                return 0;
        }
    }

    /* No overlap so the result probably won't really matter.
       Cast the result to `uintptr` and hope the compiler
       does the "usual" thing */
    if((uintptr)s1 > (uintptr)s2)
        return 1;
    else if ((uintptr)s1 < (uintptr)s2)
        return -1;
    else
        return 0;
}
SS Anne
la source
5

Est-ce que C offre quelque chose avec des fonctionnalités similaires qui permettraient de comparer en toute sécurité des pointeurs arbitraires.

Non


Considérons d'abord les pointeurs d'objet . Les pointeurs de fonction apportent un tout autre ensemble de préoccupations.

2 pointeurs p1, p2peuvent avoir des encodages différents et pointer vers la même adresse donc p1 == p2même si ce memcmp(&p1, &p2, sizeof p1)n'est pas 0. De telles architectures sont rares.

Pourtant, la conversion de ces pointeurs vers uintptr_tne 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:

// return -1,0,1 for <,==,> 
int ptrcmp(const void *c1, const void *c1) {
  // Equivalence test works on all platforms
  if (c1 == c2) {
    return 0;
  }
  // At this point, we know pointers are not equivalent.
  #ifdef UINTPTR_MAX
    uintptr_t u1 = (uintptr_t)c1;
    uintptr_t u2 = (uintptr_t)c2;
    // Below code "works" in that the computation is legal,
    //   but does it function as desired?
    // Likely, but strange systems lurk out in the wild. 
    // Check implementation before using
    #if tbd
      return (u1 > u2) - (u1 < u2);
    #else
      #error TBD code
    #endif
  #else
    #error TBD code
  #endif 
}
chux - Réintégrer Monica
la source