c++ - ¿Se agrega a un puntero "char*" UB, cuando en realidad no apunta a una matriz de caracteres?
language-lawyer (5)
C ++ 17 ( expr.add/4 ) dice:
Cuando una expresión que tiene un tipo integral se agrega o 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 es indefinido. Asimismo, la expresión P - J apunta al elemento (posiblemente hipotético) x [i − j] si 0≤i − j≤n; de lo contrario, el comportamiento es indefinido.
struct Foo {
float x, y, z;
};
Foo f;
char *p = reinterpret_cast<char*>(&f) + offsetof(Foo, z); // (*)
*reinterpret_cast<float*>(p) = 42.0f;
¿La línea está marcada con (*) UB?
reinterpret_cast<char*>(&f)
no apunta a una matriz de caracteres, sino a un flotante, por lo que debería UB de acuerdo con el párrafo citado.
Pero, si es UB, la utilidad de
offsetof
sería limitada.
¿Es UB? ¿Si no, porque no?
Cualquier interpretación que no
offsetof
uso previsto de
offsetof
debe ser incorrecta:
#include <assert.h>
#include <stddef.h>
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 el final de una matriz declarada explícitamente también debe ser incorrecta:
#include <assert.h>
#include <stddef.h>
struct S { float a[2]; float b[2]; };
static_assert(offsetof(struct S, b) == sizeof(float)*2,
"padding between S.a and S.b -- 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 el clavo de un dilema, porque la redacción de los estándares C y C ++, que tenía la intención de rechazar el segundo caso, probablemente también rechaza 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 tema y otros relacionados desde la década de 1990, y ha habido múltiples intentos de corregir la redacción, y que yo sepa, ninguno ha tenido éxito (en el sentido de que todo el código "razonable" existente se hace definitivamente conforme y todas las optimizaciones "razonables" existentes todavía están permitidas).
(Nota: todo el código anterior se escribe como se escribiría en C para enfatizar que existe el mismo problema en ambos lenguajes y se puede encontrar sin el uso de ninguna construcción de C ++).
La adición está destinada a ser válida, pero no creo que el estándar logre decirlo con 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
T
copiable trivialmente, ya sea que el objeto tenga o no un valor válido de tipoT
, los bytes subyacentes (1.7) que componen el objeto se pueden copiar en una matriz de caracteres o ununsigned char
. 42 [...]42) Al utilizar, por ejemplo, las funciones de biblioteca (17.6.1.2)
std::memcpy
ostd::memmove
.
Dice "por ejemplo" porque
std::memcpy
y
std::memmove
no son las únicas formas en que se pretende copiar los bytes subyacentes.
Se supone que también es válido un bucle
for
simple que copia byte a byte manualmente.
Para que eso funcione, la suma debe definirse para los punteros a los bytes sin procesar que componen un objeto, y la forma en que funciona la definición de expresiones, la definición de la adición no puede depender de si el resultado de la adición se utilizará posteriormente para copiar los bytes. en una matriz.
Si eso significa que esos bytes ya 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 me queda claro (sospecho que el primero), pero de cualquier manera La adición que está realizando en su código es válida.
Que yo sepa, su código es válido. Aliasing un objeto como una matriz de caracteres está explícitamente permitido según § 3.10 ¶ 10.8:
Si un programa intenta acceder al valor almacenado de un objeto a través de un valor gl diferente de uno de los siguientes tipos, el comportamiento no está definido:
- [...]
- un tipo de
unsigned char
char
ounsigned char
.
La otra pregunta es si enviar el puntero
char*
nuevo a
float*
y asignarlo a través de él es válido.
Como tu
Foo
es de tipo POD, está bien.
Se le permite calcular la dirección del miembro de un 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, se convierte en
int*
o escribe en una dirección donde no existe ningún 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 dicho en tu pregunta,
reinterpret_cast<char*>(&f)
no apunta a una matriz de caracteres, sino a un flotante , ...
...
reinterpret_cast<char*>(&f)
ni siquiera apunta a un char
, por lo que incluso si la representación del objeto es una matriz de char, el comportamiento aún no está definido.
Para
offsetof
, todavía puedes usarlo como
struct Foo {
float x, y, z;
};
Foo f;
auto p = reinterpret_cast<std::uintptr_t>(&f) + offsetof(Foo, z);
// ^^^^^^^^^^^^^^
*reinterpret_cast<float*>(p) = 42.0f;
Ver CWG 1314.
De acuerdo con 6.9 [basic.types] párrafo 4,
La representación de objeto de un objeto de tipo T es la secuencia de N objetos char sin signo tomados por el objeto de tipo T, donde N es igual a sizeof (T).
y 4.5 [intro.object] párrafo 5,
Un objeto de tipo trivialmente copiable o de diseño estándar (6.9 [basic.types]) ocupará bytes contiguos de almacenamiento.
¿Estos pasajes hacen aritmética de puntero (8.7 [expr.add] párrafo 5) dentro de un objeto de diseño estándar bien definido (por ejemplo, para escribir la propia versión de memcpy?
Justificación (agosto de 2011):
La redacción actual es lo suficientemente clara como para permitir este uso.
Estoy totalmente en desacuerdo con la declaración de CWG de que "la redacción actual es suficientemente clara", pero, sin embargo, esa es la decisión que tenemos.
Interpreto que la respuesta de CWG sugiere que un puntero a un
unsigned char
en un objeto de tipo trivialmente copiable o de diseño estándar, a los fines de la aritmética del puntero, debe interpretarse como un puntero a una matriz de caracteres
unsigned char
cuyo tamaño es igual al tamaño de El objeto en cuestión.
No sé si pretendían que también funcionaría utilizando un puntero de caracteres o (a partir de C ++ 17) un puntero
std::byte
.
(Tal vez si hubieran decidido
aclararlo en
lugar de afirmar que la redacción existente era lo suficientemente clara, entonces sabría la respuesta).
(Otra cuestión es si se requiere
std::launder
para que el código del OP esté bien definido. No entraré en esto aquí; creo que merece una pregunta por separado).