¿Por qué estas construcciones utilizan un comportamiento indefinido previo y posterior al incremento?
#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?
}
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 ).
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
A
yB
, siA
se secuencia antesB
, la ejecución deA
precederá a la ejecución deB
.
Sin secuenciar:
Si
A
no está secuenciado antes o despuésB
, entoncesA
yB
no 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
A
yB
implica que cada cálculo de valor y efecto secundario asociado conA
se secuencia antes de cada cálculo de valor y efecto secundario asociado conB
.
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 i
no están secuenciados entre sí. Eso significa que no está secuenciado si el efecto secundario por asignación se i
realizará 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 i
a la izquierda de la asignación a il
y 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 1
y 2
que depende de la secuencia de efectos secundarios por asignación y, ++
por lo tanto, invoca UB.
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.
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?)
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. ++i
Esto 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
,, u
etc.) más de una vez entre puntos de secuencia . Del borrador de la sección estándar, 6.5
pá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.4
como:
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.3
como:
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).