C ha un equivalente di std :: less da C ++?


26

Recentemente ho risposto a una domanda sul comportamento indefinito del fare p < qin C quando pe qsono puntatori a diversi oggetti / matrici. Questo mi ha fatto pensare: C ++ ha lo stesso comportamento (non definito) di <questo caso, ma offre anche il modello di libreria standard std::lessche è garantito per restituire la stessa cosa di <quando i puntatori possono essere confrontati e restituire un ordinamento coerente quando non possono.

C offre qualcosa con funzionalità simili che consentirebbe di confrontare in modo sicuro puntatori arbitrari (con lo stesso tipo)? Ho provato a guardare attraverso lo standard C11 e non ho trovato nulla, ma la mia esperienza in C è ordini di grandezza inferiori a quelli in C ++, quindi avrei potuto facilmente perdere qualcosa.


1
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

Risposte:


20

Sulle implementazioni con un modello di memoria piatta (praticamente tutto), eseguire il casting su uintptr_tWill Just Work.

(Ma vedi I confronti dei puntatori devono essere firmati o non firmati in x86 a 64 bit? Per la discussione sull'opportunità o meno di considerare i puntatori come firmati, compresi i problemi di formazione di puntatori al di fuori degli oggetti che è UB in C.)

Ma i sistemi con modelli di memoria non piane esistono, e pensando a loro può aiutare a spiegare la situazione attuale, come il C ++ avere specifiche diverse per <contro std::less.


Parte del punto di < puntatore puntatori su per separare gli oggetti UB in C (o almeno non specificato in alcune revisioni C ++) è consentire macchine strane, compresi i modelli di memoria non piatti.

Un esempio ben noto è la modalità reale x86-16 in cui i puntatori sono segmenti: offset, formando un indirizzo lineare a 20 bit tramite (segment << 4) + offset. Lo stesso indirizzo lineare può essere rappresentato da più combinazioni seg: off diverse.

Il C ++ std::lesssui puntatori su strani ISA potrebbe dover essere costoso , ad esempio "normalizzare" un segmento: offset su x86-16 per avere offset <= 15. Tuttavia, non esiste un modo portatile per implementarlo. La manipolazione richiesta per normalizzare un uintptr_t(o la rappresentazione di un oggetto puntatore) è specifica dell'implementazione.

Ma anche sui sistemi in cui il C ++ std::lessdeve essere costoso, <non deve esserlo. Ad esempio, supponendo un modello di memoria "grande" in cui un oggetto si adatta all'interno di un segmento, <può semplicemente confrontare la parte di offset e nemmeno disturbarsi con la parte di segmento. (I puntatori all'interno dello stesso oggetto avranno lo stesso segmento, e altrimenti è UB in C. C ++ 17 modificato in "non specificato", il che potrebbe comunque consentire di saltare la normalizzazione e solo confrontare gli offset.) Questo presuppone che tutti i puntatori a qualsiasi parte di un oggetto usa sempre lo stesso segvalore, non normalizzando mai. Questo è ciò che ti aspetteresti da un ABI per un modello di memoria "grande" anziché "enorme". (Vedi discussione nei commenti ).

(Ad esempio un modello di memoria potrebbe avere una dimensione massima dell'oggetto di 64 kB, ad esempio, ma uno spazio di indirizzamento totale massimo molto più ampio che ha spazio per molti oggetti di dimensioni massime. ISO C consente alle implementazioni di avere un limite sulla dimensione dell'oggetto inferiore al il valore massimo (senza segno) size_tpuò rappresentare, ad SIZE_MAXesempio, anche nei sistemi con modello di memoria piatta, GNU C limita la dimensione massima dell'oggetto a PTRDIFF_MAXmodo che il calcolo della dimensione possa ignorare l'overflow con segno .) Vedi questa risposta e discussione nei commenti.

Se si desidera consentire oggetti più grandi di un segmento, è necessario un modello di memoria "enorme" che deve preoccuparsi di traboccare la parte di offset di un puntatore quando si esegue il p++ciclo in un array o quando si esegue l'aritmetica di indicizzazione / puntatore. Ciò porta a un codice più lento ovunque, ma probabilmente significherebbe che p < qfunzionerebbe per i puntatori a oggetti diversi, poiché un'implementazione destinata a un modello di memoria "enorme" normalmente sceglierebbe di mantenere tutti i puntatori sempre normalizzati. Vedi Cosa sono i puntatori vicini, lontani ed enormi? - alcuni compilatori C reali per la modalità reale x86 avevano un'opzione per compilare per il modello "enorme" in cui tutti i puntatori erano impostati su "enormi" se non diversamente indicato.

La segmentazione in modalità reale x86 non è l'unico modello di memoria non piatta possibile , è solo un utile esempio concreto per illustrare come è stata gestita dalle implementazioni C / C ++. Nella vita reale, le implementazioni hanno esteso ISO C con il concetto di farvs.near puntatori , consentendo ai programmatori di scegliere quando possono cavarsela semplicemente memorizzando / passando intorno alla parte di offset a 16 bit, rispetto ad alcuni segmenti di dati comuni.

Ma un'implementazione ISO C pura dovrebbe scegliere tra un piccolo modello di memoria (tutto tranne il codice nello stesso 64 kB con puntatori a 16 bit) o ​​grande o enorme con tutti i puntatori a 32 bit. Alcuni loop possono essere ottimizzati incrementando solo la parte offset, ma gli oggetti puntatore non possono essere ottimizzati per essere più piccoli.


Se sapessi quale fosse la manipolazione magica per una data implementazione, potresti implementarla in C puro . Il problema è che sistemi diversi usano indirizzi diversi e i dettagli non sono parametrizzati da nessuna macro portatile.

O forse no: potrebbe comportare la ricerca di qualcosa da una tabella di segmenti speciali o qualcosa del genere, ad esempio una modalità protetta x86 invece della modalità reale in cui la parte del segmento dell'indirizzo è un indice, non un valore da spostare a sinistra. È possibile impostare segmenti parzialmente sovrapposti in modalità protetta e le parti degli indirizzi del selettore di segmento non sarebbero necessariamente ordinate nello stesso ordine degli indirizzi di base del segmento corrispondente. Ottenere un indirizzo lineare da un puntatore seg: off in modalità protetta x86 potrebbe comportare una chiamata di sistema, se GDT e / o LDT non sono mappati in pagine leggibili nel processo.

(Ovviamente i sistemi operativi tradizionali per x86 usano un modello di memoria piatta, quindi la base del segmento è sempre 0 (tranne per l'archiviazione locale del thread fso i gssegmenti) e solo la parte "offset" a 32 bit o 64 bit viene utilizzata come puntatore .)

È possibile aggiungere manualmente il codice per varie piattaforme specifiche, ad esempio per impostazione predefinita assumere flat o #ifdefqualcosa per rilevare la modalità reale x86 e dividerla uintptr_tin metà a 16 bit per seg -= off>>4; off &= 0xf;poi ricomporre quelle parti in un numero a 32 bit.


Perché sarebbe UB se il segmento non è uguale?
Ghianda

@Acorn: intendevo dire il contrario; fisso. i puntatori nello stesso oggetto avranno lo stesso segmento, altrimenti UB.
Peter Cordes,

Ma perché pensi che sia UB in ogni caso? (logica invertita o no, in realtà non me ne sono nemmeno accorta)
Ghianda

p < qè UB in C se indicano oggetti diversi, no? Lo so p - q.
Peter Cordes,

1
@Acorn: Ad ogni modo, non vedo un meccanismo che generi alias (diversi seg: off, stesso indirizzo lineare) in un programma senza UB. Quindi non è come se il compilatore dovesse fare di tutto per evitarlo; ogni accesso a un oggetto utilizza il segvalore di quell'oggetto e un offset che è> = l'offset all'interno del segmento in cui inizia quell'oggetto. C rende UB in grado di fare qualsiasi cosa tra i puntatori a oggetti diversi, comprese cose cometmp = a-b e quindi b[tmp]accedere a[0]. Questa discussione sull'alias di puntatore segmentato è un buon esempio del perché questa scelta progettuale abbia senso.
Peter Cordes,

17

Una volta ho provato a trovare un modo per aggirare questo problema e ho trovato una soluzione che funziona per la sovrapposizione di oggetti e nella maggior parte degli altri casi supponendo che il compilatore faccia la cosa "normale".

Puoi prima implementare il suggerimento in Come implementare memmove in C standard senza una copia intermedia? e poi se non funziona cast uintptr(un tipo di wrapper per uno uintptr_to unsigned long longsecondo se uintptr_tè disponibile) e ottenere un risultato molto probabilmente accurato (anche se probabilmente non importerebbe comunque):

#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;
}

5

C offre qualcosa con funzionalità simili che consentirebbe di confrontare in modo sicuro puntatori arbitrari.

No


Per prima cosa consideriamo solo i puntatori agli oggetti . I puntatori a funzione mettono in discussione tutta un'altra serie di preoccupazioni.

2 puntatori p1, p2 possono avere codifiche diverse e puntare allo stesso indirizzo, quindi p1 == p2anche se memcmp(&p1, &p2, sizeof p1)non è 0. Tali architetture sono rare.

Eppure conversione di questi puntatori a uintptr_t non richiede lo stesso risultato intero che porta a (uintptr_t)p1 != (uinptr_t)p2.

(uintptr_t)p1 < (uinptr_t)p2 è di per sé un codice legale, potrebbe non fornire le sperate funzionalità.


Se il codice deve davvero confrontare i puntatori non correlati, formare una funzione di supporto less(const void *p1, const void *p2) ed eseguire lì un codice specifico per la piattaforma.

Forse:

// 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 
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.