c++ - ¿La macro ''offsetof'' de<stddef.h> invoca un comportamiento indefinido?
undefined-behavior (6)
Ejemplo de la implementación de MSVC:
#define offsetof(s,m) /
(size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))
// ^^^^^^^^^^^
Como puede verse, hace referencia a un puntero nulo, que normalmente invoca un comportamiento indefinido. ¿Es esto una excepción a la regla o lo que está pasando?
Cuando el Estándar C especifica que ciertas acciones invocan un comportamiento indefinido, eso no significa que tales acciones hayan sido prohibidas, sino que las implementaciones fueron libres de especificar los comportamientos consecuentes o no según lo consideren oportuno. En consecuencia, las implementaciones tendrían la libertad de realizar tales acciones en los casos en que la Norma requiera un comportamiento definido, solo si las implementaciones pueden garantizar que las conductas para esas acciones sean coherentes con lo que requiere la Norma . Considere, por ejemplo, la siguiente implementación de strcpy:
char *strcpy(char *dest, char const *src)
{
ptrdiff_t diff = dest-src-1;
int ch;
while((ch = *src++) != 0)
src[diff] = ch;
return dest;
}
Si src
y dest
son punteros no relacionados, el cálculo de dest-src
generaría un comportamiento indefinido. Sin embargo, en algunas plataformas, la relación entre char*
y ptrdiff_t
es tal que, dada cualquier char* p1, p2
, el cálculo p1 + (p2-p1);
siempre será igual a p2
. En las plataformas que hacen esa garantía, la implementación anterior de strcpy
sería legítima (y en algunas de esas plataformas podría ser más rápida que cualquier alternativa plausible). Sin embargo, en algunas otras plataformas, una función de este tipo siempre puede fallar, excepto cuando ambas cadenas forman parte del mismo objeto asignado.
El mismo principio se aplica a la macro offsetof
. No hay ningún requisito para que los compiladores ofrezcan ninguna forma de obtener un comportamiento equivalente a offsetof
(aparte de usar esa macro) Si el modelo de un compilador para aritmética de punteros hace posible obtener el offsetof
comportamiento requerido utilizando el operador ->
en un puntero nulo , entonces su macro offsetof
puede hacer eso. Si un compilador no admitiría ningún esfuerzo por usar ->
en algo que no sea un puntero legítimo a una instancia del tipo, entonces es posible que deba definir un intrínseco que pueda calcular un desplazamiento de campo y definir la macro de offsetof
para usarlo. Lo importante no es que el Estándar defina los comportamientos de las acciones realizadas con macros y funciones de la biblioteca estándar, sino que la implementación asegura que los comportamientos de tales macros y funciones cumplan con los requisitos.
Cuando el estándar de idioma dice "comportamiento indefinido", cualquier compilador dado puede definir el comportamiento. El código de implementación en la biblioteca estándar generalmente se basa en eso. Así que hay dos preguntas:
(1) ¿Es el código UB con respecto al estándar C ++?
Esa es una pregunta realmente difícil, porque es un defecto casi conocido que el estándar C ++ 98/03 nunca dice en el texto normativo que, en general, es UB para eliminar la referencia a un punto nulo. Está implícito en la excepción para typeid
, donde no es UB.
Lo que puede decir con offsetof
es que es UB utilizar offsetof
con un tipo que no sea POD.
(2) ¿Es el código UB con respecto al compilador para el que está escrito?
No claro que no.
El código del proveedor de un compilador para un compilador determinado puede usar cualquier característica de ese compilador.
Salud y salud,
Esto es básicamente equivalente a preguntar si esto es UB:
s* p = 0;
volatile auto& r = p->m;
Claramente, no se genera acceso a la memoria al destino de r
, porque es volatile
y el compilador tiene prohibido generar accesos espurios a variables volatile
. Pero *s
no es volátil, por lo que el compilador podría generar un acceso a él. Ni la dirección del operador ni la conversión al tipo de referencia crean un contexto no evaluado de acuerdo con el estándar.
Entonces, no veo ninguna razón para lo volatile
, y estoy de acuerdo con los demás en que este es un comportamiento indefinido de acuerdo con el estándar. Por supuesto, a cualquier compilador se le permite definir un comportamiento donde el estándar lo deja específico de implementación o no definido.
Finalmente, una nota en la sección [dcl.ref]
dice
en particular, no puede existir una referencia nula en un programa bien definido, porque la única forma de crear dicha referencia sería vincularla al "objeto" obtenido al eliminar la referencia de un puntero nulo, lo que causa un comportamiento indefinido.
La noción de "comportamiento indefinido" no es aplicable a la implementación de la Biblioteca estándar, independientemente de si se trata de una macro, una función o cualquier otra cosa.
En el caso general, la biblioteca estándar no debe considerarse implementada en lenguaje C ++ (o C). Eso se aplica a los archivos de encabezado estándar también. La biblioteca estándar debe cumplir con sus especificaciones externas, pero todo lo demás es un detalle de implementación, exento de todos y cualquier otro requisito del lenguaje. La Biblioteca estándar siempre debe considerarse implementada en algún lenguaje "interno", que puede parecerse mucho a C ++ o C, pero aún no es C ++ ni C.
En otras palabras, la macro que usted citó no produce un comportamiento indefinido, siempre y cuando sea específicamente la macro de offsetof
definida en la Biblioteca estándar. Pero si hace exactamente lo mismo en su código (como definir su propia macro de la misma manera), de hecho resultará en un comportamiento indefinido. "Quod licet Jovi, non licet bovi".
NO es un comportamiento indefinido en C ++ si m
está en el desplazamiento 0 dentro de la estructura s
, así como en otros casos. Según el número 232 (énfasis mío):
El operador unario * realiza una indirección: la expresión a la que se aplica debe ser un puntero a un tipo de objeto o un puntero a un tipo de función y el resultado es un lvalor que se refiere al objeto o función al que apunta la expresión, si corresponde. . Si el puntero es un valor de puntero nulo (7.11 [conv.ptr]) o apunta uno más allá del último elemento de un objeto de matriz (8.7 [expr.add]), el resultado es un lvalue vacío y no se refiere a ningún objeto o función. Un valor vacío no es modificable.
Por lo tanto, el &((s *)0)->m
es un comportamiento indefinido solo si m
no es ni en el offset 0, ni en un offset correspondiente a una dirección que está más allá del último elemento de un objeto de matriz. Tenga en cuenta que la adición de un desplazamiento de 0 a null
se permite en C ++ pero no en C.
Como han señalado otros, se permite (y es muy probable) que el compilador nunca cree el comportamiento indefinido, y puede estar empaquetado con bibliotecas que hacen uso de las especificaciones mejoradas del compilador específico.
No, este NO es un comportamiento indefinido. La expresión se resuelve en tiempo de ejecución.
Tenga en cuenta que está tomando la dirección del miembro m
desde un puntero nulo. NO se está desreferiendo el puntero nulo.