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

815
#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
fuente
12
@Jarett, no, solo necesitaba algunos punteros para "secuenciar puntos". Mientras trabajaba encontré un fragmento de código con i = i ++, pensé "Esto no está modificando el valor de i". Probé y me pregunté por qué. Desde entonces, eliminé esta declaración y la reemplacé por i ++;
PiX
198
Creo que es interesante que todos SIEMPRE asuman que se hacen preguntas como esta porque el autor de la pregunta quiere UTILIZAR la construcción en cuestión. Mi primera suposición fue que PiX sabe que estos son malos, pero es curioso por qué se comportan de la manera que lo hacen en el compilador que estaba usando ... Y sí, lo que dijo UnWind ... no está definido, podría hacer cualquier cosa. .. incluyendo JCF (Jump and Catch Fire)
Brian Postow
32
Tengo curiosidad: ¿por qué los compiladores no advierten sobre construcciones como "u = u ++ + ++ u;" si el resultado es indefinido?
Aprenda OpenGL ES
55
(i++)todavía se evalúa a 1, independientemente de los paréntesis
Drew McGowen
2
Lo que sea i = (i++); lo que se pretendía hacer, ciertamente hay una forma más clara de escribirlo. Eso sería cierto incluso si estuviera bien definido. Incluso en Java, que define el comportamiento de i = (i++);, sigue siendo un código incorrecto. Solo escribei++;
Keith Thompson

Respuestas:

566

C tiene el concepto de comportamiento indefinido, es decir, algunas construcciones de lenguaje son sintácticamente válidas pero no 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 de lenguaje querían que hubiera un margen de maniobra en la semántica, en lugar de exigir que todas las implementaciones manejen el desbordamiento de enteros exactamente de la misma manera, lo que probablemente impondría serios costos de rendimiento, 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 un comportamiento indefinido . No hay problema, no hay "debería" involucrado. Si el comportamiento indefinido cambia cuando se declara una de las variables involucradas volatile, eso 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 (vea la entrada de Wikipedia sobre puntos de secuencia ).

relajarse
fuente
8
@ PiX: Las cosas no están definidas por varias razones posibles. Estos incluyen: no hay un "resultado correcto" claro, diferentes arquitecturas de máquina favorecerían fuertemente los diferentes resultados, la práctica existente no es consistente o está más allá del alcance del estándar (por ejemplo, qué nombres de archivo son válidos).
Richard
Solo para confundir a todos, algunos de estos ejemplos ahora están bien definidos en C11, por ejemplo i = ++i + 1;.
MM
2
Al leer el Estándar y la justificación publicada, está claro por qué existe el concepto de UB. El estándar nunca tuvo la intención de describir completamente todo lo que una implementación de C debe hacer para ser adecuado para un propósito particular (ver la discusión de la regla de "Un programa"), sino que se basa en el juicio de los implementadores y el deseo de producir implementaciones de calidad útiles. Una implementación de calidad adecuada para la programación de sistemas de bajo nivel necesitará definir el comportamiento de acciones que no serían necesarias en aplicaciones de procesamiento de números de alta gama. En lugar de tratar de complicar el estándar ...
supercat
3
... al entrar en detalles extremos sobre qué casos de esquina están o no definidos, los autores de la Norma reconocieron que los implementadores deben estar mejor preparados para juzgar qué tipos de comportamientos serán necesarios para los tipos de programas que se espera que apoyen . Los compiladores hipermodernistas pretenden que realizar ciertas acciones UB tenía la intención de implicar que ningún programa de calidad debería necesitarlas, pero el Estándar y la justificación son inconsistentes con una intención tan supuesta.
supercat
1
@jrh: escribí esa respuesta antes de darme cuenta de lo fuera de control que se había puesto la filosofía hipermoderna. Lo que me molesta es la progresión de "No necesitamos reconocer oficialmente este comportamiento porque las plataformas donde se necesita pueden soportarlo de todos modos" a "Podemos eliminar este comportamiento sin proporcionar un reemplazo utilizable porque nunca fue reconocido y, por lo tanto, cualquier código necesitándolo estaba roto ". Muchos comportamientos deberían haber quedado en desuso hace mucho tiempo a favor de los reemplazos que fueron en todos los sentidos mejores , pero eso habría requerido reconocer su legitimidad.
supercat
78

Simplemente compile y desarme su línea de código, si está dispuesto a saber exactamente cómo es que 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.

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

badp
fuente
¿Cómo obtengo el código de la máquina? Yo uso Dev C ++, y yo jugaba alrededor opción 'Generación de código' en la configuración del compilador con, pero ve ninguna salida adicional de archivos o cualquier salida de la consola
bad_keypoints
55
@ronnieaka gcc evil.c -c -o evil.biny gdb evil.bindisassemble evil, o cualesquiera que sean los equivalentes de Windows de ellos :)
badp
21
Esta respuesta realmente no aborda la cuestión de Why are these constructs undefined behavior?.
Shafik Yaghmour
99
Por otro lado, será más fácil compilar el ensamblado (con gcc -S evil.c), que es todo lo que se necesita aquí. Montar y luego desarmar es solo una forma indirecta de hacerlo.
Kat
50
Para el registro, si por alguna razón se pregunta qué hace una construcción determinada, y especialmente si existe alguna sospecha de que podría ser un comportamiento indefinido, el antiguo consejo de "simplemente pruébelo con su compilador y vea" es potencialmente bastante peligroso. Aprenderá, en el mejor de los casos, qué hace con esta versión de su compilador, en estas circunstancias, hoy . Usted no aprender mucho o nada acerca de lo que está garantizado que hacer. En general, "simplemente pruébelo con su compilador" conduce a programas no portables que funcionan solo con su compilador.
Steve Summit
64

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

Entre el punto de secuencia anterior y el 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 acceder a él después del siguiente punto de secuencia, el comportamiento no está definido.

Christoph
fuente
2
Sería lo anterior implica que 'i = i = 5;?" Sería un comportamiento indefinido
supercat
1
@supercat hasta donde yo sé i=i=5también es un comportamiento indefinido
dhein
2
@Zaibis: la lógica que me gusta usar para la mayoría de los lugares aplica que, en teoría, una plataforma multiprocesador podría implementar algo A=B=5;como "Bloqueo de escritura A; Bloqueo de escritura B; Almacenar 5 en A; almacenar 5 en B; Desbloquear B ; Desbloquear A; ", y una declaración C=A+B;como" Read-lock A; Read-lock B; Compute A + B; Unlock A and B; Write-lock C; Store result; Unlock C; ". Eso aseguraría que si un hilo lo hiciera A=B=5;mientras que otro lo hiciera, C=A+B;el último hilo vería que ambas escrituras han tenido lugar o ninguna. Potencialmente una garantía útil. Si un hilo lo hizo I=I=5;, sin embargo, ...
supercat
1
... y el compilador no se dio cuenta de que ambas escrituras estaban en la misma ubicación (si uno o ambos valores implican punteros, eso puede ser difícil de determinar), el código generado podría llegar a un punto muerto. No creo que ninguna implementación del mundo real implemente dicho bloqueo como parte de su comportamiento normal, pero sería permisible bajo el estándar, y si el hardware pudiera implementar dichos comportamientos a bajo costo, podría ser útil. En el hardware actual, dicho comportamiento sería demasiado costoso de implementar por defecto, pero eso no significa que siempre sea así.
supercat
1
@supercat, pero ¿no sería suficiente la regla de acceso al punto de secuencia de c99 para declararlo como un comportamiento indefinido? Entonces, ¿no importa qué técnicamente podría implementar el hardware?
dhein
55

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

Secuenciado: (5.1.2.3)

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

Sin secuencia:

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

Las evaluaciones pueden ser una de dos cosas:

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

Ahora volviendo 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 de escalar es unsequenced con relación a ya 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 es indefinido . [...]

Por lo tanto, la expresión anterior invoca UB porque dos efectos secundarios en el mismo objeto no iestán secuenciados entre sí. Eso significa que no se secuencia si el efecto secundario por asignación ise realizará antes o después del efecto secundario ++.
Dependiendo de si la asignación ocurre antes o después del incremento, se producirán resultados diferentes y ese es el caso del comportamiento indefinido .

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

el hecho de que ++aparezca después de la variable no significa que el incremento ocurra tarde . El incremento puede ocurrir tan pronto como al compilador le guste , siempre que el compilador se asegure de 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  

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

hacks
fuente
52

El comportamiento en realidad no puede ser explicado porque invoca tanto el comportamiento no especificado y un comportamiento indefinido , por lo que no podemos hacer predicciones generales sobre este código, aunque si se lee de Olve Maudal trabajo como Profundo C y no especificado y Indefinido veces se puede hacer buen adivina en casos muy específicos con un compilador y un entorno específicos, pero no lo haga cerca de la producción.

Entonces, pasando al comportamiento no especificado , en el borrador de la sección estándar c99 , el 6.5párrafo 3 dice (el énfasis es mío ):

La agrupación de operadores y operandos se indica mediante la sintaxis.74) Excepto como se especifica más adelante (para la función call (), &&, ||,?: Y operadores de coma), el orden de evaluación de subexpresiones y el orden en qué efectos secundarios tienen lugar no están especificados.

Entonces, cuando tenemos una línea como esta:

i = i++ + ++i;

No sabemos si se evaluará i++o ++ise evaluará primero. Esto es principalmente para dar al compilador mejores opciones para la optimización .

También tenemos un comportamiento no definido aquí también ya que el programa está modificando las variables ( i, u, etc ..) más de una vez entre los 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, 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 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, lo que terminará ;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 ofrece dos o más posibilidades y no impone requisitos adicionales sobre los cuales se elige en ningún caso

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

comportamiento, al usar una construcción de programa no portable 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 la situación por completo 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 finalizar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).

Shafik Yaghmour
fuente
33

Otra forma de responder a esto, en lugar de atascarse en detalles arcanos de puntos de secuencia y comportamiento indefinido, es simplemente preguntar, ¿qué se supone que significan? ¿Qué intentaba hacer el programador?

El primer fragmento sobre el que pregunté i = i++ + ++i, es bastante loco en mi libro. Nadie lo escribiría nunca en un programa real, no es obvio lo que hace, no hay un algoritmo concebible que alguien podría haber intentado codificar que hubiera resultado en esta secuencia de operaciones artificial. Y dado que 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 entender 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 asignar el resultado nuevamente a i. Pero hay un par de formas de hacer esto en C. La forma más básica de agregar 1 a i, y asignar el resultado a i, es la misma en casi cualquier lenguaje de programación:

i = i + 1

C, por supuesto, tiene un atajo útil:

i++

Esto significa "agregar 1 a i y asignar el resultado nuevamente a i". Entonces, si construimos una mezcolanza de los dos, escribiendo

i = i++

lo que realmente estamos diciendo es "agregue 1 a i, y asigne el resultado nuevamente 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 expresiones locas es cuando las personas las usan como ejemplos artificiales de cómo se supone que funciona ++. Y, por supuesto, es importante entender cómo funciona ++. Pero una regla práctica para usar ++ es: "Si no es obvio lo que significa una expresión usando ++, no la escriba".

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

Ver también la pregunta 3.8 y el resto de las preguntas en la sección 3 de la lista C FAQ .

Steve Summit
fuente
1
Un problema bastante desagradable con respecto al comportamiento indefinido es que, aunque solía ser seguro en el 99.9% de los compiladores, *p=(*q)++;significa if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;que ya no es el caso. La C hipermoderna requeriría escribir algo como la última formulación (aunque no hay una forma estándar de indicar que el código no le importa lo que hay *p) para lograr el nivel de eficiencia que los compiladores solían proporcionar con la primera (la elsecláusula es necesaria para permitir el compilador optimiza lo ifque requerirían algunos compiladores más nuevos).
supercat
@supercat Ahora creo que cualquier compilador que sea lo suficientemente "inteligente" para realizar ese tipo de optimización también debe ser lo suficientemente inteligente como para mirar las assertdeclaraciones, de modo que el programador pueda preceder a la línea en cuestión con un simple assert(p != q). (Por supuesto, tomar ese curso también requeriría una reescritura <assert.h>para no eliminar aserciones directamente en versiones que no sean de depuración, sino que las convierta en algo así para __builtin_assert_disabled()que el compilador lo pueda ver y luego no emita código).
Steve Summit,
25

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

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

o

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

o variantes similares.

Si bien esto también es un comportamiento indefinido, como ya se dijo, existen diferencias sutiles cuando printf()se trata de comparar 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 noprintf() está especificado . Eso significa, expresiones i++y ++ipodrían evaluarse en cualquier orden. El estándar C11 tiene algunas descripciones relevantes sobre esto:

Anexo J, comportamientos no especificados

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

3.4.4, comportamiento no especificado

El uso de un valor no especificado u otro comportamiento donde esta Norma Internacional ofrece dos o más posibilidades y no impone requisitos adicionales sobre los cuales se elige en ningún caso.

EJEMPLO Un ejemplo de comportamiento no especificado es el orden en que se evalúan los argumentos de una función.

El comportamiento no especificado en sí NO es un problema. Considere este ejemplo:

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

Esto también tiene un comportamiento no especificado porque el orden de evaluación de ++xy y++no está especificado. Pero es una declaración perfectamente legal y válida. No hay un comportamiento indefinido en esta declaración. Porque las modificaciones ( ++xy y++) se realizan a objetos distintos .

Lo que representa la siguiente declaración

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

como comportamiento indefinido es el hecho de que estas dos expresiones modifican el mismo objeto isin un punto de secuencia intermedio .


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

Esta es una distinción importante porque el operador de coma 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 produce solo el valor del último operando. Así que en j = (++i, i++);, ++iincrementos ia 6y i++rendimientos antiguo valor de i( 6), que se asigna a j. Luego se ivuelve 7debido al post-incremento.

Entonces, si la coma en la llamada de función fuera un operador de coma, entonces

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

No será un problema. Pero invoca un comportamiento indefinido porque la coma aquí es un separador .


Para aquellos que son nuevos en el comportamiento indefinido, se beneficiarían de leer Lo que todo programador de C debe saber sobre el comportamiento indefinido para comprender el concepto y muchas otras variantes del comportamiento indefinido en C.

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

PÁGINAS
fuente
Esta secuencia int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));parece dar un comportamiento estable (evaluación de argumento de derecha a izquierda en gcc v7.3.0; resultado "a = 110 b = 40 c = 60"). ¿Es porque las asignaciones se consideran 'declaraciones completas' y, por lo tanto, introducen un punto de secuencia? ¿No debería eso resultar en una evaluación de argumento / declaración de izquierda a derecha? O, ¿es solo manifestación de un comportamiento indefinido?
kavadias
@kavadias Esa declaración printf implica un comportamiento indefinido, por la misma razón explicada anteriormente. Estás escribiendo by cen 3er y 4to argumentos respectivamente y estás leyendo en 2do argumento. Pero no hay una secuencia entre estas expresiones (2 °, 3 ° y 4 ° args). gcc / clang tiene una opción -Wsequence-pointque también puede ayudar a encontrarlos.
PP
23

Si bien es poco probable que algún compilador y procesador lo haga, sería legal, según 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

Si bien no creo que ningún procesador sea compatible con el hardware para permitir que tal cosa se haga de manera eficiente, uno puede imaginar fácilmente situaciones en las que tal comportamiento haría más fácil el código multiproceso (por ejemplo, garantizaría que si dos hilos intentan realizar lo anterior secuencia simultánea,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) e intercalara las instrucciones anteriores a lo largo de la evaluación de la expresión general (también legal), y si no se notara que sucedió una de las otras instrucciones para acceder i, sería posible (y legal) para el compilador generar una secuencia de instrucciones que se estancarían. Sin duda, un compilador seguramente detectará el problema en el caso en que ise use la misma variable en ambos lugares, pero si una rutina acepta referencias a dos punteros py q, y usa (*p)y y(*q) en la expresión anterior (en lugar de usaridos veces) no se requeriría que el compilador reconozca o evite el punto muerto que ocurriría si se pasara la misma dirección del objeto para ambospq

Super gato
fuente
16

Mientras que la sintaxis de las expresiones gusta a = a++o a++ + a++es legal, el comportamiento de estas construcciones es indefinido debido a que una se en C estándar no se cumple. C99 6.5p2 :

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

Con la nota a pie de página 73 aclarando aún más que

  1. Este párrafo presenta expresiones de sentencias indefinidas como

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

    mientras permite

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

Los diversos puntos de secuencia se enumeran en el Anexo C de C11 (y C99 ):

  1. Los siguientes son los puntos de secuencia descritos en 5.1.2.3:

    • Entre las evaluaciones del designador de funciones y los argumentos reales en una llamada de función y la llamada real. (6.5.2.2).
    • Entre las evaluaciones del primer y segundo operandos de los siguientes operadores: AND && lógico (6.5.13); OR lógico || (6.5.14); coma, (6.5.17).
    • Entre las evaluaciones del primer operando del condicional? : operador y el segundo y tercer operandos evaluados (6.5.15).
    • El final de un declarador completo: declaradores (6.7.6);
    • Entre la evaluación de una expresión completa y la siguiente expresión completa a evaluar. Las siguientes son expresiones completas: un inicializador que no es parte de un literal compuesto (6.7.9); la expresión en una declaración de expresión (6.8.3); la expresión controladora de una declaración de selección (if o switch) (6.8.4); la expresión controladora de un enunciado while o do (6.8.5); cada una de las expresiones (opcionales) de una declaración for (6.8.5.3); la expresión (opcional) en una declaración de devolución (6.8.6.4).
    • Inmediatamente antes de que una función de biblioteca regrese (7.1.4).
    • Después de las acciones asociadas con cada especificador de conversión de función de entrada / salida formateada (7.21.6, 7.29.2).
    • Inmediatamente antes e inmediatamente después de cada llamada a una función de comparación, y también entre cualquier llamada a una función de comparación y cualquier movimiento de los objetos pasados ​​como argumentos a esa llamada (7.22.5).

La redacción del mismo párrafo en C11 es:

  1. 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 es indefinido. Si hay múltiples ordenamientos permitidos de las subexpresiones de una expresión, el comportamiento no está definido si se produce un efecto secundario no secuenciado en cualquiera de los ordenamientos.84)

Puede detectar dichos errores en un programa, por ejemplo, utilizando una versión reciente de GCC con -Wally -Werror, y luego GCC se negará por completo a compilar su programa. La siguiente es la salida de gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

La parte importante es saber qué es un punto de secuencia, y qué es un punto de secuencia y qué no lo es . Por ejemplo, el operador de coma es un punto de secuencia, entonces

j = (i ++, ++ i);

está bien definido e incrementará ien uno, produciendo el valor anterior, descarta ese valor; luego en el operador de coma, resuelva los efectos secundarios; y luego se incrementa ien uno, y el valor resultante se convierte en el valor de la expresión, es decir, esta es solo una forma artificial de escribir j = (i += 2)que, una vez más, es una forma "inteligente" de escribir

i += 2;
j = i;

Sin embargo, las ,listas de argumentos en función no son operadores de coma y no hay un punto de secuencia entre las evaluaciones de argumentos distintos; en cambio, sus evaluaciones no tienen secuencia entre sí; entonces la llamada a la función

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

tiene un comportamiento indefinido porque no hay un punto de secuencia entre las evaluaciones de i++y ++ien los argumentos de la función y i, por lo tanto , el valor de se modifica dos veces, por ambos i++y ++i, entre el punto de secuencia anterior y el siguiente.

Antti Haapala
fuente
14

El estándar C dice que una variable solo debe asignarse 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 de la forma:

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

y así sucesivamente violan esa regla. El estándar también dice que el comportamiento no está definido y no está especificado. Algunos compiladores los detectan y producen algún resultado, pero esto no es estándar.

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

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

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

Nikhil Vidhani
fuente
Por supuesto, no se aplica a diferentes variables dentro de una expresión. ¡Sería una falla total de diseño si lo hiciera! Todo lo que necesita en el segundo ejemplo es que ambos se incrementen entre el final de la declaración y el siguiente, y eso está garantizado, precisamente debido al concepto de puntos de secuencia en el centro de todo esto.
underscore_d
11

En /programming/29505280/incrementing-array-index-in-c alguien preguntó acerca de una declaración como:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

que imprime 7 ... el OP esperaba que imprimiera 6.

No ++ise garantiza que todos los incrementos se completen antes del resto de los cálculos. De hecho, diferentes compiladores obtendrán diferentes resultados aquí. En el ejemplo que nos ha facilitado, la primera 2 ++iejecutados, entonces los valores de k[]se leyeron, a continuación, la última ++ia continuación k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

Los compiladores modernos optimizarán esto muy bien. De hecho, posiblemente mejor que el código que escribió originalmente (suponiendo que haya funcionado de la manera que esperaba).

TomOnTime
fuente
5

En el documento n1188 del sitio ISO W14 se proporciona una buena explicación sobre lo que sucede en este tipo de cálculo. .

Les explico las ideas.

La regla principal de la norma ISO 9899 que se aplica en esta situación es 6.5p2.

Entre el punto de secuencia anterior y el 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á.

Los puntos de secuencia en una expresión como i=i++son antes i=y despuési++ .

En el documento que cité anteriormente, se explica que puede descubrir que el programa está formado por pequeños cuadros, cada cuadro contiene las instrucciones entre 2 puntos de secuencia consecutivos. Los puntos de secuencia se definen en el anexo C de la norma, en el caso de que i=i++haya 2 puntos de secuencia que delimitan una expresión completa. Tal expresión es sintácticamente equivalente con una entrada expression-statementen la forma de la gramática Backus-Naur (se proporciona una gramática en el anexo A de la Norma).

Por lo tanto, el orden de las instrucciones dentro de una caja no tiene un orden claro.

i=i++

puede ser interpretado como

tmp = i
i=i+1
i = tmp

o como

tmp = i
i = tmp
i=i+1

porque ambas formas para interpretar el código i=i++son válidas y porque ambas generan respuestas diferentes, el comportamiento es indefinido.

Entonces, se puede ver un punto de secuencia al principio y al final de cada cuadro que compone el programa [los cuadros son unidades atómicas en C] y dentro de un cuadro el orden de las instrucciones no está definido en todos los casos. Cambiando ese orden uno puede cambiar el resultado a veces.

EDITAR:

Otra buena fuente para explicar tales ambigüedades son las entradas del sitio c-faq (también publicado como libro ), a saber, aquí y aquí y aquí .

alinsoar
fuente
¿Cómo esta respuesta agregó nuevas a las respuestas existentes? También las explicaciones para i=i++es muy similar a esta respuesta .
piratea el
@haccks No leí las otras respuestas. Quería explicar en mi propio idioma lo que aprendí del documento mencionado del sitio oficial de ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar
5

Su pregunta probablemente no fue, "¿Por qué estas construcciones son comportamientos indefinidos en C?". Probablemente su pregunta fue: "¿Por qué este código (usando ++) no me dio el valor que esperaba?", Y alguien marcó su pregunta como un duplicado y lo envió aquí.

Esta respuesta intenta responder a esa pregunta: ¿por qué su código no le dio la respuesta que esperaba y cómo puede aprender a reconocer (y evitar) expresiones que no funcionarán como se esperaba?

Supongo que ya ha escuchado la definición básica de C ++y --operadores, y cómo el prefijo ++xdifiere del postfix x++. Pero es difícil pensar en estos operadores, así que para asegurarte de que entiendes, tal vez escribiste un pequeño programa de prueba que involucra algo como

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Pero, para su sorpresa, este programa no lo ayudó a comprender: imprimió un resultado extraño, inesperado e inexplicable, lo que sugiere que tal vez ++hace algo completamente diferente, en absoluto lo que pensaba que hizo.

O tal vez estás viendo una expresión difícil de entender como

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

Quizás alguien te dio ese código como un rompecabezas. Este código tampoco tiene sentido, especialmente si lo ejecuta, y si lo compila y ejecuta bajo dos compiladores diferentes, ¡es probable que obtenga dos respuestas diferentes! ¿Que pasa con eso? ¿Qué respuesta es la correcta? (Y la respuesta es que ambos lo son, o ninguno lo es).

Como ya has escuchado, todas estas expresiones son indefinidas , lo que significa que el lenguaje C no garantiza lo que harán. Este es un resultado extraño y sorprendente, porque probablemente pensó que cualquier programa que pudiera escribir, siempre y cuando se compilara y ejecutara, generaría un resultado único y bien definido. Pero en el caso de un comportamiento indefinido, eso no es así.

¿Qué hace que una expresión sea indefinida? ¿Son las expresiones envolventes ++y --siempre indefinidas? Por supuesto que no: estos son operadores útiles, y si los usa correctamente, están perfectamente bien definidos.

Para las expresiones de las que estamos hablando, lo que las hace indefinidas es cuando ocurren muchas cosas a la vez, cuando no estamos seguros de en qué orden sucederán las cosas, pero cuando el orden es importante para el resultado que obtenemos.

Volvamos a los dos ejemplos que he usado en esta respuesta. Cuando escribi

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

la pregunta es, antes de llamar printf, ¿el compilador calcula el valor de xprimero, o x++, o tal vez ++x? Pero resulta que no sabemos . No hay una regla en C que diga que los argumentos de una función se evalúan de izquierda a derecha, o de derecha a izquierda, o en algún otro orden. Así que no podemos decir si el compilador hará xprimero, y luego ++x, a continuación x++, o x++entonces ++xa continuación x, o algún otro fin. Pero el orden claramente importa, porque dependiendo del orden que use el compilador, obtendremos claramente diferentes resultados impresos printf.

¿Qué hay de esta expresión loca?

x = x++ + ++x;

El problema con esta expresión es que contiene tres intentos diferentes de modificar el valor de x: (1) la x++parte intenta agregar 1 a x, almacenar el nuevo valor xy devolver el valor anterior de x; (2) la ++xparte intenta agregar 1 a x, almacenar el nuevo valor xy devolver el nuevo valor de x; y (3) la x =parte intenta asignar la suma de los otros dos a x. ¿Cuál de esos tres intentos de tareas "ganará"? ¿A cuál de los tres valores se le asignará realmente x? De nuevo, y quizás sorprendentemente, no hay una regla en C que nos diga.

Puede imaginar que la precedencia o la asociatividad o la evaluación de izquierda a derecha le dicen en qué orden suceden las cosas, pero no es así. Puede que no me creas, pero por favor toma mi palabra y lo diré de nuevo: la precedencia y la asociatividad no determinan todos los aspectos del orden de evaluación de una expresión en C. En particular, si dentro de una expresión hay múltiples diferentes lugares donde tratamos de asignar un nuevo valor a algo como x, la precedencia y la asociatividad no nos dicen cuál de esos intentos ocurre primero, o el último, o algo así.


Entonces, con todo ese trasfondo e introducción fuera del camino, si quieres asegurarte de que todos tus programas estén bien definidos, ¿qué expresiones puedes escribir y cuáles no?

Estas expresiones están todas bien:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Estas expresiones son todas indefinidas:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

Y la última pregunta es, ¿cómo puede saber qué expresiones están bien definidas y qué expresiones no están definidas?

Como dije antes, las expresiones indefinidas son aquellas en las que hay demasiadas cosas a la vez, donde no puedes estar seguro de en qué orden suceden las cosas y dónde importa el orden:

  1. Si hay una variable que está siendo modificada (asignada) en dos o más lugares diferentes, ¿cómo sabe qué modificación ocurre primero?
  2. Si hay una variable que se está modificando en un lugar y que su valor se usa en otro lugar, ¿cómo sabe si usa el valor anterior o el nuevo?

Como un ejemplo de # 1, en la expresión

x = x++ + ++x;

Hay tres intentos de modificar `x.

Como un ejemplo de # 2, en la expresión

y = x + x++;

ambos usamos el valor de xy lo modificamos.

Entonces esa es la respuesta: asegúrese de que en cualquier expresión que escriba, cada variable se modifique como máximo una vez, y si se modifica una variable, no intente usar el valor de esa variable en otro lugar.

Steve Summit
fuente
3

La razón es que el programa está ejecutando un comportamiento indefinido. El problema radica en el orden de evaluación, porque no se requieren puntos de secuencia según el estándar C ++ 98 (no se secuencian operaciones antes o después de otra según la terminología de C ++ 11).

Sin embargo, si se atiene a un compilador, encontrará que el comportamiento es persistente, siempre que no agregue llamadas a funciones o punteros, lo que haría que el comportamiento sea más desordenado.

  • Primero el GCC: Usando Nuwen MinGW 15 GCC 7.1 obtendrás:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

¿Cómo funciona GCC? evalúa las expresiones secundarias en un orden de izquierda a derecha para el lado derecho (RHS), luego asigna el valor al lado izquierdo (LHS). Así es exactamente como Java y C # se comportan y definen sus estándares. (Sí, el software equivalente en Java y C # tiene comportamientos definidos). Evalúa cada subexpresión una por una en la Declaración RHS en un orden de izquierda a derecha; para cada subexpresión: el ++ c (pre-incremento) se evalúa primero, luego se usa el valor c para la operación, luego el incremento posterior c ++).

según GCC C ++: operadores

En GCC C ++, la precedencia de los operadores controla el orden en que se evalúan los operadores individuales

El código equivalente en el comportamiento definido C ++ como GCC entiende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

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

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Luego vamos a Visual Studio . Visual Studio 2015, obtienes:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

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

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

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

¿Cómo funciona el estudio visual? Toma otro enfoque, evalúa todas las expresiones de pre-incrementos en el primer paso, luego usa valores de variables en las operaciones en el segundo paso, asigna de RHS a LHS en el tercer paso, luego en el último paso evalúa todos los expresiones de incremento posterior en una pasada.

Entonces, el equivalente en el comportamiento definido C ++ como Visual C ++ comprende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

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

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

como lo indica la documentación de Visual Studio en Precedencia y orden de evaluación :

Cuando varios operadores aparecen juntos, tienen la misma precedencia y se evalúan de acuerdo con su asociatividad. Los operadores en la tabla se describen en las secciones que comienzan con Operadores Postfix.

Mohamed El-Nakib
fuente
1
Edité la pregunta para agregar la UB en la evaluación de argumentos de función, ya que esta pregunta a menudo se usa como un duplicado para eso. (El último ejemplo)
Antti Haapala
1
También la pregunta es sobre c ahora, no sobre C ++
Antti Haapala