Sulle implementazioni con un modello di memoria piatta (praticamente tutto), eseguire il casting su uintptr_t
Will 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::less
sui 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::less
deve 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 seg
valore, 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_t
può rappresentare, ad SIZE_MAX
esempio, anche nei sistemi con modello di memoria piatta, GNU C limita la dimensione massima dell'oggetto a PTRDIFF_MAX
modo 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 < q
funzionerebbe 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 far
vs.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 fs
o i gs
segmenti) 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 #ifdef
qualcosa per rilevare la modalità reale x86 e dividerla uintptr_t
in metà a 16 bit per seg -= off>>4; off &= 0xf;
poi ricomporre quelle parti in un numero a 32 bit.