¿El estándar C ++ permite que un bool no inicializado bloquee un programa?

500

Sé que un "comportamiento indefinido" en C ++ puede permitir que el compilador haga lo que quiera. Sin embargo, tuve un bloqueo que me sorprendió, ya que supuse que el código era lo suficientemente seguro.

En este caso, el problema real ocurrió solo en una plataforma específica que usa un compilador específico, y solo si la optimización estaba habilitada.

Intenté varias cosas para reproducir el problema y simplificarlo al máximo. Aquí hay un extracto de una función llamada Serialize, que tomaría un parámetro bool y copiaría la cadena trueo falseen un búfer de destino existente.

¿Estaría esta función en una revisión de código, no habría forma de decir que, de hecho, podría fallar si el parámetro bool fuera un valor no inicializado?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

Si este código se ejecuta con las optimizaciones de clang 5.0.0 +, se bloqueará.

El esperado operador ternario boolValue ? "true" : "false"parecía lo suficientemente seguro para mí, estaba asumiendo: "Cualquier valor de basura que contenga boolValueno importa, ya que de todos modos se evaluará como verdadero o falso".

He configurado un ejemplo de Compiler Explorer que muestra el problema en el desmontaje, aquí el ejemplo completo. Nota: para reprobar el problema, la combinación que encontré que funcionó es usando Clang 5.0.0 con la optimización -O2.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

El problema surge debido al optimizador: fue lo suficientemente inteligente como para deducir que las cadenas "verdadero" y "falso" solo difieren en longitud en 1. Entonces, en lugar de calcular realmente la longitud, utiliza el valor del bool en sí, que debería técnicamente puede ser 0 o 1, y dice así:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

Si bien esto es "inteligente", por así decirlo, mi pregunta es: ¿permite el estándar C ++ que un compilador suponga que un bool solo puede tener una representación numérica interna de '0' o '1' y usarlo de esa manera?

¿O es este un caso de implementación definida, en cuyo caso la implementación asumió que todas sus funciones solo contendrán 0 o 1, y cualquier otro valor es territorio de comportamiento indefinido?

Remz
fuente
200
Es una gran pregunta Es una ilustración sólida de cómo el comportamiento indefinido no es solo una preocupación teórica. Cuando la gente dice que algo puede suceder como resultado de UB, ese "cualquier cosa" puede ser realmente sorprendente. Uno podría suponer que el comportamiento indefinido aún se manifiesta de manera predecible, pero en estos días con optimizadores modernos eso no es del todo cierto. OP se tomó el tiempo para crear un MCVE, investigó el problema a fondo, inspeccionó el desmontaje y formuló una pregunta clara y directa al respecto. No podría pedir más.
John Kugelman
77
Observe que el requisito de que "no es cero evalúa a true" es una regla sobre las operaciones booleanas, incluida la "asignación a un bool" (que podría invocar implícitamente un static_cast<bool>()dependiente según los detalles). Sin embargo, no es un requisito sobre la representación interna de un boolelegido por el compilador.
Euro Micelli
2
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Samuel Liew
3
En una nota muy relacionada, esta es una fuente "divertida" de incompatibilidad binaria. Si tiene un ABI A que pone en cero los valores antes de llamar a una función, pero compila funciones de modo que asume que los parámetros están rellenados con ceros, y un ABI B es lo opuesto (no rellena con ceros, pero no asume cero (parámetros acolchados), funcionará principalmente , pero una función que usa B ABI causará problemas si llama a una función que usa A ABI que toma un parámetro 'pequeño'. IIRC tienes esto en x86 con clang e ICC.
TLW
1
@TLW: Aunque el Estándar no requiere que las implementaciones proporcionen ningún medio para llamar o ser llamado por código externo, habría sido útil tener un medio para especificar tales cosas para implementaciones donde son relevantes (implementaciones donde tales detalles no son relevante podría ignorar tales atributos).
supercat

Respuestas:

285

Sí, ISO C ++ permite (pero no requiere) implementaciones para tomar esta decisión.

Pero también tenga en cuenta que ISO C ++ permite que un compilador emita código que se bloquea a propósito (por ejemplo, con una instrucción ilegal) si el programa encuentra UB, por ejemplo, como una forma de ayudarlo a encontrar errores. (O porque es una DeathStation 9000. Cumplir estrictamente no es suficiente para que una implementación de C ++ sea útil para cualquier propósito real). Por lo tanto, ISO C ++ permitiría que un compilador creara un asm que se bloqueó (por razones totalmente diferentes) incluso en un código similar que lee un no inicializado uint32_t. Aunque se requiere que sea un tipo de diseño fijo sin representaciones de trampa.

Es una pregunta interesante sobre cómo funcionan las implementaciones reales, pero recuerde que incluso si la respuesta fuera diferente, su código seguiría siendo inseguro porque C ++ moderno no es una versión portátil del lenguaje ensamblador.


Estás compilando para el x86-64 System V ABI , que especifica que a boolcomo función arg en un registro está representado por los patrones de bitsfalse=0 ytrue=1 en los 8 bits bajos del registro 1 . En la memoria, booles un tipo de 1 byte que nuevamente debe tener un valor entero de 0 o 1.

(Un ABI es un conjunto de opciones de implementación que los compiladores de la misma plataforma acuerdan para que puedan crear códigos que se invoquen entre sí, incluidos los tamaños de tipo, las reglas de diseño de estructura y las convenciones de llamada).

ISO C ++ no lo especifica, pero esta decisión ABI está muy extendida porque hace que la conversión bool-> int sea barata (solo extensión cero) . No conozco ninguna ABI que no permita que el compilador asuma 0 o 1 boolpara cualquier arquitectura (no solo x86). Permite optimizaciones como !myboolcon xor eax,1para voltear el bit bajo: cualquier código posible que pueda voltear un bit / entero / bool entre 0 y 1 en una sola instrucción de CPU . O compilando a&&bun bit a bit Y para booltipos. Algunos compiladores realmente aprovechan los valores booleanos como 8 bits en los compiladores. ¿Las operaciones en ellos son ineficientes? .

En general, la regla as-if permite que el compilador aproveche las cosas que son verdaderas en la plataforma objetivo para la que se compila , porque el resultado final será un código ejecutable que implementa el mismo comportamiento visible externamente como la fuente C ++. (Con todas las restricciones que Undefined Behavior impone a lo que en realidad es "externamente visible": no con un depurador, sino desde otro hilo en un programa C ++ bien formado / legal).

El compilador es, sin duda permitió a sacar el máximo provecho de una garantía ABI en su código-gen, y hacer que el código que has encontrado lo que optimiza strlen(whichString)a
5U - boolValue.
(Por cierto, esta optimización es un poco inteligente, pero tal vez miope versus ramificación y alineación memcpycomo almacenes de datos inmediatos 2 ).

O el compilador podría haber creado una tabla de punteros e indexarla con el valor entero de bool, suponiendo nuevamente que era un 0 o 1. ( Esta posibilidad es lo que sugiere la respuesta de @ Barmar ).


Su __attribute((noinline))constructor con la optimización habilitada condujo al sonido metálico simplemente cargando un byte de la pila para usar como uninitializedBool. Hizo espacio para el objeto maincon push rax(que es más pequeño y por varias razones casi tan eficiente como sub rsp, 8), por lo que cualquier basura que haya en AL al entrar maines el valor para el que se usó uninitializedBool. Es por eso que realmente obtuviste valores que no eran solo0 .

5U - random garbagepuede ajustarse fácilmente a un gran valor sin signo, lo que lleva a la memoria a ir a la memoria sin asignar. El destino está en el almacenamiento estático, no en la pila, por lo que no está sobrescribiendo una dirección de retorno o algo así.


Otras implementaciones podrían tomar diferentes decisiones, por ejemplo, false=0y true=any non-zero value. Entonces, el sonido de claxon probablemente no generaría código que se bloquee para esta instancia específica de UB. (Pero aún así se permitiría si quisiera). No conozco ninguna implementación que elija otra cosa para lo que x86-64 hacebool , pero el estándar C ++ permite muchas cosas que nadie hace o incluso querría hacer. hardware similar a las CPU actuales.

ISO C ++ deja sin especificar lo que encontrará cuando examine o modifique la representación de objeto de abool . (p. ej. memcpyal boolingresar unsigned char, lo que se le permite hacer porque char*puede alias cualquier cosa. Yunsigned char se garantiza que no tendrá bits de relleno, por lo que el estándar C ++ le permite formalmente representar objetos hexagonales sin ningún UB. Proyección de puntero para copiar el objeto la representación es diferente de la asignación char foo = my_bool, por supuesto, por lo que la booleanización a 0 o 1 no sucedería y obtendría la representación del objeto sin procesar).

Ha "ocultado" parcialmente la UB en esta ruta de ejecución desde el compilador connoinline . Sin embargo, incluso si no está en línea, las optimizaciones interprocediales aún podrían hacer una versión de la función que depende de la definición de otra función. (Primero, clang está haciendo un ejecutable, no una biblioteca compartida de Unix donde puede ocurrir la interposición de símbolos. Segundo, la definición dentro de la class{}definición, por lo que todas las unidades de traducción deben tener la misma definición.inline palabra clave).

Por lo tanto, un compilador podría emitir solo una reto ud2(instrucción ilegal) como la definición de main, porque la ruta de ejecución que comienza en la parte superior maininevitablemente encuentra un Comportamiento indefinido. (Lo que el compilador puede ver en el momento de la compilación si decide seguir la ruta a través del constructor no en línea).

Cualquier programa que encuentre UB está totalmente indefinido para toda su existencia. Pero UB dentro de una función o if()rama que nunca se ejecuta realmente no corrompe el resto del programa. En la práctica, eso significa que los compiladores pueden decidir emitir una instrucción ilegal, o a ret, o no emitir nada y caer en el siguiente bloque / función, para que todo el bloque básico que se puede probar en el momento de la compilación contenga o conduzca a UB.

CCG y Sonido metálico en la práctica hacen realidad a veces emiten ud2en la UB, en lugar de intentar siquiera para generar código para rutas de ejecución que no tienen sentido. O para casos como caerse del final de una no voidfunción, a veces gcc omitirá una retinstrucción. Si estabas pensando que "mi función simplemente volverá con cualquier basura que haya en RAX", estás muy equivocado. Los compiladores modernos de C ++ ya no tratan el lenguaje como un lenguaje ensamblador portátil. Su programa realmente tiene que ser C ++ válido, sin hacer suposiciones acerca de cómo una versión independiente no en línea de su función podría verse en asm.

Otro ejemplo divertido es ¿Por qué el acceso no alineado a la memoria mmap'ed a veces es predeterminado en AMD64? . x86 no falla en enteros no alineados, ¿verdad? Entonces, ¿por qué un desalineado uint16_t*sería un problema? Porque alignof(uint16_t) == 2, y violar esa suposición condujo a una segfault cuando se auto-vectoriza con SSE2.

Vea también Lo que todo programador de C debe saber sobre el comportamiento indefinido # 1/3 , un artículo de un desarrollador de clang.

Punto clave: si el compilador notó el UB en el momento de la compilación, podría "romper" (emitir un asm sorprendente) la ruta a través de su código que causa UB incluso si apunta a un ABI donde cualquier patrón de bits es una representación de objeto válida bool.

Espere una hostilidad total hacia muchos errores por parte del programador, especialmente de lo que advierten los compiladores modernos. Es por eso que debe usar -Wally corregir las advertencias. C ++ no es un lenguaje fácil de usar, y algo en C ++ puede ser inseguro incluso si estaría seguro en el destino para el que está compilando. (por ejemplo, el desbordamiento firmado es UB en C ++ y los compiladores supondrán que no sucede, incluso cuando se compila para el complemento x86 de 2, a menos que useclang/gcc -fwrapv ).

La UB visible en tiempo de compilación siempre es peligrosa, y es realmente difícil estar seguro (con la optimización del tiempo de enlace) de que realmente ha ocultado UB del compilador y, por lo tanto, puede razonar sobre qué tipo de asm generará.

No ser demasiado dramático; a menudo los compiladores le permiten salirse con la suya y emitir código como espera incluso cuando algo es UB. Pero tal vez sea un problema en el futuro si los desarrolladores del compilador implementan una optimización que obtiene más información sobre los rangos de valores (por ejemplo, que una variable no es negativa, lo que quizás le permita optimizar la extensión de signo para liberar la extensión cero en x86- 64) Por ejemplo, en gcc y clang actuales, hacer tmp = a+INT_MINno se optimiza a<0como siempre falso, solo que tmpsiempre es negativo. (Porque INT_MIN+ a=INT_MAXes negativo en el objetivo del complemento de este 2, ya no puede ser más alto que eso).

Por lo tanto, gcc / clang no retrocede actualmente para derivar información de rango para las entradas de un cálculo, solo en los resultados basados ​​en la suposición de un desbordamiento no firmado: ejemplo en Godbolt . No sé si esta optimización se "pierde" intencionalmente en nombre de la facilidad de uso o qué.

También tenga en cuenta que las implementaciones (también conocidas como compiladores) pueden definir el comportamiento que ISO C ++ deja sin definir . Por ejemplo, todos los compiladores que admiten los elementos intrínsecos de Intel (como _mm_add_ps(__m128, __m128)la vectorización SIMD manual) deben permitir formar punteros mal alineados, que es UB en C ++, incluso si no los desreferencia. __m128i _mm_loadu_si128(const __m128i *)realiza cargas no alineadas tomando un __m128i*argumento desalineado , no un void*o char*. ¿Es `reinterpret_cast`ing entre el puntero de vector de hardware y el tipo correspondiente un comportamiento indefinido?

GNU C / C ++ también define el comportamiento de desplazar a la izquierda un número con signo negativo (incluso sin -fwrapv), por separado de las reglas normales de UB con desbordamiento con signo. ( Esto es UB en ISO C ++ , mientras que los desplazamientos a la derecha de los números con signo están definidos por la implementación (lógica frente a aritmética); las implementaciones de buena calidad eligen la aritmética en HW que tiene desplazamientos aritméticos a la derecha, pero ISO C ++ no especifica). Esto se documenta en la sección Integer del manual de GCC , junto con la definición del comportamiento definido por la implementación de que los estándares C requieren implementaciones para definir de una forma u otra.

Definitivamente, hay problemas de calidad de implementación que preocupan a los desarrolladores de compiladores; Por lo general, no están tratando de hacer compiladores que sean intencionalmente hostiles, pero aprovecharse de todos los baches UB en C ++ (excepto los que eligen definir) para optimizar mejor puede ser casi indistinguible a veces.


Nota al pie 1 : Los 56 bits superiores pueden ser basura que el destinatario debe ignorar, como es habitual para los tipos más estrechos que un registro.

( Otros ABIs hacen tomar decisiones diferentes aquí . Algunos no requieren tipos enteros estrechas ser cero o signo extendido para llenar un registro o cuando se pasa a regresar de funciones, como MIPS64 y PowerPC64. Ver la última sección de esta respuesta x86-64 que se compara con los ISA anteriores ).

Por ejemplo, una persona que llama podría haber calculado a & 0x01010101en RDI y haberlo usado para otra cosa, antes de llamar bool_func(a&1). La persona que llama podría optimizar el &1porque ya lo hizo al byte bajo como parte de and edi, 0x01010101, y sabe que la persona que llama debe ignorar los bytes altos.

O si se pasa un bool como el tercer argumento, tal vez una persona que llama que optimiza el tamaño del código lo carga en mov dl, [mem]lugar de movzx edx, [mem]guardar 1 byte a costa de una falsa dependencia del valor anterior de RDX (u otro efecto de registro parcial, dependiendo en modelo de CPU). O para el primer argumento, en mov dil, byte [r10]lugar de movzx edi, byte [r10], porque ambos requieren un prefijo REX de todos modos.

Esta es la razón por la cual se emite el sonido metálico movzx eax, dilen Serializelugar de sub eax, edi. (Para los argumentos enteros, clang viola esta regla ABI, dependiendo del comportamiento indocumentado de gcc y clang a enteros estrechos de extensión cero o signo a 32 bits. Se requiere una extensión signo o cero al agregar un desplazamiento de 32 bits a un puntero para el x86-64 ABI? Así que me interesó ver que no hace lo mismo bool).


Nota a pie de página 2: después de bifurcar, solo tendría una movtienda de 4 bytes inmediata o una tienda de 4 bytes + 1 byte. La longitud está implícita en los anchos de tienda + compensaciones.

OTOH, glibc memcpy hará dos cargas / tiendas de 4 bytes con una superposición que depende de la longitud, por lo que esto realmente hace que todo esté libre de ramas condicionales en el booleano. Vea el L(between_4_7):bloque en la memoria / memoria de glibc. O al menos, siga el mismo camino para cualquiera de los booleanos en la ramificación de memcpy para seleccionar un tamaño de fragmento.

Si está en línea, puede usar 2x mov-media + cmovy un desplazamiento condicional, o puede dejar los datos de la cadena en la memoria.

O si se está ajustando para Intel Ice Lake ( con la función Fast Short REP MOV ), un real rep movsbpodría ser óptimo. glibc memcpypodría comenzar a usar rep movsb para tamaños pequeños en CPU con esa función, ahorrando muchas ramificaciones.


Herramientas para detectar UB y uso de valores no inicializados

En gcc y clang, puede compilar -fsanitize=undefinedpara agregar instrumentación en tiempo de ejecución que avisará o generará un error en UB que ocurre en tiempo de ejecución. Sin embargo, eso no captará variables unitarias. (Debido a que no aumenta los tamaños de letra para dejar espacio para un bit "no inicializado").

Ver https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

Para encontrar el uso de datos no inicializados, hay Address Sanitizer y Memory Sanitizer en clang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer muestra ejemplos de clang -fsanitize=memory -fPIE -piedetección de lecturas de memoria no inicializadas. Podría funcionar mejor si compila sin optimización, por lo que todas las lecturas de variables terminan realmente cargándose de la memoria en el asm. Muestran que se usa en los-O2 en un caso en el que la carga no se optimizaría. No lo he intentado yo mismo. (En algunos casos, por ejemplo, no inicializando un acumulador antes de sumar una matriz, clang -O3 emitirá un código que suma un registro vectorial que nunca se inicializó. Entonces, con la optimización, puede tener un caso en el que no hay lectura de memoria asociada con la UB . Pero-fsanitize=memory cambia el asm generado, y podría resultar en una verificación para esto).

Tolerará la copia de memoria no inicializada, y también operaciones lógicas y aritméticas simples con ella. En general, MemorySanitizer rastrea silenciosamente la propagación de datos no inicializados en la memoria e informa una advertencia cuando se toma (o no se toma) una ramificación de código, según un valor no inicializado.

MemorySanitizer implementa un subconjunto de funcionalidades que se encuentra en Valgrind (herramienta Memcheck).

Debería funcionar para este caso porque la llamada a glibc memcpycon una lengthmemoria calculada a partir de memoria no inicializada (dentro de la biblioteca) dará como resultado una bifurcación basada en length. Si hubiera incluido una versión completamente sin ramificaciones que acaba de usar cmov, indexación y dos tiendas, podría no haber funcionado.

Valgrindmemcheck también buscará este tipo de problema, de nuevo no se quejará si el programa simplemente copia datos no inicializados. Pero dice que detectará cuándo un "salto o movimiento condicional depende de valores no inicializados", para tratar de detectar cualquier comportamiento visible desde el exterior que dependa de datos no inicializados.

Quizás la idea detrás de no marcar solo una carga es que las estructuras pueden tener relleno, y copiar toda la estructura (incluido el relleno) con una carga / almacén de vector ancho no es un error, incluso si los miembros individuales solo se escribieron uno a la vez. En el nivel asm, se ha perdido la información sobre lo que estaba rellenando y lo que realmente es parte del valor.

Peter Cordes
fuente
2
He visto un caso peor en el que la variable tomó un valor no dentro del rango de un entero de 8 bits, sino solo de todo el registro de la CPU. E Itanium tiene una peor aún, el uso de una variable no inicializada puede bloquearse por completo.
Joshua
2
@Joshua: oh, claro, buen punto, la especulación explícita de Itanium etiquetará los valores de registro con un equivalente de "no un número", de modo que el uso de las fallas de valor.
Peter Cordes el
11
Además, esto también ilustra por qué el UB featurebug se introdujo en el diseño de los lenguajes C y C ++ en primer lugar: porque le da al compilador exactamente este tipo de libertad, que ahora ha permitido que los compiladores más modernos realicen estos programas de alta calidad. optimizaciones que hacen de C / C ++ lenguajes de nivel medio de alto rendimiento.
The_Sympathizer
2
Y así continúa la guerra entre los escritores de compiladores de C ++ y los programadores de C ++ que intentan escribir programas útiles. Esta respuesta, totalmente completa para responder a esta pregunta, también podría usarse como una copia publicitaria convincente para los proveedores de herramientas de análisis estático ...
davidbak
44
@The_Sympathizer: UB se incluyó para permitir que las implementaciones se comporten de la forma que sea más útil para sus clientes . No pretendía sugerir que todos los comportamientos deberían considerarse igualmente útiles.
supercat
56

El compilador puede asumir que un valor booleano pasado como argumento es un valor booleano válido (es decir, uno que se ha inicializado o convertido a trueo false). El truevalor no tiene que ser el mismo que el entero 1; de hecho, puede haber varias representaciones de truey false, pero el parámetro debe ser una representación válida de uno de esos dos valores, donde la "representación válida" es la implementación. definido.

Entonces, si no puede inicializar un bool, o si logra sobrescribirlo a través de algún puntero de un tipo diferente, entonces las suposiciones del compilador serán incorrectas y se producirá un Comportamiento indefinido. Te habían advertido:

50) El uso de un valor bool en las formas descritas por esta Norma Internacional como "indefinido", como al examinar el valor de un objeto automático no inicializado, puede hacer que se comporte como si no fuera ni verdadero ni falso. (Nota al pie del párrafo 6 del §6.9.1, Tipos fundamentales)

rici
fuente
11
El " truevalor no tiene que ser el mismo que el entero 1" es un poco engañoso. Claro, el patrón de bits real podría ser otra cosa, pero cuando se convierte / promueve implícitamente (la única forma en que vería un valor distinto de true/ false), truees siempre 1y falsesiempre es0 . Por supuesto, dicho compilador tampoco podría usar el truco que este compilador estaba tratando de usar (usando el hecho de que boolel patrón de bits real solo podía ser 0o 1), por lo que es algo irrelevante para el problema del OP.
ShadowRanger
44
@ShadowRanger Siempre puede inspeccionar la representación del objeto directamente.
TC
77
@shadowranger: mi punto es que la implementación está a cargo. Si limita las representaciones válidas del truepatrón de bits 1, esa es su prerrogativa. Si elige algún otro conjunto de representaciones, entonces no podría usar la optimización que se menciona aquí. Si elige esa representación particular, entonces puede. Solo necesita ser internamente consistente. Usted puede examinar la reclamación de un boolcopiándolo en una matriz de bytes; eso no es UB (pero está definido por la implementación)
rici
3
Sí, los compiladores optimizadores (es decir, la implementación de C ++ en el mundo real) a menudo emiten código que depende de booltener un patrón de bits de 0o 1. No vuelven a booleanizar boolcada vez que lo leen de la memoria (o un registro que contiene una función arg). Eso es lo que dice esta respuesta. ejemplos : gcc4.7 + puede optimizar return a||ba or eax, edien una función que regresa bool, o MSVC puede optimizar a&ba test cl, dl. x86 testes un bit a bitand , así que si cl=1y dl=2prueba establece banderas de acuerdo con cl&dl = 0.
Peter Cordes
55
El punto sobre el comportamiento indefinido es que el compilador puede sacar muchas más conclusiones al respecto, por ejemplo, suponer que nunca se toma una ruta de código que conduzca al acceso a un valor no inicializado, ya que esto es responsabilidad del programador. . Por lo tanto, no se trata solo de la posibilidad de que los valores de bajo nivel puedan ser diferentes de cero o uno.
Holger
52

La función en sí es correcta, pero en su programa de prueba, la declaración que llama a la función causa un comportamiento indefinido al usar el valor de una variable no inicializada.

El error está en la función de llamada, y podría detectarse mediante la revisión del código o el análisis estático de la función de llamada. Usando el enlace del explorador del compilador, el compilador gcc 8.2 detecta el error. (Tal vez podría presentar un informe de error contra el sonido metálico que no encuentra el problema).

El comportamiento indefinido significa que cualquier cosa puede suceder, lo que incluye que el programa se bloquee unas pocas líneas después del evento que desencadenó el comportamiento indefinido.

NÓTESE BIEN. La respuesta a "¿Puede un comportamiento indefinido causar _____?" es siempre "Sí". Esa es literalmente la definición de comportamiento indefinido.

MM
fuente
2
¿Es verdadera la primera cláusula? ¿Simplemente copiar un boolUB no inicializado dispara?
Joshua Green el
10
@JoshuaGreen ver [dcl.init] / 12 "Si una evaluación produce un valor indeterminado, el comportamiento es indefinido excepto en los siguientes casos:" (y ninguno de esos casos tiene una excepción para bool). Copiar requiere evaluar la fuente
MM
8
@JoshuaGreen Y la razón de esto es que puede tener una plataforma que desencadena una falla de hardware si accede a algunos valores no válidos para algunos tipos. A veces se les llama "representaciones de trampas".
David Schwartz
77
Itanium, aunque oscuro, es una CPU que todavía está en producción, tiene valores de trampa y tiene dos compiladores C ++ al menos semi-modernos (Intel / HP). Es, literalmente, tiene true, falsey not-a-thingvalores para booleanos.
MSalters
3
Por otro lado, la respuesta a "¿El estándar requiere que todos los compiladores procesen algo de una manera determinada" es generalmente "no", incluso / especialmente en los casos en que es obvio que cualquier compilador de calidad debería hacerlo; cuanto más obvio sea algo, menor será la necesidad de que los autores de la Norma lo digan realmente.
supercat
23

Un bool solo puede contener los valores dependientes de la implementación utilizados internamente para truey false, y el código generado puede suponer que solo contendrá uno de estos dos valores.

Normalmente, la implementación usará el número entero 0for falseand 1for true, para simplificar las conversiones entre booly int, y if (boolvar)generar el mismo código que if (intvar). En ese caso, uno puede imaginar que el código generado para el ternario en la asignación usaría el valor como índice en una matriz de punteros a las dos cadenas, es decir, podría convertirse en algo como:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

Si boolValueno se inicializa, en realidad podría contener cualquier valor entero, lo que provocaría el acceso fuera de los límites de la stringsmatriz.

Barmar
fuente
1
@SidS Gracias. Teóricamente, las representaciones internas podrían ser lo contrario de cómo emiten hacia / desde enteros, pero eso sería perverso.
Barmar
1
Tienes razón, y tu ejemplo también se bloqueará. Sin embargo, es "visible" para una revisión de código que está utilizando una variable no inicializada como índice de una matriz. Además, se bloqueará incluso en la depuración (por ejemplo, algún depurador / compilador se inicializará con patrones específicos para que sea más fácil ver cuándo se bloquea). En mi ejemplo, la parte sorprendente es que el uso del bool es invisible: el optimizador decidió usarlo en un cálculo que no está presente en el código fuente.
Remz
3
@Remz Solo estoy usando la matriz para mostrar a qué podría ser equivalente el código generado, sin sugerir que alguien realmente escribiría eso.
Barmar
1
@Remz Vuelva a emitir el boolto intcon *(int *)&boolValuee imprímalo con fines de depuración, vea si es diferente 0o 1cuando falla. Si ese es el caso, confirma la teoría de que el compilador está optimizando el if en línea como una matriz que explica por qué está fallando.
Havenard el
2
@MSalters: std::bitset<8>no me da buenos nombres para todas mis diferentes banderas. Dependiendo de lo que sean, eso puede ser importante.
Martin Bonner apoya a Monica el
15

Resumiendo mucho su pregunta, está preguntando ¿El estándar C ++ permite que un compilador suponga boolque solo puede tener una representación numérica interna de '0' o '1' y usarla de esa manera?

El estándar no dice nada sobre la representación interna de a bool. Solo define lo que sucede cuando se lanza un boolaint (o viceversa). Principalmente, debido a estas conversiones integrales (y al hecho de que las personas confían bastante en ellas), el compilador usará 0 y 1, pero no tiene que hacerlo (aunque debe respetar las restricciones de cualquier ABI de nivel inferior que use )

Entonces, el compilador, cuando ve que booltiene derecho a considerar que dicho boolcontiene cualquiera de los patrones de bits ' true' o ' false' y hace lo que se siente. Entonces, si los valores para trueyfalse son 1 y 0, respectivamente, el compilador se permite de hecho a optimizar el strlena 5 - <boolean value>. ¡Otros comportamientos divertidos son posibles!

Como se dice repetidamente aquí, el comportamiento indefinido tiene resultados indefinidos. Incluyendo pero no limitado a

  • Su código funciona como esperaba
  • Tu código falla en momentos aleatorios
  • Su código no se ejecuta en absoluto.

Vea lo que todo programador debe saber sobre el comportamiento indefinido

Tom Tanner
fuente