¿Es este comportamiento C indefinido?

Nuestro profesor de progtwigción C hizo esta pregunta a la clase:

Te dan el código:

int x=1; printf("%d",++x,x+1); 

¿Qué salida producirá siempre?

La mayoría de los estudiantes dijo comportamiento indefinido. ¿Alguien puede ayudarme a entender por qué es así?

Gracias por la edición y las respuestas, pero todavía estoy confundido.

El resultado es probable que sea 2 en todos los casos razonables. En realidad, lo que tienes es un comportamiento indefinido.

Específicamente, el estándar dice:

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

Hay un punto de secuencia antes de evaluar los argumentos para una función, y un punto de secuencia después de que todos los argumentos hayan sido evaluados (pero la función aún no se haya llamado). Entre esos dos (es decir, mientras se evalúan los argumentos) no hay un punto de secuencia (a menos que un argumento sea una expresión que incluya una internamente, como usar el operador && || o , ).

Eso significa que la llamada a printf está leyendo el valor anterior tanto para determinar el valor almacenado (es decir, ++x ) como para determinar el valor del segundo argumento (es decir, x+1 ). Esto claramente infringe los requisitos citados anteriormente, lo que resulta en un comportamiento indefinido.

El hecho de que haya proporcionado un argumento adicional para el que no se proporciona un especificador de conversión no da como resultado un comportamiento indefinido. Si proporciona menos argumentos que los especificadores de conversión, o si el tipo (promocionado) del argumento no concuerda con el del especificador de conversión, obtendrá un comportamiento indefinido, pero pasar un parámetro extra no lo hace.

Cada vez que el comportamiento de un progtwig no está definido, cualquier cosa puede suceder: la frase clásica es que “los demonios pueden volar por la nariz”, aunque la mayoría de las implementaciones no llegan tan lejos.

Los argumentos de una función se evalúan conceptualmente en paralelo (el término técnico es que no hay punto de secuencia entre su evaluación). Eso significa que las expresiones ++x y x+1 se pueden evaluar en este orden, en el orden opuesto, o de alguna manera intercalada . Cuando modifica una variable e intenta acceder a su valor en paralelo, el comportamiento no está definido.

Con muchas implementaciones, los argumentos se evalúan en secuencia (aunque no siempre de izquierda a derecha). Entonces es poco probable que veas algo más que 2 en el mundo real.

Sin embargo, un comstackdor podría generar código como este:

  1. Cargue x en el registro r1 .
  2. Calcule x+1 agregando 1 a r1 .
  3. Calcule ++x agregando 1 a r1 . Está bien porque x se ha cargado en r1 . Teniendo en cuenta cómo se diseñó el comstackdor, el paso 2 no puede haber modificado r1 , porque eso solo podría suceder si x se leyó y se escribió entre dos puntos de secuencia. Que está prohibido por el estándar C
  4. Almacene r1 en x .

Y en este (hipotético, pero correcto) comstackdor, el progtwig imprimiría 3.

( EDITAR: pasar un argumento extra a printf es correcto (§7.19.6.1-2 en N1256 , gracias a Prasoon Saurav ) por señalar esto. También: agregó un ejemplo).

La respuesta correcta es: el código produce un comportamiento indefinido.

La razón por la cual el comportamiento no está definido es que las dos expresiones ++x y x + 1 están modificando x y leyendo x para una razón no relacionada (a modificación) y estas dos acciones no están separadas por un punto de secuencia. Esto da como resultado un comportamiento indefinido en C (y C ++). El requisito se proporciona en 6.5 / 2 del estándar de lenguaje C.

Tenga en cuenta que el comportamiento indefinido en este caso no tiene absolutamente nada que ver con el hecho de que la función printf solo tiene un especificador de formato y dos argumentos reales. Proporcionar más argumentos para printf que los especificadores de formato en el formato de cadena es perfectamente legal en C. Una vez más, el problema radica en la violación de los requisitos de evaluación de expresión del lenguaje C.

También tenga en cuenta que algunos participantes de esta discusión no logran captar el concepto de comportamiento indefinido e insisten en mezclarlo con el concepto de comportamiento no especificado . Para ilustrar mejor la diferencia, consideremos el siguiente ejemplo simple

 int inc_x(int *x) { return ++*x; } int x_plus_1(int x) { return x + 1; } int x = 1; printf("%d", inc_x(&x), x_plus_1(x)); 

El código anterior es “equivalente” al original, excepto que las operaciones que involucran a nuestra x están envueltas en funciones. ¿Qué va a pasar en este último ejemplo?

No hay un comportamiento indefinido en este código. Pero dado que el orden de evaluación de los argumentos printf no está especificado , este código produce un comportamiento no especificado , es decir, es posible que se llame printf("%d", 2, 2) como printf("%d", 2, 2) o como printf("%d", 2, 3) . En ambos casos, la salida será de hecho 2 . Sin embargo, la diferencia importante de esta variante es que todos los accesos a x se envuelven en puntos de secuencia presentes al principio y al final de cada función, por lo que esta variante no produce un comportamiento indefinido.

Este es exactamente el razonamiento que otros carteles intentan forzar sobre el ejemplo original. Pero no se puede hacer. El ejemplo original produce un comportamiento indefinido , que es una bestia completamente diferente. Al parecer, intentan insistir en que, en la práctica, un comportamiento indefinido siempre es equivalente a un comportamiento no especificado. Este es un reclamo totalmente falso que solo indica la falta de experiencia de quienes lo hacen. El código original produce un comportamiento indefinido, punto.

Para continuar con el ejemplo, modifiquemos la muestra del código anterior para

 printf("%d %d", inc_x(&x), x_plus_1(x)); 

la salida del código será generalmente impredecible. Puede imprimir 2 2 o puede imprimir 2 3 . Sin embargo, tenga en cuenta que, aunque el comportamiento es impredecible, todavía no produce un comportamiento indefinido . El comportamiento no está especificado , bit no definido . El comportamiento no especificado está restringido a dos posibilidades: ya sea 2 2 o 2 3 . El comportamiento indefinido no está restringido a nada. Puede formatear su disco duro en lugar de imprimir algo. Siente la diferencia.

La mayoría de los estudiantes dijo comportamiento indefinido. ¿Alguien puede ayudarme a entender por qué es así?

Porque no se especifica el orden en el que se calculan los parámetros de la función.

¿Qué salida producirá siempre?

Producirá 2 en todos los entornos que se me ocurra. Sin embargo, la interpretación estricta del estándar C99 deja el comportamiento indefinido porque los accesos a x no cumplen los requisitos que existen entre los puntos de secuencia.

La mayoría de los estudiantes dijo comportamiento indefinido. ¿Alguien puede ayudarme a entender por qué es así?

Ahora abordaré la segunda pregunta que entiendo como “¿Por qué la mayoría de los estudiantes de mi clase dicen que el código mostrado constituye un comportamiento indefinido?” y creo que ningún otro cartel ha respondido hasta ahora. Una parte de los estudiantes habrá recordado ejemplos de valor indefinido de expresiones como

 f(++i,i) 

El código que proporcione se ajusta a este patrón, pero los alumnos piensan erróneamente que el comportamiento se define de todos modos porque printf ignora el último parámetro. Este matiz confunde a muchos estudiantes. Otra parte del estudiante estará tan versada en el estándar como David Thornley y dirá “comportamiento indefinido” por las razones correctas explicadas anteriormente.

Los puntos sobre el comportamiento indefinido son correctos, pero hay una dificultad adicional: printf puede fallar. Está haciendo el archivo IO; hay varios motivos por los que podría fallar, y es imposible eliminarlos sin conocer el progtwig completo y el contexto en el que se ejecutará.

Haciéndose eco de codadigir, la respuesta es 2.

printf se invocará con el argumento 2 y lo imprimirá.

Si este código se pone en un contexto como:

 void do_something() { int x=1; printf("%d",++x,x+1); } 

Entonces, el comportamiento de esa función se define completa e inequívocamente. No estoy, por supuesto, argumentando que esto es bueno o correcto o que el valor de x es determinable después.

El resultado será siempre (para el 99.98% de los comstackdores y sistemas que cumplen con stadard más importantes) 2.

De acuerdo con el estándar, esto parece ser, por definición , “comportamiento indefinido”, una definición / respuesta que se autojustifica y que no dice nada sobre lo que realmente puede suceder, y especialmente por qué .

La férula de utilidad (que no es una herramienta de verificación de cumplimiento estándar), y los progtwigdores de férulas, consideran esto como “comportamiento no especificado”. Esto significa, básicamente, que la evaluación de (x+1) puede dar 1 + 1 o 2 + 1, dependiendo de cuándo se realiza realmente la actualización de x . Sin embargo, como la expresión se descarta (el formato printf lee 1 argumento), la salida no se ve afectada, y aún podemos decir que es 2.

undefined.c: 7: 20: El argumento 2 modifica x, utilizado por el argumento 3 (el orden de evaluación de los parámetros reales no está definido): printf (“% d \ n”, ++ x, x + 1) El código tiene un comportamiento no especificado. El orden de evaluación de los parámetros o subexpresiones de la función no está definido, de modo que si un valor se usa y modifica en diferentes lugares no separados por un orden de evaluación de restricción de punto de secuencia, entonces el resultado de la expresión no se especifica.

Como se dijo anteriormente, el comportamiento no especificado afecta solo la evaluación de (x+1) , no la statement completa u otras expresiones de la misma. Entonces, en el caso del “comportamiento no especificado”, podemos decir que el resultado es 2 y nadie puede objetar.

Pero este no es un comportamiento no especificado, parece ser un “comportamiento indefinido”. Y el “comportamiento indefinido” parece tener que ser algo que afecta a la statement completa en lugar de la expresión única. Esto se debe al misterio sobre dónde se produce el “comportamiento indefinido” (es decir, qué afecta exactamente).

Si hubiera motivaciones para adjuntar el “comportamiento indefinido” solo a la expresión (x+1) , como en el caso del “comportamiento no especificado”, entonces podríamos decir que el resultado siempre es (100%) 2. Adjuntar el ” comportamiento indefinido “solo para (x+1) significa que no podemos decir si es 1 + 1 o 2 + 1; es solo ” cualquier cosa “. Pero, de nuevo, ese “cualquier cosa” se descarta debido a la impresión, y esto significa que la respuesta sería “siempre (100%) 2”.

En cambio, debido a asimetrías misteriosas, el “comportamiento indefinido” no se puede asociar solo al x+1 , pero de hecho debe afectar al menos el ++x (que por cierto es el responsable del comportamiento indefinido), si no toda la statement. Si infecta solo la expresión ++x , el resultado es un “valor indefinido”, es decir, cualquier número entero, por ejemplo, -5847834 o 9032. Si infecta la statement completa, entonces podría ver gargabe en la salida de la consola, probablemente podría haber para detener el progtwig con ctrl-c, posiblemente antes de que empiece a ahogar su CPU.

Según una leyenda urbana, el “comportamiento indefinido” infecta no solo todo el progtwig, sino también su computadora y las leyes de la física, de modo que criaturas misteriosas puedan ser creadas por su progtwig y puedan volar o comerse.

Ninguna respuesta explica nada de manera competente sobre el tema. Son solo un “oh, mira, el estándar dice esto” (¡y es solo una interpretación, como de costumbre!). Así que al menos has aprendido que los “estándares existen”, y hacen áridas las preguntas educativas (ya que por supuesto, no olvides que tu código es incorrecto , independientemente del comportamiento indefinido / no especificado y otros hechos estándar), inútiles los argumentos lógicos y sin objective las investigaciones profundas y la comprensión.