c++ language-lawyer sizeof undefined-behavior null-pointer

¿La evaluación de la expresión a la que se aplica sizeof hace que sea legal desreferenciar un puntero nulo o no válido dentro de sizeof en C++?



language-lawyer undefined-behavior (3)

En primer lugar, he visto esta pregunta sobre C99 y la respuesta aceptada operando referencias no se evalúa la redacción en el borrador estándar de C99. No estoy seguro de que esta respuesta se aplique a C ++ 03. También existe esta pregunta sobre C ++ que tiene una respuesta aceptada que cita una redacción similar y también En algunos contextos, aparecen operandos no evaluados. Un operando no evaluado no se evalúa. fraseología.

Tengo este codigo:

int* ptr = 0; void* buffer = malloc( 10 * sizeof( *ptr ) );

La pregunta es: ¿hay una desreferencia de puntero nulo (y, por lo tanto, UB) dentro de sizeof() ?

C ++ 03 5.3.3 / 1 dice que el operador sizeof produce el número de bytes en la representación del objeto de su operando. El operando es una expresión, que no se evalúa, o una identificación de tipo entre paréntesis.

Los enlaces a las respuestas citan este texto u otro similar y hacen uso de la parte "no evaluado" para deducir que no hay UB.

Sin embargo, no puedo encontrar exactamente dónde el estándar vincula la evaluación de tener o no tener UB en este caso.

¿"No evaluar" la expresión a la que se aplica sizeof hace legal desreferenciar un puntero nulo o no válido dentro de sizeof en C ++?


Como solicitó explícitamente referencias estándar - [expr.sizeof] / 1:

El operando es una expresión, que es un operando no evaluado (Cláusula 5) , o una identificación de tipo entre paréntesis.

[expr] / 8:

En algunos contextos, aparecen operandos no evaluados (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). Un operando no evaluado no se evalúa.

Debido a que la expresión (es decir, la desreferenciación) nunca se evalúa, esta expresión no está sujeta a algunas restricciones que normalmente estaría violando. Solo se inspecciona el tipo. De hecho, el estándar utiliza referencias nulas en un ejemplo en [dcl.fct] / 12:

Un tipo de retorno de trailing es más útil para un tipo que sería más complicado de especificar antes del id del declarador :

template <class T, class U> auto add(T t, U u) -> decltype(t + u);

más bien que

template <class T, class U> decltype((*(T*)0) + (*(U*)0)) add(T t, U u);

- nota final ]


Creo que esto actualmente no se especifica en el estándar, como muchos temas como ¿Cuál es la categoría de valor de los operandos de los operadores de C ++ cuando no se especifica? . No creo que haya sido intencional, como lo señala hvd, probablemente sea obvio para el comité.

En este caso específico, creo que tenemos la evidencia para mostrar cuál era la intención. Del comentario del GB 91 de la reunión de Rapperswil que dice:

Es un poco desagradable desreferenciar un puntero nulo como parte de nuestra especificación, ya que estamos jugando al límite del comportamiento indefinido. Con la adición de la plantilla de función de declive, ya utilizada en estas mismas expresiones, esto ya no es necesario.

y sugirió una expresión alternativa, se refiere a esta expresión que ya no está en el estándar pero se puede encontrar en N3090 :

noexcept(*(U*)0 = declval<U>())

La sugerencia fue rechazada ya que esto no invoca un comportamiento indefinido ya que no está evaluado:

No existe un comportamiento indefinido porque la expresión es un operando no evaluado. No está nada claro que el cambio propuesto sería más claro.

Esta lógica también se aplica a sizeof ya que sus operandos no están evaluados.

Digo no especificado pero me pregunto si esto está cubierto por la sección 4.1 [conv.lval] que dice:

El valor contenido en el objeto indicado por lvalue es el resultado de rvalue. Cuando se produce una conversión lvalue-to-rvalue dentro del operando de sizeof (5.3.3), no se accede al valor contenido en el objeto referenciado, ya que ese operador no evalúa su operando.

Dice que no se accede al valor contenido, lo que si seguimos la lógica del problema 232 significa que no hay un comportamiento indefinido:

En otras palabras, es solo el acto de "obtener", de la conversión de valor a valor, lo que desencadena el comportamiento mal formado o indefinido

Esto es algo especulativo ya que el problema aún no está resuelto.


La especificación solo dice que desreferenciar algún puntero que es NULL es UB. Como sizeof () no es una función real, y en realidad no usa los argumentos para otra cosa que no sea obtener el tipo, nunca hace referencia al puntero. Por eso funciona. Alguien más puede obtener todos los puntos para buscar la redacción de especificaciones que establece que "no se hace referencia al argumento de sizeof ".

Tenga en cuenta que también es completamente legal hacer int arr[2]; size_t s = sizeof(arr[-111100000]); int arr[2]; size_t s = sizeof(arr[-111100000]); tampoco, no importa cuál sea el índice, porque sizeof nunca "hace nada" al argumento pasado.

Otro ejemplo para mostrar cómo "no está haciendo nada" sería algo como esto:

int func() { int *ptr = reinterpret_cast<int*>(32); *ptr = 7; return 42; } size_t size = sizeof(func());

Nuevamente, esto no se bloqueará, porque el compilador resuelve func() al tipo que produce.

Del mismo modo, si sizeof realmente "hace algo" con el argumento, ¿qué sucedería al hacer esto?

char *buffer = new sizeof(char[10000000000]);

¿ 10000000000 una asignación de pila 10000000000 y luego devolvería el tamaño después de que se bloqueara el código porque no hay suficientes megabytes de pila? [En algunos sistemas, el tamaño de la pila se cuenta en bytes, no en megabytes]. Y aunque nadie escribe un código como ese, fácilmente se te ocurre algo similar usando typedef de buffer_type como una matriz de caracteres o algún tipo de struct con gran contenido.