Perché questa implementazione di offsetof () funziona?

In ANSI C, offsetof è definito come di seguito.

#define offsetof(st, m) \ ((size_t) ( (char *)&((st *)(0))->m - (char *)0 )) 

Perché questo non genera un errore di segmentazione dal momento che stiamo dereferenziando un puntatore NULL? O si tratta di una sorta di hack del compilatore in cui vede che viene eliminato solo l’indirizzo dell’offset, quindi calcola staticamente l’indirizzo senza effettivamente dereferenziarlo? Inoltre questo codice è portatile?

In nessun punto del codice precedente c’è qualcosa di dereferenziato. Un dereferenziamento si verifica quando * o -> viene utilizzato su un valore di indirizzo per trovare il valore di riferimento. L’unico uso di * sopra è in una dichiarazione di tipo ai fini della fusione.

L’operatore -> è usato sopra ma non è usato per accedere al valore. Invece è usato per prendere l’indirizzo del valore. Ecco un esempio di codice non-macro che dovrebbe renderlo un po ‘più chiaro

 SomeType *pSomeType = GetTheValue(); int* pMember = &(pSomeType->SomeIntMember); 

La seconda riga in realtà non causa un dereferenziamento (dipende dall’implementazione). Restituisce semplicemente l’indirizzo di SomeIntMember all’interno del valore pSomeType .

Quello che vedi è un sacco di casting tra tipi arbitrari e puntatori di caratteri. Il motivo del char è che è uno degli unici tipi (forse il solo) nello standard C89 che ha una dimensione esplicita. La dimensione è 1. Assicurandosi che la dimensione sia una, il codice sopra può fare la magia malvagia di calcolare l’offset reale del valore.

In ANSI C, offsetof NON è definito in questo modo. Uno dei motivi per cui non è definito in questo modo è che alcuni ambienti lanciano eccezioni a puntatori nulli o si bloccano in altri modi. Quindi, ANSI C lascia l’implementazione di offsetof( ) aperta ai compilatori di compilatori.

Il codice mostrato sopra è tipico per i compilatori / ambienti che non controllano triggersmente i puntatori NULL, ma falliscono solo quando i byte vengono letti da un puntatore NULL.

Anche se questa è una tipica implementazione di offsetof , non è richiesta dallo standard, che dice semplicemente:

I seguenti tipi e macro sono definiti nell’intestazione standard […]

offsetof( type , member-designator )

che si espande a un’espressione costante intera che ha tipo size_t , il cui valore è l’offset in byte, al membro della struttura (designato da member-designator ), dall’inizio della sua struttura (designata dal type ). Il tipo e la designazione del membro devono essere tali da dare

type static t;

quindi l’espressione &(t. member-designator ) valuta una costante di indirizzo. (Se il membro specificato è un campo di bit, il comportamento non è definito).

Leggi “La libreria C standard” di PJ Plauger per una discussione su di esso e sugli altri elementi in che sono tutte funzioni di confine che potrebbero (dovrebbe?) Essere nella lingua corretta e che potrebbe richiedere un supporto speciale per il compilatore .

È solo di interesse storico, ma ho usato un primo compilatore C ANSI su 386 / IX (vedi, ti ho detto di interesse storico, circa 1990) che si è bloccato su quella versione di offsetof ma ha funzionato quando l’ho rivisto per:

 #define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024)) 

Questo era un bug del compilatore, non ultimo perché l’intestazione era distribuita con il compilatore e non funzionava.

Per rispondere all’ultima parte della domanda, il codice non è portatile.

Il risultato della sottrazione di due puntatori è definito e portabile solo se i due puntatori puntano a oggetti nello stesso array o puntano a uno dopo l’ultimo object dell’array (7.6.2 Operatori additivi, H & S Fifth Edition)

Non segfault perché non stai dereferenziandolo. L’indirizzo del puntatore viene utilizzato come un numero che viene sottratto da un altro numero, non utilizzato per indirizzare le operazioni di memoria.

Calcola l’offset del membro m rispetto all’indirizzo iniziale della rappresentazione di un object di tipo st .

((st *)(0)) riferisce a un puntatore NULL di tipo st * . &((st *)(0))->m riferisce all’indirizzo del membro m in questo object. Poiché l’indirizzo iniziale di questo object è 0 (NULL) , l’indirizzo del membro m è esattamente l’offset.

char * conversione char * e la differenza calcola l’offset in byte. Secondo le operazioni del puntatore, quando si fa una differenza tra due puntatori di tipo T * , il risultato è il numero di oggetti di tipo T rappresentati tra i due indirizzi contenuti dagli operandi.

Elenco 1: un insieme rappresentativo di definizioni di macro offsetof()

 // Keil 8051 compiler #define offsetof(s,m) (size_t)&(((s *)0)->m) // Microsoft x86 compiler (version 7) #define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m) // Diab Coldfire compiler #define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0)) typedef struct { int i; float f; char c; } SFOO; int main(void) { printf("Offset of 'f' is %zu\n", offsetof(SFOO, f)); } 

I vari operatori all’interno della macro vengono valutati in un ordine tale da eseguire i seguenti passaggi:

  1. ((s *)0) prende il numero intero zero e lo lancia come puntatore a s .
  2. ((s *)0)->m dereferenze che puntano a puntare al membro della struttura m .
  3. &(((s *)0)->m) calcola l’indirizzo di m .
  4. (size_t)&(((s *)0)->m) trasmette il risultato a un tipo di dati appropriato.

Per definizione, la struttura stessa risiede all’indirizzo 0. Ne consegue che l’indirizzo del campo puntato a (Passo 3 sopra) deve essere l’offset, in byte, dall’inizio della struttura.