Permítanme decir esto claramente: no invocamos comportamientos indefinidos en nuestros programas . Nunca es una buena idea, punto. Hay raras excepciones a esta regla; por ejemplo, si es un implementador de bibliotecas que implementa offsetof . Si su caso se encuentra bajo tal excepción, probablemente ya lo sepa. En este caso, sabemos que el uso de variables automáticas no inicializadas es un comportamiento indefinido .
Los compiladores se han vuelto muy agresivos con optimizaciones en torno al comportamiento indefinido y podemos encontrar muchos casos en los que el comportamiento indefinido ha dado lugar a fallas de seguridad. ¿El caso más infame es probablemente la eliminación de la comprobación de puntero nulo del kernel de Linux que menciono en mi respuesta al error de compilación de C ++? donde una optimización del compilador en torno al comportamiento indefinido convirtió un bucle finito en uno infinito.
Podemos leer las optimizaciones peligrosas y la pérdida de causalidad de CERT ( video ) que dice, entre otras cosas:
Cada vez más, los escritores de compiladores aprovechan comportamientos indefinidos en los lenguajes de programación C y C ++ para mejorar las optimizaciones.
Con frecuencia, estas optimizaciones interfieren con la capacidad de los desarrolladores para realizar análisis de causa y efecto en su código fuente, es decir, analizar la dependencia de los resultados posteriores de los resultados anteriores.
En consecuencia, estas optimizaciones eliminan la causalidad en el software y aumentan la probabilidad de fallas, defectos y vulnerabilidades del software.
Específicamente con respecto a los valores indeterminados, el informe de defectos estándar C 451: La inestabilidad de las variables automáticas no inicializadas hace una lectura interesante. Todavía no se ha resuelto, pero introduce el concepto de valores tambaleantes, lo que significa que la indeterminación de un valor puede propagarse a través del programa y puede tener diferentes valores indeterminados en diferentes puntos del programa.
No conozco ningún ejemplo de dónde sucede esto, pero en este momento no podemos descartarlo.
Ejemplos reales, no el resultado que esperas
Es poco probable que obtenga valores aleatorios. Un compilador podría optimizar por completo el ciclo. Por ejemplo, con este caso simplificado:
void updateEffect(int arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r ;
}
}
clang lo optimiza ( verlo en vivo )
updateEffect(int*): # @updateEffect(int*)
retq
o tal vez obtenga todos los ceros, como con este caso modificado:
void updateEffect(int arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r%255 ;
}
}
verlo en vivo :
updateEffect(int*): # @updateEffect(int*)
xorps %xmm0, %xmm0
movups %xmm0, 64(%rdi)
movups %xmm0, 48(%rdi)
movups %xmm0, 32(%rdi)
movups %xmm0, 16(%rdi)
movups %xmm0, (%rdi)
retq
Ambos casos son formas perfectamente aceptables de comportamiento indefinido.
Tenga en cuenta que si estamos en un Itanium podríamos terminar con un valor de trampa :
[...] si el registro tiene un valor especial que no es nada, lea las trampas del registro, excepto por algunas instrucciones [...]
Otras notas importantes
Es interesante observar la variación entre gcc y clang notada en el proyecto de UB Canarias sobre cuán dispuestos están a aprovechar el comportamiento indefinido con respecto a la memoria no inicializada. Las notas del artículo ( énfasis mío ):
Por supuesto, debemos ser completamente claros con nosotros mismos de que cualquier expectativa no tiene nada que ver con el estándar del lenguaje y todo lo que tiene que ver con lo que un compilador en particular hace, ya sea porque los proveedores de ese compilador no están dispuestos a explotar esa UB o simplemente porque aún no han llegado a explotarlo . Cuando no existe una garantía real del proveedor del compilador, nos gusta decir que los UB aún no explotados son bombas de tiempo : están esperando para explotar el próximo mes o el próximo año cuando el compilador se vuelva un poco más agresivo.
Como Matthieu M. señala, lo que todo programador C debe saber sobre el comportamiento indefinido # 2/3 también es relevante para esta pregunta. Dice entre otras cosas ( énfasis mío ):
Lo importante y aterrador es darse cuenta de que casi cualquier
optimización basada en un comportamiento indefinido puede comenzar a activarse en un código defectuoso en cualquier momento en el futuro . La alineación, el desenrollado de bucles, la promoción de memoria y otras optimizaciones seguirán mejorando, y una parte importante de su razón para existir es exponer optimizaciones secundarias como las anteriores.
Para mí, esto es profundamente insatisfactorio, en parte porque el compilador inevitablemente termina siendo culpado, pero también porque significa que enormes cuerpos de código C son minas terrestres que esperan explotar.
Para completar, probablemente debería mencionar que las implementaciones pueden optar por hacer que el comportamiento indefinido esté bien definido, por ejemplo, gcc permite la escritura de tipos a través de uniones, mientras que en C ++ esto parece un comportamiento indefinido . Si este es el caso, la implementación debería documentarlo y, por lo general, esto no será portátil.