¿Cuáles son todos los comportamientos indefinidos comunes que un programador de C ++ debe conocer? [cerrado]

201

¿Cuáles son todos los comportamientos indefinidos comunes que un programador de C ++ debe conocer?

Diga, como:

a[i] = i++;

yesraaj
fuente
3
Estás seguro. Eso se ve bien definido.
Martin York
17
6.2.2 Orden de evaluación [expr.evaluation] en el lenguaje de programación C ++, dígalo. No tengo ninguna otra referencia
yesraaj
44
Tiene razón ... solo miró 6.2.2 en el lenguaje de programación C ++ y dice v [i] = i ++ no está definido
dancavallaro
44
Me imagino porque el comiler hace ejecutar el i ++ antes o después de calcular la ubicación de la memoria de v [i]. claro, siempre me asignarán allí. pero podría escribir en v [i] o v [i + 1] dependiendo del orden de las operaciones ..
Evan Teran
2
Todo lo que dice el lenguaje de programación C ++ es "El orden de las operaciones de subexpresiones dentro de una expresión es indefinido. En particular, no se puede suponer que la expresión se evalúa de izquierda a derecha".
dancavallaro

Respuestas:

233

Puntero

  • Desreferenciar un NULLpuntero
  • Desreferenciar un puntero devuelto por una "nueva" asignación de tamaño cero
  • Usar punteros a objetos cuya vida útil ha finalizado (por ejemplo, apilar objetos asignados u objetos eliminados)
  • Desreferenciar un puntero que aún no se ha inicializado definitivamente
  • Realización de aritmética de puntero que produce un resultado fuera de los límites (arriba o abajo) de una matriz.
  • Desreferenciar el puntero en una ubicación más allá del final de una matriz.
  • Convertir punteros a objetos de tipos incompatibles
  • Utilizando memcpypara copiar buffers superpuestos .

El buffer se desborda

  • Lectura o escritura en un objeto o matriz en un desplazamiento que es negativo o superior al tamaño de ese objeto (desbordamiento de pila / montón)

Desbordamientos de enteros

  • Desbordamiento de entero firmado
  • Evaluar una expresión que no está matemáticamente definida
  • Valores de desplazamiento a la izquierda en una cantidad negativa (los desplazamientos a la derecha en cantidades negativas están definidos por la implementación)
  • Cambio de valores en una cantidad mayor o igual al número de bits en el número (por ejemplo, int64_t i = 1; i <<= 72no está definido)

Tipos, Reparto y Const

  • Convertir un valor numérico en un valor que no puede ser representado por el tipo de destino (ya sea directamente o mediante static_cast)
  • Usar una variable automática antes de que se haya asignado definitivamente (por ejemplo, int i; i++; cout << i;)
  • Usar el valor de cualquier objeto de tipo que no sea volatileosig_atomic_t al recibir una señal
  • Intentar modificar un literal de cadena o cualquier otro objeto constante durante su vida útil
  • Concatenando un literal de cadena estrecho con ancho durante el preprocesamiento

Función y plantilla

  • No devolver un valor de una función de retorno de valor (directamente o saliendo de un bloque try)
  • Múltiples definiciones diferentes para la misma entidad (clase, plantilla, enumeración, función en línea, función miembro estática, etc.)
  • Infinita recursividad en la creación de instancias de plantillas
  • Llamar a una función usando diferentes parámetros o enlaces a los parámetros y enlaces que la función se define como usando.

OOP

  • Destrucciones en cascada de objetos con duración de almacenamiento estático
  • El resultado de asignar objetos parcialmente superpuestos
  • Reingresar recursivamente una función durante la inicialización de sus objetos estáticos
  • Realizar llamadas a funciones virtuales a funciones virtuales puras de un objeto desde su constructor o destructor
  • Refiriéndose a miembros no estáticos de objetos que no han sido construidos o ya han sido destruidos

Archivo fuente y preprocesamiento

  • Un archivo fuente no vacío que no termina con una nueva línea o termina con una barra diagonal inversa (antes de C ++ 11)
  • Una barra diagonal inversa seguida de un carácter que no forma parte de los códigos de escape especificados en un carácter o cadena constante (esto se define en la implementación en C ++ 11).
  • Exceder los límites de implementación (número de bloques anidados, número de funciones en un programa, espacio de pila disponible ...)
  • Valores numéricos del preprocesador que no se pueden representar mediante un long int
  • Directiva de preprocesamiento en el lado izquierdo de una definición de macro similar a una función
  • Generando dinámicamente el token definido en una #ifexpresión

Para ser clasificado

  • Llamar a la salida durante la destrucción de un programa con duración de almacenamiento estático
Diomidis Spinellis
fuente
Hm ... NaN (x / 0) e Infinity (0/0) estaban cubiertos por el IEE 754, si C ++ se diseñó más tarde, ¿por qué registra x / 0 como indefinido?
nuevo123456
Re: "Una barra invertida seguida de un carácter que no es parte de los códigos de escape especificados en un carácter o cadena constante". Eso es UB en C89 (§3.1.3.4) y C ++ 03 (que incorpora C89), pero no en C99. C99 dice que "el resultado no es un token y se requiere un diagnóstico" (§6.4.4.4). Presumiblemente, C ++ 0x (que incorpora C89) será el mismo.
Adam Rosenfield
1
El estándar C99 tiene una lista de comportamientos indefinidos en el apéndice J.2. Se necesitaría algo de trabajo para adaptar esta lista a C ++. Tendría que cambiar las referencias a las cláusulas C ++ correctas en lugar de las cláusulas C99, eliminar todo lo irrelevante y también verificar si todas esas cosas realmente no están definidas en C ++ y C. Pero proporciona un comienzo.
Steve Jessop
1
@ new123456: no todas las unidades de coma flotante son compatibles con IEE754. Si C ++ requería el cumplimiento de IEE754, los compiladores tendrían que probar y manejar el caso donde el RHS es cero a través de una verificación explícita. Al hacer que el comportamiento sea indefinido, el compilador puede evitar esa sobrecarga diciendo "si usa una FPU que no sea IEE754, no obtendrá el comportamiento de IEEE754 FPU".
SecurityMatt
1
"Evaluación de una expresión cuyo resultado no está en el rango de los tipos correspondientes" ... el desbordamiento de enteros está bien definido para los tipos integrales SIN FIRMAR, simplemente no con signo.
nacitar sevaht
31

El orden en que se evalúan los parámetros de la función es el comportamiento no especificado . (Esto no hará que su programa se bloquee, explote u ordene pizza ... a diferencia del comportamiento indefinido ).

El único requisito es que todos los parámetros deben evaluarse completamente antes de llamar a la función.


Esta:

// The simple obvious one.
callFunc(getA(),getB());

Puede ser equivalente a esto:

int a = getA();
int b = getB();
callFunc(a,b);

O esto:

int b = getB();
int a = getA();
callFunc(a,b);

Puede ser cualquiera; depende del compilador. El resultado puede importar, dependiendo de los efectos secundarios.

Martin York
fuente
23
El orden no está especificado, no está definido.
Rob Kennedy,
1
Odio este :) Perdí un día de trabajo una vez que rastreé uno de estos casos ... de todos modos aprendí mi lección y afortunadamente no volví a caer
Robert Gould
2
@Rob: Discutiría con usted sobre el cambio de significado aquí, pero sé que el comité de estándares es muy exigente con la definición exacta de estas dos palabras. Así que lo cambiaré :-)
Martin York
2
Tuve suerte con este. Me mordió cuando estaba en la universidad y tuve un profesor que lo examinó y me contó mi problema en unos 5 segundos. No sé cuánto tiempo habría desperdiciado la depuración de lo contrario.
Bill the Lizard
27

El compilador es libre de reordenar las partes de evaluación de una expresión (suponiendo que el significado no haya cambiado).

De la pregunta original:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Doble comprobación de bloqueo. Y un error fácil de cometer.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.
Martin York
fuente
¿Qué se entiende por punto de secuencia?
yesraaj
1
Ooh ... eso es desagradable, especialmente desde que vi esa estructura exacta recomendada en Java
Tom
Tenga en cuenta que algunos compiladores definen el comportamiento en esta situación. En VC ++ 2005+, por ejemplo, si a es volátil, las barreras de memoria necesarias se configuran para evitar el reordenamiento de instrucciones para que funcione el bloqueo de doble verificación.
Eclipse
Martin York: <i> // (c) está garantizado que sucederá después de (a) y (b) </i> ¿Es así? Es cierto que en ese ejemplo en particular, el único escenario en el que podría importar sería si 'i' fuera una variable volátil asignada a un registro de hardware, y un [i] (antiguo valor de 'i') tuviera un alias, pero ¿hay alguna ¿garantiza que el incremento ocurrirá antes de un punto de secuencia?
supercat
5

Mi favorita es "La recursión infinita en la creación de instancias de plantillas" porque creo que es la única en la que el comportamiento indefinido ocurre en el momento de la compilación.

Daniel Earwicker
fuente
Hecho esto antes, pero no veo cómo está indefinido. Es bastante obvio que estás haciendo una recursión infinita en el último momento.
Robert Gould
El problema es que el compilador no puede examinar su código y decidir con precisión si sufrirá una recursión infinita o no. Es una instancia del problema de detención. Ver: stackoverflow.com/questions/235984/…
Daniel Earwicker
Sí, definitivamente es un problema detenido
Robert Gould
hizo que mi sistema se bloqueara debido al intercambio causado por muy poca memoria.
Johannes Schaub - litb
2
Las constantes del preprocesador que no encajan en un int también son tiempos de compilación.
Joshua
5

Asignación a una constante después de despojar constusando const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
yesraaj
fuente
5

Además del comportamiento indefinido , también existe el comportamiento igualmente desagradable definido por la implementación .

El comportamiento indefinido ocurre cuando un programa hace algo cuyo resultado no está especificado por el estándar.

El comportamiento definido por la implementación es una acción de un programa cuyo resultado no está definido por el estándar, pero que la implementación debe documentar. Un ejemplo es "Literales de caracteres multibyte", de la pregunta de desbordamiento de pila ¿Hay un compilador de C que no puede compilar esto? .

El comportamiento definido por la implementación solo lo muerde cuando comienza a portar (¡pero actualizar a una nueva versión del compilador también es portar!)

Constantin
fuente
4

Las variables solo pueden actualizarse una vez en una expresión (técnicamente una vez entre puntos de secuencia).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
Martin York
fuente
De hecho al menos una vez entre dos puntos de secuencia.
Prasoon Saurav
2
@Prasoon: Creo que quisiste decir: como máximo una vez entre dos puntos de secuencia. :-)
Nawaz
3

Una comprensión básica de los diversos límites ambientales. La lista completa se encuentra en la sección 5.2.4.1 de la especificación C. Aquí hay algunos;

  • 127 parámetros en una definición de función
  • 127 argumentos en una llamada de función
  • 127 parámetros en una macro definición
  • 127 argumentos en una macro invocación
  • 4095 caracteres en una línea de origen lógica
  • 4095 caracteres en un literal de cadena de caracteres o literal de cadena ancha (después de la concatenación)
  • 65535 bytes en un objeto (solo en un entorno alojado)
  • 15 niveles de envejecimiento para # archivos incluidos
  • 1023 etiquetas de caso para una declaración de cambio (excluyendo aquellas para cualquier declaración de cambio anidado)

En realidad, me sorprendió un poco el límite de 1023 etiquetas de caso para una declaración de cambio, puedo prever que se supere con bastante facilidad para el código / lex / analizador generado.

Si se exceden estos límites, tiene un comportamiento indefinido (fallas, fallas de seguridad, etc.).

Correcto, sé que esto es de la especificación C, pero C ++ comparte estos soportes básicos.

RandomNickName42
fuente
9
Si alcanza estos límites, tiene más problemas que un comportamiento indefinido.
nuevo123456
Podría FÁCILMENTE exceder 65535 bytes en un objeto, como un STD :: vector
Demi
2

Utilizando memcpypara copiar entre regiones de memoria superpuestas. Por ejemplo:

char a[256] = {};
memcpy(a, a, sizeof(a));

El comportamiento no está definido de acuerdo con el Estándar C, que está incluido en el Estándar C ++ 03.

7.21.2.1 La función memcpy

Sinopsis

1 / #include void * memcpy (void * restrict s1, const void * restrict s2, size_t n);

Descripción

2 / La función memcpy copia n caracteres del objeto señalado por s2 en el objeto señalado por s1. Si la copia se lleva a cabo entre objetos que se superponen, el comportamiento es indefinido. Devuelve 3 La función memcpy devuelve el valor de s1.

7.21.2.2 La función memmove

Sinopsis

1 #include void * memmove (void * s1, const void * s2, size_t n);

Descripción

2 La función memmove copia n caracteres del objeto señalado por s2 en el objeto señalado por s1. La copia se realiza como si los n caracteres del objeto señalado por s2 se copiaran primero en una matriz temporal de n caracteres que no se superponen a los objetos señalados por s1 y s2, y luego los n caracteres de la matriz temporal se copian en El objeto señalado por s1. Devoluciones

3 La función memmove devuelve el valor de s1.

John Dibling
fuente
2

El único tipo para el que C ++ garantiza un tamaño es char. Y el tamaño es 1. El tamaño de todos los demás tipos depende de la plataforma.

JaredPar
fuente
¿No es eso para lo que sirve <cstdint>? Define tipos como uint16_6 y otros.
Jasper Bekkers
Sí, pero el tamaño de la mayoría de los tipos, por ejemplo, no está bien definido.
JaredPar
Además, cstdint aún no forma parte del estándar actual de c ++. vea boost / stdint.hpp para una solución actualmente portátil.
Evan Teran
Ese no es un comportamiento indefinido. El estándar dice que la plataforma conforme define los tamaños, en lugar del estándar que los define.
Daniel Earwicker
1
@JaredPar: es una publicación compleja con muchos hilos de conversación, así que lo resumí todo aquí . La conclusión es esta: "5. Para representar -2147483647 y +2147483647 en binario, necesita 32 bits".
John Dibling
2

Los objetos de nivel de espacio de nombres en unidades de compilación diferentes nunca deberían depender unos de otros para la inicialización, porque su orden de inicialización no está definido.

yesraaj
fuente