¿Está agregando un puntero “char *” UB, cuando en realidad no apunta a una matriz de caracteres?

C ++ 17 ( expr.add / 4 ) dice:

Cuando una expresión que tiene un tipo integral se agrega o se resta de un puntero, el resultado tiene el tipo del operando del puntero. Si la expresión P apunta al elemento x [i] de un objeto de matriz x con n elementos, las expresiones P + J y J + P (donde J tiene el valor j) apuntan al elemento (posiblemente hipotético) x [i + j] si 0≤i + j≤n; de lo contrario, el comportamiento no está definido. Del mismo modo, la expresión P – J apunta al elemento (posiblemente hipotético) x [i-j] si 0≤i-j≤n; de lo contrario, el comportamiento no está definido.

struct Foo { float x, y, z; }; Foo f; char *p = reinterpret_cast(&f) + offsetof(Foo, z); // (*) *reinterpret_cast(p) = 42.0f; 

Tiene la línea marcada con (*) UB? reinterpret_cast(&f) no apunta a una matriz char, sino a una línea flotante, por lo que debe ser UB según el párrafo citado. Pero, si es UB, entonces la utilidad de offsetof sería limitada.

¿Es UB? ¿Si no, porque no?

La adición tiene la intención de ser válida, pero no creo que la norma se las arregle para decirlo con la suficiente claridad. Citando N4140 (aproximadamente C ++ 14):

3.9 Tipos [tipos básicos]

2 Para cualquier objeto (que no sea un subobjeto de clase base) de tipo trivialmente copiable T , ya sea que el objeto tenga o no un valor válido de tipo T , los bytes subyacentes (1.7) que componen el objeto se pueden copiar en una matriz de caracteres o unsigned char . 42 […]

42) Utilizando, por ejemplo, las funciones de la biblioteca (17.6.1.2) std::memcpy o std::memmove .

Dice “por ejemplo” porque std::memcpy y std::memmove no son las únicas formas en que los bytes subyacentes están destinados a ser copiados. También se supone que un bucle for simple que copia byte por byte manualmente también es válido.

Para que esto funcione, debe definirse la adición de punteros a los bytes brutos que componen un objeto, y la forma en que la definición de las expresiones funciona, la definición de la sum no puede depender de si el resultado de la sum se usará posteriormente para copiar los bytes. en una matriz.

Si eso significa que esos bytes forman una matriz o si esta es una excepción especial a las reglas generales para el operador + que de alguna manera se omite en la descripción del operador, no es claro para mí (sospecho que la primera), pero de cualquier manera haría la adición que está realizando en su código es válida.

Cualquier interpretación que no offsetof uso previsto de offsetof debe ser incorrecta:

 #include  #include  struct S { float a, b, c; }; const size_t idx_S[] = { offsetof(struct S, a), offsetof(struct S, b), offsetof(struct S, c), }; float read_S(struct S *sp, unsigned int idx) { assert(idx < 3); return *(float *)(((char *)sp) + idx_S[idx]); // intended to be valid } 

Sin embargo, cualquier interpretación que permita pasar del final de una matriz explícitamente declarada también debe ser incorrecta:

 #include  #include  struct S { float a[2]; float b[2]; }; static_assert(offsetof(struct S, b) == sizeof(float)*2, "padding between Sa and Sb -- should be impossible"); float read_S(struct S *sp, unsigned int idx) { assert(idx < 4); return sp->a[idx]; // undefined behavior if idx >= 2, // reading past end of array } 

Y ahora estamos en la mira de un dilema, porque la redacción en los estándares C y C ++, que pretendía rechazar el segundo caso, probablemente también desautoriza el primer caso.

Esto se conoce comúnmente como "¿qué es un objeto?" problema. Las personas, incluidos los miembros de los comités C y C ++, han estado discutiendo sobre este y otros temas relacionados desde la década de 1990, y se han realizado múltiples bashs para corregir la redacción, y hasta donde sé, ninguno ha tenido éxito (en el sentido de que todos el código "razonable" existente se considera definitivamente conforme y todas las optimizaciones "razonables" existentes todavía están permitidas).

(Nota: todo el código anterior se escribe como se escribirá en C para enfatizar que existe el mismo problema en ambos idiomas, y se puede encontrar sin el uso de construcciones de C ++).

Hasta donde yo sé, tu código es válido. Aliasing un objeto como una matriz char está explícitamente permitido según § 3.10 ¶ 10.8:

Si un progtwig intenta acceder al valor almacenado de un objeto a través de un glvalue distinto de uno de los siguientes tipos, el comportamiento no está definido:

  • […]
  • un char o un tipo de unsigned char .

La otra pregunta es si devolver el puntero char* a float* y asignarlo es válido. Como tu Foo es un tipo POD, está bien. Se le permite calcular la dirección de un miembro de POD (dado que el cálculo en sí no es UB) y luego acceder al miembro a través de esa dirección. No debe abusar de esto para, por ejemplo, obtener acceso a un miembro private de un objeto que no sea POD. Además, sería UB si, por ejemplo, emitir a int* o escribir en una dirección donde no existe objeto de tipo float . El razonamiento detrás de esto se puede encontrar en la sección citada anteriormente.

Sí, esto no está definido. Como has declarado en tu pregunta,

reinterpret_cast(&f) no apunta a una matriz de caracteres, sino a un elemento flotante , …

reinterpret_cast(&f) incluso no apunta a un char , por lo que incluso si la representación del objeto es una matriz char, el comportamiento aún no está definido.

Para offsetof , todavía puede usarlo como

 struct Foo { float x, y, z; }; Foo f; auto p = reinterpret_cast(&f) + offsetof(Foo, z); // ^^^^^^^^^^^^^^ *reinterpret_cast(p) = 42.0f;