Undefined behavior
Hace que todo el programa carezca de sentido si se violan ciertas reglas del lenguaje.
Contenidos |
Explicación
El estándar de C++ define precisamente el comportamiento observable de cada programa de C++ que no cae en una de las siguientes categorías:
- ill-formed - El programa tiene errores de sintaxis o errores semánticos diagnosticables.
-
- Se requiere que un compilador conforme de C++ emita un diagnóstico, incluso si define una extensión del lenguaje que asigna significado a dicho código (como con arreglos de longitud variable).
- El texto del estándar utiliza shall , shall not , y ill-formed para indicar estos requisitos.
- mal formado, no se requiere diagnóstico - El programa tiene errores semánticos que pueden no ser diagnosticables en el caso general (por ejemplo, violaciones de la ODR u otros errores que solo son detectables en tiempo de enlace).
-
- El comportamiento es indefinido si se ejecuta dicho programa.
- comportamiento definido por la implementación - El comportamiento del programa varía entre implementaciones, y la implementación conforme debe documentar los efectos de cada comportamiento.
-
- Por ejemplo, el tipo de std::size_t o el número de bits en un byte, o el texto de std::bad_alloc::what .
- Un subconjunto del comportamiento definido por la implementación es el comportamiento específico de la configuración regional , que depende de la configuración regional proporcionada por la implementación.
- comportamiento no especificado - El comportamiento del programa varía entre implementaciones, y la implementación conforme no está obligada a documentar los efectos de cada comportamiento.
-
- Por ejemplo, order of evaluation , si los string literals idénticos son distintos, la cantidad de sobrecarga en la asignación de arrays, etc.
- Cada comportamiento no especificado resulta en uno de un conjunto de resultados válidos.
|
(desde C++26) |
- comportamiento indefinido - No existen restricciones sobre el comportamiento del programa.
-
- Algunos ejemplos de comportamiento indefinido son carreras de datos, accesos a memoria fuera de los límites de un arreglo, desbordamiento de enteros con signo, desreferencia de puntero nulo, más de una modificación del mismo escalar en una expresión sin ningún punto de secuencia intermedio (hasta C++11) que no está secuenciada (desde C++11) , acceso a un objeto mediante un puntero de un tipo diferente , etc.
- Las implementaciones no están obligadas a diagnosticar comportamiento indefinido (aunque muchas situaciones simples son diagnosticadas), y el programa compilado no está obligado a hacer nada significativo.
|
(desde C++11) |
UB y optimización
Debido a que los programas correctos en C++ están libres de comportamiento indefinido, los compiladores pueden producir resultados inesperados cuando un programa que realmente tiene UB se compila con optimizaciones habilitadas:
Por ejemplo,
Desbordamiento con signo
int foo(int x) { return x + 1 > x; // puede ser verdadero o comportamiento indefinido debido al desbordamiento con signo }
puede compilarse como ( demo )
foo(int): mov eax, 1 ret
Acceso fuera de límites
int table[4] = {}; bool exists_in_table(int v) { // retornar verdadero en una de las primeras 4 iteraciones o UB debido a acceso fuera de límites for (int i = 0; i <= 4; i++) if (table[i] == v) return true; return false; }
Puede compilarse como ( demo )
exists_in_table(int): mov eax, 1 ret
Escalar no inicializado
std::size_t f(int x) { std::size_t a; if (x) // ya sea x distinto de cero o comportamiento indefinido a = 42; return a; }
Puede compilarse como ( demo )
f(int): mov eax, 42 ret
La salida mostrada se observó en una versión anterior de gcc
Salida posible:
p is true p is false
Escalar no válido
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // leer desde b ahora es comportamiento indefinido return b == 0; }
Puede compilarse como ( demo )
f(): mov eax, 11 ret
Desreferenciación de puntero nulo
Los ejemplos demuestran la lectura del resultado de desreferenciar un puntero nulo.
int foo(int* p) { int x = *p; if (!p) return x; // O bien hay UB arriba o esta rama nunca se ejecuta else return 0; } int bar() { int* p = nullptr; return *p; // UB incondicional }
puede compilarse como ( demo )
foo(int*): xor eax, eax ret bar(): ret
Acceso al puntero pasado a std::realloc
Elija clang para observar la salida mostrada
#include <cstdlib> #include <iostream> int main() { int* p = (int*)std::malloc(sizeof(int)); int* q = (int*)std::realloc(p, sizeof(int)); *p = 1; // UB access to a pointer that was passed to realloc *q = 2; if (p == q) // UB access to a pointer that was passed to realloc std::cout << *p << *q << '\n'; }
Salida posible:
12
Bucle infinito sin efectos secundarios
Elija clang o el gcc más reciente para observar la salida mostrada.
#include <iostream> bool fermat() { const int max_value = 1000; // Non-trivial infinite loop with no side effects is UB for (int a = 1, b = 1, c = 1; true; ) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return true; // disproved :() a++; if (a > max_value) { a = 1; b++; } if (b > max_value) { b = 1; c++; } if (c > max_value) c = 1; } return false; // not disproved } int main() { std::cout << "Fermat's Last Theorem "; fermat() ? std::cout << "has been disproved!\n" : std::cout << "has not been disproved.\n"; }
Salida posible:
Fermat's Last Theorem has been disproved!
Mal formado con mensaje de diagnóstico
Tenga en cuenta que los compiladores tienen permitido extender el lenguaje de maneras que den significado a programas mal formados. Lo único que el estándar de C++ requiere en tales casos es un mensaje de diagnóstico (advertencia del compilador), a menos que el programa fuera "ill-formed no diagnostic required".
Por ejemplo, a menos que las extensiones de lenguaje estén deshabilitadas mediante
--pedantic-errors
, GCC compilará el siguiente ejemplo
solo con una advertencia
aunque
aparece en el estándar de C++
como un ejemplo de un "error" (ver también
GCC Bugzilla #55783
)
#include <iostream> // Ejemplo de ajuste, no usar constante double a{1.0}; // Estándar C++23, §9.4.5 Inicialización de lista [dcl.init.list], Ejemplo #6: struct S { // sin constructores de lista de inicialización S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK, invoca #1 S s2{a, 2, 3}; // error: narrowing S s3{}; // OK, invoca #2 // — fin del ejemplo] S::S(int, double, double) {} S::S() {} int main() { std::cout << "All checks have passed.\n"; }
Salida posible:
main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
^
static_cast<int>( )
1 error generated.
Referencias
| Contenido extendido |
|---|
|
Véase también
[[
assume
(
expression
)]]
(C++23)
|
especifica que la
expresión
siempre se evaluará como
true
en un punto dado
(especificador de atributo) |
[[
indeterminate
]]
(C++26)
|
especifica que un objeto tiene un valor indeterminado si no está inicializado
(especificador de atributo) |
|
(C++23)
|
marca un punto de ejecución inalcanzable
(función) |
|
Documentación de C
para
Comportamiento indefinido
|
|