Pregunta ¿Por qué estos constructos (usando ++) son un comportamiento indefinido en C?


#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

711
2018-06-04 09:17


origen


Respuestas:


C tiene el concepto de comportamiento indefinido, es decir, algunos constructos de lenguaje son sintácticamente válidos, pero no se puede predecir el comportamiento cuando se ejecuta el código.

Hasta donde yo sé, el estándar no dice explícitamente por qué el concepto de comportamiento indefinido existe. En mi opinión, es simplemente porque los diseñadores del lenguaje querían que haya cierta flexibilidad en la semántica, en lugar de exigir que todas las implementaciones manejen el desbordamiento de enteros de la misma manera, lo que muy probablemente impondría costos de rendimiento serios, simplemente dejaron el comportamiento indefinido de modo que si escribe código que causa un desbordamiento de enteros, cualquier cosa puede suceder.

Entonces, con eso en mente, ¿por qué son estos "problemas"? El lenguaje dice claramente que ciertas cosas conducen a comportamiento indefinido. No hay problema, no hay "debería" involucrado. Si el comportamiento indefinido cambia cuando se declara una de las variables involucradas volatileeso no prueba ni cambia nada Es indefinido; no puedes razonar sobre el comportamiento.

Su ejemplo más interesante, el que tiene

u = (u++);

es un ejemplo de libro de texto de comportamiento indefinido (ver la entrada de Wikipedia en puntos de secuencia)


513
2018-06-04 09:20



Simplemente compile y desmonte su línea de código, si está tan interesado en saber exactamente cómo obtiene lo que está obteniendo.

Esto es lo que obtengo en mi máquina, junto con lo que creo que está sucediendo:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Yo ... supongo que la instrucción 0x00000014 fue algún tipo de optimización del compilador?)


75
2018-05-24 13:26



Creo que las partes relevantes del estándar C99 son 6.5 Expressions, §2

Entre el punto de secuencia anterior y siguiente, un objeto tendrá su valor almacenado   modificado como máximo una vez por la evaluación de una expresión. Además, el valor anterior   se leerá solo para determinar el valor que se almacenará.

y 6.5.16 Operadores de Asignación, §4:

El orden de evaluación de los operandos no está especificado. Si se intenta modificar   el resultado de un operador de asignación o para acceder a él después del siguiente punto de secuencia, el   el comportamiento no está definido


55
2018-06-04 09:35



El comportamiento no puede ser explicado porque invoca ambos comportamiento no especificado y comportamiento indefinido, por lo que no podemos hacer predicciones generales sobre este código, aunque si lee Olve Maudal's trabajo como Deep C y Sin especificar y sin definir a veces puede hacer buenas suposiciones en casos muy específicos con un compilador y entorno específico, pero no haga eso en ningún lugar cerca de la producción.

Así que pasar a comportamiento no especificado, en proyecto de norma c99 sección6.5 párrafo 3 dice(énfasis mío)

La agrupación de operadores y operandos se indica mediante la sintaxis.74) Excepto lo especificado   más tarde (para los operadores function-call (), &&, ||,?:, y comma), el orden de evaluación de las subexpresiones y el orden en que se producen los efectos secundarios no están especificados.

Entonces cuando tenemos una línea como esta:

i = i++ + ++i;

no sabemos si i++ o ++i será evaluado primero. Esto es principalmente para dar al compilador mejores opciones para la optimización.

También tenemos comportamiento indefinido aquí también, ya que el programa está modificando variables (i, u, etc.) más de una vez entre puntos de secuencia. Desde el borrador de la sección estándar 6.5 párrafo 2(énfasis mío)

Entre el punto de secuencia anterior y siguiente, un objeto tendrá su valor almacenado   modificado como máximo una vez por la evaluación de una expresión. Además, el valor anterior   se leerá solo para determinar el valor que se almacenará.

cita los siguientes ejemplos de código como no definidos:

i = ++i + 1;
a[i++] = i; 

En todos estos ejemplos, el código está intentando modificar un objeto más de una vez en el mismo punto de secuencia, que terminará con el ; en cada uno de estos casos:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Comportamiento no especificado se define en el proyecto de norma c99 en la sección 3.4.4 como:

uso de un valor no especificado u otro comportamiento donde este Estándar Internacional proporciona   dos o más posibilidades e impone ningún requisito adicional sobre el cual se elija en   ejemplo

y comportamiento indefinido se define en la sección 3.4.3 como:

comportamiento, al usar una construcción de programa errónea o noportable o de datos erróneos,   para lo cual esta Norma Internacional no impone requisitos

y señala que:

El comportamiento indefinido varía desde ignorar completamente la situación con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico), hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).


42
2017-08-15 19:25



La mayoría de las respuestas aquí citadas del estándar C enfatizan que el comportamiento de estos constructos no está definido. Comprender por qué el comportamiento de estos constructos no está definido, comprendamos estos términos primero a la luz del estándar C11:

Secuenciado: (5.1.2.3)

Dadas dos evaluaciones A y B, Si A está secuenciado antes B, luego la ejecución de A deberá preceder a la ejecución de B.

Sin secuencia:

Si A no está secuenciado antes o después B, entonces A y B no son secuenciados

Las evaluaciones pueden ser una de dos cosas:

  • cómputos de valor, que resuelven el resultado de una expresión; y
  • efectos secundarios, que son modificaciones de objetos.

Punto de secuencia:

La presencia de un punto de secuencia entre la evaluación de expresiones A y B implica que cada cómputo de valor y efecto secundario asociado con A está secuenciado antes de cada cómputo de valor y efecto secundario asociado con B.

Ahora llegando a la pregunta, para las expresiones como

int i = 1;
i = i++;

el estándar dice que:

6.5 Expresiones:

Si un efecto secundario en un objeto escalar no se está secuenciando en relación conya sea un efecto secundario diferente en el mismo objeto escalar o un cálculo de valor usando el valor del mismo objeto escalar, el comportamiento no está definido. [...]

Por lo tanto, la expresión anterior invoca UB porque dos efectos secundarios en el mismo objeto i es no secuenciado en relación el uno con el otro. Eso significa que no se secuencia si el efecto secundario por asignación a i se hará antes o después del efecto secundario por ++.
Dependiendo de si la asignación ocurre antes o después del incremento, se producirán diferentes resultados y ese es el caso de comportamiento indefinido.

Vamos a renombrar el i a la izquierda de la asignación se il y a la derecha de la asignación (en la expresión i++) ser ir, entonces la expresión será como

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Un punto importante con respecto a Postfix ++ operador es eso:

solo porque el ++ viene después de la variable no significa que el incremento se produce tarde. El incremento puede ocurrir tan pronto como el compilador le gusta siempre que el compilador asegure que se usa el valor original.

Significa la expresión il = ir++ podría ser evaluado ya sea como

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

o

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

dando como resultado dos resultados diferentes 1 y 2 que depende de la secuencia de efectos secundarios por asignación y ++ y por lo tanto invoca a UB.


41
2018-06-27 00:27



Otra forma de responder a esto, en lugar de empantanarse en detalles arcanos de puntos de secuencia y comportamiento indefinido, es simplemente preguntar, ¿Qué se supone que quieren decir?  ¿Qué estaba tratando de hacer el programador?

El primer fragmento sobre el que se preguntó, i = i++ + ++i, es bastante claramente loco en mi libro. Nadie lo escribiría en un programa real, no es obvio qué es lo que hace, no hay ningún algoritmo concebible que alguien pueda haber estado tratando de codificar que hubiera resultado en esta particular secuencia artificial de operaciones. Y como no es obvio para ti y para mí lo que se supone que debe hacer, está bien en mi libro si el compilador tampoco puede descubrir qué se supone que debe hacer.

El segundo fragmento i = i++, es un poco más fácil de entender Alguien claramente está tratando de incrementar i, y asignarle el resultado a i. Pero hay algunas maneras de hacerlo en C. La forma más básica de agregar 1 a i y asignarle el resultado a i es la misma en casi cualquier lenguaje de programación:

i = i + 1

C, por supuesto, tiene un atajo práctico:

i++

Esto significa, "agregue 1 a i, y asigne el resultado a i". Entonces, si construimos un batiburrillo de los dos, escribiendo

i = i++

lo que realmente estamos diciendo es "agregue 1 a i, y asigne el resultado a i, y asigne el resultado nuevamente a i". Estamos confundidos, así que no me molesta demasiado si el compilador también se confunde.

Siendo realistas, la única vez que se escriben estas locas expresiones es cuando las personas las usan como ejemplos artificiales de cómo se supone que ++ debe funcionar. Y, por supuesto, es importante entender cómo funciona ++. Pero una regla práctica para usar ++ es: "Si no es obvio qué significa una expresión usando ++, no la escriba".

Solíamos pasar innumerables horas en comp.lang.c discutiendo expresiones como estas y por qué no están definidos Dos de mis respuestas más largas, que intentan explicar realmente por qué, están archivadas en la web:


27
2018-06-18 11:55



Si bien es poco probable que los compiladores y procesadores lo hagan, sería legal, bajo el estándar C, que el compilador implemente "i ++" con la secuencia:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

Aunque no creo que ningún procesador admita el hardware para permitir que tal cosa se haga de manera eficiente, uno puede imaginar fácilmente situaciones en las que dicho comportamiento haría que el código multiproceso sea más fácil (por ejemplo, garantizaría que si dos hilos intentan realizar lo anterior secuencia simultáneamente i se incrementaría en dos) y no es totalmente inconcebible que algún procesador futuro pueda proporcionar una característica como esa.

Si el compilador fuera a escribir i++ como se indicó anteriormente (legal bajo el estándar) y fueron para intercalar las instrucciones anteriores a lo largo de la evaluación de la expresión general (también legal), y si no pasó a notar que una de las otras instrucciones pasó a tener acceso i, sería posible (y legal) que el compilador genere una secuencia de instrucciones que bloquearía. Para estar seguro, un compilador casi con certeza detectaría el problema en el caso donde la misma variable i se usa en ambos lugares, pero si una rutina acepta referencias a dos punteros p y qy usa (*p) y (*q) en la expresión anterior (en lugar de usar i dos veces) el compilador no debería reconocer ni evitar el punto muerto que ocurriría si se pasara la misma dirección del objeto para ambos p y q.


22
2017-12-05 18:30



A menudo, esta pregunta está vinculada como un duplicado de preguntas relacionadas con códigos como

printf("%d %d\n", i, i++);

o

printf("%d %d\n", ++i, i++);

o variantes similares.

Si bien esto también es comportamiento indefinido como ya se dijo, hay diferencias sutiles cuando printf() está involucrado cuando se compara con una declaración como:

   x = i++ + i++;

En la siguiente declaración:

printf("%d %d\n", ++i, i++);

el orden de evaluación de argumentos en printf() es no especificado. Eso significa expresiones i++ y ++i podría ser evaluado en cualquier orden C11 estándar tiene algunas descripciones relevantes sobre esto:

Anexo J, comportamientos no especificados

El orden en que el designador de función, los argumentos y   subexpresiones dentro de los argumentos se evalúan en una llamada de función   (6.5.2.2).

3.4.4, comportamiento no especificado

El uso de un valor no especificado u otro comportamiento donde este   Norma Internacional proporciona dos o más posibilidades e impone   no se requieren más requisitos en ningún caso.

EJEMPLO Un ejemplo de comportamiento no especificado es el orden en el cual el   argumentos para una función son evaluados.

los comportamiento no especificado en sí mismo NO es un problema. Considera este ejemplo:

printf("%d %d\n", ++x, y++);

Esto también tiene comportamiento no especificado porque el orden de evaluación de ++x y y++ no está especificado Pero es una declaración perfectamente legal y válida. Hay no comportamiento indefinido en esta declaración. Porque las modificaciones (++x y y++) están hechos para distinto objetos.

Lo que hace la siguiente declaración

printf("%d %d\n", ++i, i++);

como comportamiento indefinido es el hecho de que estas dos expresiones modifican la mismo objeto i sin una intervención punto de secuencia.


Otro detalle es que el coma involucrado en la llamada printf () es una separador, no la operador de coma.

Esta es una distinción importante porque el operador de coma no introduce un punto de secuencia entre la evaluación de sus operandos, lo que hace que lo siguiente sea legal:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

El operador de coma evalúa sus operandos de izquierda a derecha y solo arroja el valor del último operando. Entonces en j = (++i, i++);, ++i incrementos i a 6 y i++ produce un valor antiguo de i (6) que está asignado a j. Entonces i se convierte 7 debido al incremento posterior

Entonces si el coma en la llamada a la función eran ser un operador de coma y luego

printf("%d %d\n", ++i, i++);

no será un problema Pero invoca comportamiento indefinido porque el coma aquí hay un separador.


Para aquellos que son nuevos en comportamiento indefinido se beneficiaría de la lectura Lo que todo programador de C debe saber sobre el comportamiento indefinido  para entender el concepto y muchas otras variantes de comportamiento indefinido en C.

Esta publicación: Comportamiento indefinido, no especificado y definido por la implementación también es relevante.


16
2017-12-30 20:26



El estándar C dice que una variable solo se debe asignar como máximo una vez entre dos puntos de secuencia. Un punto y coma, por ejemplo, es un punto de secuencia.
Entonces cada declaración del formulario:

i = i++;
i = i++ + ++i;

y así sucesivamente violar esa regla. El estándar también dice que el comportamiento no está definido y no está definido. Algunos compiladores detectan estos y producen algunos resultados, pero esto no es por norma.

Sin embargo, dos variables diferentes pueden incrementarse entre dos puntos de secuencia.

while(*src++ = *dst++);

Lo anterior es una práctica de codificación común al copiar / analizar cadenas.


13
2017-09-11 12:36