¿Por qué estas construcciones utilizan un comportamiento indefinido previo y posterior al incremento?

Resuelto PiX asked hace 15 años • 15 respuestas
#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\n", ++w, w); // shouldn't this print 1 1

   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?
}
PiX avatar Jun 04 '09 16:06 PiX
Aceptado

C tiene el concepto de comportamiento indefinido, es decir, algunas construcciones del lenguaje son sintácticamente válidas 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é existe el concepto de comportamiento indefinido. En mi opinión, es simplemente porque los diseñadores del lenguaje querían que hubiera cierta libertad en la semántica, en lugar de exigir que todas las implementaciones manejen el desbordamiento de enteros exactamente de la misma manera, lo que muy probablemente impondría serios costos de rendimiento, simplemente dejaron el comportamiento. indefinido para que si escribe código que provoca un desbordamiento de enteros, pueda pasar cualquier cosa.

Entonces, con eso en mente, ¿por qué surgen estos "problemas"? El lenguaje dice claramente que ciertas cosas conducen a un comportamiento indefinido . No hay ningún problema, no hay ningún "debería" involucrado. Si el comportamiento indefinido cambia cuando se declara una de las variables involucradas volatile, eso no prueba ni cambia nada. No está definido ; 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 (consulte la entrada de Wikipedia sobre puntos de secuencia ).

unwind avatar Jun 04 '2009 09:06 unwind

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

Secuenciado: (5.1.2.3)

Dadas dos evaluaciones cualesquiera Ay B, si Ase secuencia antes B, la ejecución de Aprecederá a la ejecución de B.

Sin secuenciar:

Si Ano está secuenciado antes o después B, entonces Ay Bno están secuenciados.

Las evaluaciones pueden ser una de dos cosas:

  • cálculos de valores , que calculan 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 Ay Bimplica que cada cálculo de valor y efecto secundario asociado con Ase secuencia antes de cada cálculo de valor y efecto secundario asociado con B.

Ahora llegando a la pregunta, para expresiones como

int i = 1;
i = i++;

estándar dice que:

6.5 Expresiones:

Si un efecto secundario en un objeto escalar no está secuenciado en relación con un efecto secundario diferente en el mismo objeto escalar o un cálculo de valor utilizando 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 ino están secuenciados entre sí. Eso significa que no está secuenciado si el efecto secundario por asignación se irealizará 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 del comportamiento indefinido .

Cambiemos el nombre de ia la izquierda de la asignación a ily a la derecha de la asignación (en la expresión i++) a ir, luego 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 al operador Postfix ++es que:

Sólo porque ++viene después de la variable no significa que el incremento ocurra tarde . El incremento puede ocurrir tan pronto como el compilador lo desee, siempre y cuando el compilador garantice que se utiliza el valor original .

Significa que la expresión il = ir++podría evaluarse 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  

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

haccks avatar Jun 27 '2015 00:06 haccks

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

Entre el punto de secuencia anterior y el siguiente, el valor almacenado de un objeto deberá modificarse como máximo una vez mediante la evaluación de una expresión. Además, el valor anterior se leerá únicamente 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 acceder a él después del siguiente punto de secuencia, el comportamiento no está definido.

Christoph avatar Jun 04 '2009 09:06 Christoph

Simplemente compila y desensambla tu línea de código, si estás dispuesto a saber exactamente cómo obtienes lo que estás obteniendo.

Esto es lo que aparece en mi máquina, junto con lo que creo que está pasando:

$ 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.

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

badp avatar May 24 '2010 13:05 badp

El comportamiento realmente no se puede explicar porque invoca tanto un comportamiento no especificado como un comportamiento indefinido , por lo que no podemos hacer ninguna predicción general sobre este código, aunque si lees el trabajo de Olve Maudal , como Deep C y Unspecified and Undefinido, a veces puedes hacer algo bueno. conjeturas en casos muy específicos con un compilador y un entorno específicos, pero no lo haga cerca de la producción.

Pasando al comportamiento no especificado , en el borrador de la sección estándar c996.5 , el párrafo 3 dice ( el énfasis es mío ):

La agrupación de operadores y operandos se indica mediante la sintaxis.74) Excepto lo que se especifica más adelante (para la llamada de función (), &&, ||, ?: y operadores de coma), el orden de evaluación de las subexpresiones y el orden en Los efectos secundarios que se producen no están especificados.

Entonces cuando tenemos una línea como esta:

i = i++ + ++i;

No sabemos si será evaluado i++o será primero. ++iEsto es principalmente para darle al compilador mejores opciones de optimización .

También tenemos un comportamiento indefinido aquí ya que el programa modifica variables ( i,, uetc.) más de una vez entre puntos de secuencia . Del borrador de la sección estándar, 6.5párrafo 2 ( énfasis mío ):

Entre el punto de secuencia anterior y el siguiente, el valor almacenado de un objeto deberá modificarse como máximo una vez mediante la evaluación de una expresión. Además, el valor anterior se leerá únicamente para determinar el valor que se almacenará .

cita los siguientes ejemplos de código como indefinidos:

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

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

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

El comportamiento no especificado se define en el borrador del estándar c99 en la sección 3.4.4como:

uso de un valor no especificado, u otro comportamiento donde esta Norma Internacional proporciona dos o más posibilidades y no impone requisitos adicionales sobre cuál se elige en cualquier caso

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

comportamiento, tras el uso de una construcción de programa no portátil o errónea o de datos erróneos, para los cuales esta Norma Internacional no impone requisitos

y señala que:

El posible comportamiento indefinido varía desde ignorar completamente la situación con resultados impredecibles, hasta comportarse durante la traducción o 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 finalizar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).

Shafik Yaghmour avatar Aug 15 '2013 19:08 Shafik Yaghmour