¿Cómo se usa `offsetof` para acceder a un campo de una forma estándar conforme?

Supongamos que tengo una estructura y extraigo el desplazamiento a un miembro:

struct A { int x; }; size_t xoff = offsetof(A, x); 

¿cómo puedo, dado un puntero a la struct A extraer el miembro de una forma conforme con los estándares? Suponiendo, por supuesto, que tenemos una struct A* correcta struct A* y un desplazamiento correcto. Un bash sería hacer algo como:

 int getint(struct A* base, size_t off) { return *(int*)((char*)base + off); } 

Lo que probablemente funcionará, pero tenga en cuenta, por ejemplo, que la aritmética de punteros solo parece estar definida en el estándar si los punteros son punteros de la misma matriz (o uno más allá del final), esto no es así. Así que técnicamente esa construcción parece depender de un comportamiento indefinido.

Otro enfoque sería

 int getint(struct A* base, size_t off) { return *(int*)((uintptr_t)base + off); } 

lo que probablemente también funcionaría, pero tenga en cuenta que no se requiere que intptr_t exista y que yo sepa, arithmetics en intptr_t no necesita dar el resultado correcto (por ejemplo, recuerdo que algunas CPU tienen la capacidad de manejar direcciones alineadas sin byte lo que sugeriría que intptr_t aumenta en pasos de 8 para cada char en una matriz).

Parece que hay algo olvidado en el estándar (o algo que me he perdido).

De acuerdo con el Estándar C , 7.19 Definiciones comunes , párrafo 3, offsetof() se define como:

Las macros son

 NULL 

que se expande a una constante de puntero nula definida por la implementación; y

 offsetof(*type*, *member-designator*) 

que se expande a una expresión constante entera que tiene el tipo size_t , cuyo valor es el desplazamiento en bytes, al miembro de estructura (designado por miembro-designador ), desde el principio de su estructura (designado por tipo ).

Entonces, offsetoff() devuelve un desplazamiento en bytes .

Y 6.2.6.1 General , el párrafo 4 establece:

Los valores almacenados en objetos sin campo de bits de cualquier otro tipo de objeto consisten en n × CHAR_BIT bits, donde n es el tamaño de un objeto de ese tipo, en bytes.

Dado que CHAR_BIT se define como el número de bits en un char , un char es un byte .

Por lo tanto, esto es correcto, según el estándar:

 int getint(struct A* base, size_t off) { return *(int*)((char*)base + off); } 

Eso convierte la base a un char * y agrega bytes a la dirección. Si off es el resultado de offsetof(A, x); , la dirección resultante es la dirección de x dentro de la structure A que apunta la base .

Tu segundo ejemplo:

 int getint(struct A* base, size_t off) { return *(int*)((intptr_t)base + off); } 

depende del resultado de la adición del valor intptr_t firmado con el valor size_t sin firmar sin firmar.

La razón por la que el estándar (6.5.6) solo permite la aritmética de punteros para matrices, es que las estructuras pueden tener bytes de relleno para cumplir con los requisitos de alineación. Por lo tanto, hacer aritmética de punteros dentro de una estructura es de hecho un comportamiento formalmente indefinido.

En la práctica, funcionará siempre que sepas lo que estás haciendo. base + off no puede fallar, porque sabemos que hay datos válidos allí y que no están desalineados, dado que se accede correctamente.

Por lo tanto, (intptr_t)base + off es, de hecho, un código mucho mejor, ya que ya no hay aritmética de punteros, sino simplemente aritmética de enteros. Debido a que intptr_t es un entero, no es un puntero.

Como se señaló en un comentario, no se garantiza que este tipo exista, es opcional según 7.20.1.4/1. Supongo que para la máxima portabilidad, podría cambiar a otros tipos que se garantiza que existen, como intmax_t o ptrdiff_t . Sin embargo, es discutible si un comstackdor C99 / C11 sin soporte para intptr_t es útil.

(Hay un pequeño problema de tipo aquí, es decir, que intptr_t es un tipo firmado y no necesariamente compatible con size_t . Es posible que tenga problemas de promoción de tipo implícito. Es más seguro usar uintptr_t si es posible).

La siguiente pregunta es si *(int*)((intptr_t)base + off) es un comportamiento bien definido. La parte del estándar con respecto a las conversiones de punteros (6.3.2.3) dice que:

Cualquier tipo de puntero se puede convertir en un tipo entero. Excepto como se especificó previamente, el resultado está definido por la implementación. Si el resultado no se puede representar en el tipo entero, el comportamiento es indefinido. El resultado no necesita estar en el rango de valores de cualquier tipo de entero.

Para este caso específico, sabemos que tenemos un int alineado correctamente, por lo que está bien.

(Tampoco creo que se aplique ningún problema de alias de puntero. Al menos comstackr con gcc -O3 -fstrict-aliasing -Wstrict-aliasing=2 no se rompe el código).