Namespaces
Variants

Undefined behavior

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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.
  • comportamiento erróneo - El comportamiento (incorrecto) que se recomienda que la implementación diagnostique.
  • El comportamiento erróneo es siempre consecuencia de código de programa incorrecto.
  • La evaluación de una expresión constante nunca resulta en comportamiento erróneo.
  • Si la ejecución contiene una operación especificada como teniendo comportamiento erróneo, se permite y recomienda a la implementación emitir un diagnóstico, y se le permite terminar la ejecución en un momento no especificado después de esa operación.
  • Una implementación puede emitir un diagnóstico si puede determinar que el comportamiento erróneo es alcanzable bajo un conjunto de suposiciones específicas de la implementación sobre el comportamiento del programa, lo que puede resultar en falsos positivos.
Ejemplos de comportamiento erróneo
#include <cassert>
#include <cstring>
void f()
{   
    int d1, d2;       // d1, d2 have erroneous values
    int e1 = d1;      // erroneous behavior
    int e2 = d1;      // erroneous behavior
    assert(e1 == e2); // holds
    assert(e1 == d1); // holds, erroneous behavior
    assert(e2 == d1); // holds, erroneous behavior
    std::memcpy(&d2, &d1, sizeof(int)); // no erroneous behavior, but
                                        // d2 has an erroneous value
    assert(e1 == d2); // holds, erroneous behavior
    assert(e2 == d2); // holds, erroneous behavior
}
unsigned char g(bool b)
{
    unsigned char c;     // c has erroneous value
    unsigned char d = c; // no erroneous behavior, but d has an erroneous value
    assert(c == d);      // holds, both integral promotions have erroneous behavior
    int e = d;           // erroneous behavior
    return b ? d : 0;    // erroneous behavior if b is true
}
(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.
  • comportamiento indefinido en tiempo de ejecución - El comportamiento que no está definido excepto cuando ocurre durante la evaluación de una expresión como una expresión constante principal .
(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

#include <cstdio>
int main()
{
    bool p; // uninitialized local variable
    if (p)  // UB access to uninitialized scalar
        std::puts("p is true");
    if (!p) // UB access to uninitialized scalar
        std::puts("p is false");
}

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
  • Estándar C++23 (ISO/IEC 14882:2024):
  • 3.25 programa mal formado [defns.ill.formed]
  • 3.26 comportamiento definido por la implementación [defns.impl.defined]
  • 3.66 comportamiento no especificado [defns.unspecified]
  • 3.68 programa bien formado [defns.well.formed]
  • Estándar C++20 (ISO/IEC 14882:2020):
  • TBD programa mal formado [defns.ill.formed]
  • TBD comportamiento definido por la implementación [defns.impl.defined]
  • TBD comportamiento no especificado [defns.unspecified]
  • TBD programa bien formado [defns.well.formed]
  • Estándar C++17 (ISO/IEC 14882:2017):
  • TBD programa mal formado [defns.ill.formed]
  • TBD comportamiento definido por la implementación [defns.impl.defined]
  • TBD comportamiento no especificado [defns.unspecified]
  • TBD programa bien formado [defns.well.formed]
  • Estándar C++14 (ISO/IEC 14882:2014):
  • TBD programa mal formado [defns.ill.formed]
  • TBD comportamiento definido por la implementación [defns.impl.defined]
  • TBD comportamiento no especificado [defns.unspecified]
  • TBD programa bien formado [defns.well.formed]
  • Estándar C++11 (ISO/IEC 14882:2011):
  • TBD programa mal formado [defns.ill.formed]
  • TBD comportamiento definido por la implementación [defns.impl.defined]
  • TBD comportamiento no especificado [defns.unspecified]
  • TBD programa bien formado [defns.well.formed]
  • Estándar C++98 (ISO/IEC 14882:1998):
  • TBD programa mal formado [defns.ill.formed]
  • TBD comportamiento definido por la implementación [defns.impl.defined]
  • TBD comportamiento no especificado [defns.unspecified]
  • TBD programa bien formado [defns.well.formed]

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)
(C++26)
especifica que un objeto tiene un valor indeterminado si no está inicializado
(especificador de atributo)
marca un punto de ejecución inalcanzable
(función)
Documentación de C para Comportamiento indefinido

Enlaces externos

1. Blog del Proyecto LLVM: Lo Que Todo Programador de C Debe Saber Sobre Comportamiento No Definido #1/3
2. Blog del Proyecto LLVM: Lo Que Todo Programador de C Debe Saber Sobre Comportamiento No Definido #2/3
3. Blog del Proyecto LLVM: Lo Que Todo Programador de C Debe Saber Sobre Comportamiento No Definido #3/3
4. El comportamiento no definido puede resultar en viajes en el tiempo (entre otras cosas, pero los viajes en el tiempo son los más extraños)
5. Entendiendo el Desbordamiento de Enteros en C/C++
6. Diversión con Punteros NULL, parte 1 (explotación local en Linux 2.6.30 causada por CNB debido a desreferencia de puntero nulo)
7. Comportamiento No Definido y el Último Teorema de Fermat
8. Guía del programador de C++ sobre comportamiento no definido