C ++ y PHP vs C # y Java – resultados desiguales

Encontré algo un poco extraño en C # y Java. Echemos un vistazo a este código C ++:

#include  using namespace std; class Simple { public: static int f() { X = X + 10; return 1; } static int X; }; int Simple::X = 0; int main() { Simple::X += Simple::f(); printf("X = %d", Simple::X); return 0; } 

En una consola, verá X = 11 ( Mire el resultado aquí – IdeOne C ++ ).

Ahora veamos el mismo código en C #:

 class Program { static int x = 0; static int f() { x = x + 10; return 1; } public static void Main() { x += f(); System.Console.WriteLine(x); } } 

En una consola, verá 1 (¡no 11!) (Observe el resultado aquí – IdeOne C # Sé lo que está pensando ahora – “¿Cómo es posible?”, Pero vamos al siguiente código.

Código Java:

 import java.util.*; import java.lang.*; import java.io.*; /* Name of the class has to be "Main" only if the class is public. */ class Ideone { static int X = 0; static int f() { X = X + 10; return 1; } public static void main (String[] args) throws java.lang.Exception { Formatter f = new Formatter(); f.format("X = %d", X += f()); System.out.println(f.toString()); } } 

Resultado igual que en C # (X = 1, mira el resultado aquí ).

Y, por última vez, echemos un vistazo al código PHP:

  

El resultado es 11 (mira el resultado aquí ).

Tengo una pequeña teoría: estos lenguajes (C # y Java) están haciendo una copia local de la variable estática X en la stack (¿están ignorando la palabra clave estática ?). Y esa es la razón por la cual el resultado en esos idiomas es 1.

¿Hay alguien aquí que tenga otras versiones?

El estándar C ++ establece:

Con respecto a una llamada de función con secuencia indeterminada, la operación de una asignación compuesta es una única evaluación. [Nota: Por lo tanto, una llamada a función no debe intervenir entre la conversión lvalue-a-rvalue y el efecto secundario asociado con cualquier operador de asignación compuesta único. -finalizar nota]

§5.17 [expr.ass]

Por lo tanto, como en la misma evaluación usas X y una función con un efecto secundario en X , el resultado no está definido, porque:

Si un efecto secundario en un objeto escalar no se está secuenciando en relación con otro efecto secundario en el mismo objeto escalar o con un cálculo de valor que utiliza el valor del mismo objeto escalar, el comportamiento no está definido.

§1.9 [intro.execution]

Resulta ser 11 en muchos comstackdores, pero no hay ninguna garantía de que un comstackdor de C ++ no le dé 1 en cuanto a los otros lenguajes.

Si aún es escéptico, otro análisis del estándar conduce a la misma conclusión: el estándar también dice en la misma sección que arriba:

El comportamiento de una expresión de la forma E1 op = E2 es equivalente a E1 = E1 op E2 excepto que E1 se evalúa solo una vez.

En tu caso X = X + f() excepto que X se evalúa solo una vez.
Como no hay garantía en el orden de la evaluación, en X + f() , no se puede dar por sentado que se evalúa primero f y luego X

Apéndice

No soy un experto en Java, pero las reglas de Java especifican claramente el orden de evaluación en una expresión, que se garantiza que será de izquierda a derecha en la sección 15.7 de las Especificaciones del lenguaje Java . En la sección 15.26.2. Operadores de Asignación de Compuestos las especificaciones de Java también dicen que E1 op= E2 es equivalente a E1 = (T) ((E1) op (E2)) .

En su progtwig Java, esto significa nuevamente que su expresión es equivalente a X = X + f() y se evalúa primero X , luego f() . Por lo tanto, el efecto secundario de f() no se tiene en cuenta en el resultado.

Entonces su comstackdor de Java no tiene un error. Simplemente cumple con las especificaciones.

Gracias a los comentarios de Deduplicator y user694733, aquí hay una versión modificada de mi respuesta original.


La versión de C ++ tiene indefinido comportamiento no especificado

Hay una diferencia sutil entre “indefinido” y “no especificado”, en el sentido de que el primero permite que un progtwig haga cualquier cosa (incluido el locking) mientras que el segundo le permite elegir entre un conjunto de conductas permitidas particulares sin dictar qué elección es la correcta.

Excepto en casos muy raros, siempre querrás evitar ambos.


Un buen punto de partida para entender todo el problema son las preguntas frecuentes de C ++. ¿Por qué algunas personas piensan que x = ++ y + y ++ es malo? , ¿Cuál es el valor de i ++ + i ++? y ¿Cuál es el problema con los “puntos de secuencia”? :

Entre el punto de secuencia anterior y siguiente, un objeto escalar tendrá su valor almacenado modificado como máximo una vez por la evaluación de una expresión.

(…)

Básicamente, en C y C ++, si lee una variable dos veces en una expresión donde también la escribe, el resultado no está definido .

(…)

En ciertos puntos específicos de la secuencia de ejecución denominados puntos de secuencia, todos los efectos secundarios de las evaluaciones previas deberán estar completos y no se habrán producido los efectos secundarios de las evaluaciones posteriores. (…) Los “determinados puntos específicos” que se denominan puntos de secuencia son (…) después de la evaluación de todos los parámetros de una función, pero antes de que se ejecute la primera expresión dentro de la función.

En resumen, modificar una variable dos veces entre dos puntos de secuencia consecutivos produce un comportamiento indefinido, pero una llamada a función introduce un punto de secuencia intermedio (en realidad, dos puntos de secuencia intermedios, porque la instrucción de retorno crea otra).

Esto significa que el hecho de que tenga una llamada de función en su expresión “guarda” su Simple::X += Simple::f(); línea de indefinido y lo convierte en “solo” no especificado.

Tanto el 1 como el 11 son posibles y los resultados son correctos, mientras que imprimir 123, bloquear o enviar un correo electrónico insultante a su jefe no son comportamientos permitidos; nunca obtendrá una garantía si se imprimirá 1 u 11.


El siguiente ejemplo es ligeramente diferente. Aparentemente es una simplificación del código original, pero realmente sirve para resaltar la diferencia entre el comportamiento indefinido y no especificado:

 #include  int main() { int x = 0; x += (x += 10, 1); std::cout << x << "\n"; } 

Aquí el comportamiento no está definido, porque la llamada a la función se ha ido, por lo que ambas modificaciones de x ocurren entre dos puntos secuenciales consecutivos. El comstackdor está permitido por la especificación del lenguaje C ++ para crear un progtwig que imprime 123, se bloquea o envía un correo electrónico insultante a su jefe.

(Lo del correo electrónico, por supuesto, es solo un bash cómico muy común de explicar qué tan indefinido realmente significa que todo vale . Los lockings a menudo son un resultado más realista de un comportamiento indefinido).

De hecho, el , 1 (al igual que la statement de devolución en su código original) es una pista falsa. Lo siguiente produce un comportamiento indefinido también:

 #include  int main() { int x = 0; x += (x += 10); std::cout << x << "\n"; } 

Esto puede imprimir 20 (lo hace en mi máquina con VC ++ 2013) pero el comportamiento aún no está definido.

(Nota: esto se aplica a los operadores incorporados. La sobrecarga del operador cambia el comportamiento a especificado , porque los operadores sobrecargados copian la syntax de los incorporados pero tienen la semántica de las funciones, lo que significa que un operador += sobrecargado de una función personalizada el tipo que aparece en una expresión es en realidad una llamada a función . Por lo tanto, no solo se introducen los puntos de secuencia, sino que se elimina toda la ambigüedad, y la expresión se vuelve equivalente a x.operator+=(x.operator+=(10)); orden de evaluación del argumento. Probablemente esto sea irrelevante para su pregunta, pero debe mencionarse de todos modos).

Por el contrario, la versión de Java

 import java.io.*; class Ideone { public static void main(String[] args) { int x = 0; x += (x += 10); System.out.println(x); } } 

debe imprimir 10. Esto se debe a que Java no tiene un comportamiento indefinido ni indeterminado con respecto al orden de evaluación. No hay puntos de secuencia que preocuparse. Consulte la Especificación del lenguaje Java 15.7. Orden de evaluación :

El lenguaje de progtwigción Java garantiza que los operandos de los operadores parecen evaluarse en un orden de evaluación específico, es decir, de izquierda a derecha.

Entonces en el caso de Java, x += (x += 10) , interpretado de izquierda a derecha, significa que primero se agrega algo a 0 , y que algo es 0 + 10 . De ahí 0 + (0 + 10) = 10 .

Ver también el ejemplo 15.7.1-2 en la especificación de Java.

Volviendo al ejemplo original, esto también significa que el ejemplo más complejo con la variable estática tiene un comportamiento definido y especificado en Java.


Honestamente, no sé sobre C # y PHP, pero supongo que ambos tienen algún orden de evaluación garantizado también. C ++, a diferencia de la mayoría de los otros lenguajes de progtwigción (pero como C) tiende a permitir un comportamiento mucho más indefinido y no especificado que otros lenguajes. Eso no es bueno o malo. Es una compensación entre solidez y eficiencia . Elegir el lenguaje de progtwigción correcto para una tarea o proyecto en particular siempre es cuestión de analizar las compensaciones.

En cualquier caso, las expresiones con tales efectos secundarios son un mal estilo de progtwigción en los cuatro idiomas .

Una última palabra:

Encontré un pequeño error en C # y Java.

No debe asumir que encuentra errores en las especificaciones del lenguaje o comstackdores si no tiene muchos años de experiencia profesional como ingeniero de software.

Como Christophe ya ha escrito, esto es básicamente una operación indefinida.

Entonces, ¿por qué C ++ y PHP lo hacen de una manera, y C # y Java a la inversa?

En este caso (que puede ser diferente para diferentes comstackdores y plataformas), el orden de evaluación de los argumentos en C ++ se invierte en comparación con C # – C # evalúa los argumentos en orden de escritura, mientras que la muestra de C ++ lo hace al revés. Esto se reduce a las convenciones de llamadas predeterminadas que ambos usan, pero nuevamente – para C ++, esta es una operación no definida, por lo que puede diferir según otras condiciones.

Para ilustrar, este código C #:

 class Program { static int x = 0; static int f() { x = x + 10; return 1; } public static void Main() { x = f() + x; System.Console.WriteLine(x); } } 

Producirá 11 en la salida, en lugar de 1 .

Eso es simplemente porque C # evalúa “en orden”, entonces en su ejemplo, primero lee x y luego llama a f() , mientras que en el mío, primero llama a f() y luego lee x .

Ahora, esto aún podría ser irrealizable. IL (.NET’s bytecode) tiene + como prácticamente cualquier otro método, pero las optimizaciones realizadas por el comstackdor JIT pueden dar como resultado un orden diferente de evaluación. Por otro lado, dado que C # (y .NET) definen el orden de evaluación / ejecución, entonces supongo que un comstackdor compatible siempre debe producir este resultado.

En cualquier caso, es un resultado maravilloso e inesperado que has encontrado, y una historia de advertencia: los efectos secundarios en los métodos pueden ser un problema incluso en los idiomas imperativos 🙂

Ah, y por supuesto, static significa algo diferente en C # vs. C ++. He visto ese error cometido por C ++ antes de llegar a C #.

EDITAR :

Permítanme ampliar un poco el tema de “diferentes idiomas”. Automáticamente asumió que el resultado de C ++ es el correcto, porque cuando realiza el cálculo de forma manual, realiza la evaluación en un orden determinado y ha determinado que este orden cumple con los resultados de C ++. Sin embargo, ni C ++ ni C # hacen análisis en la expresión, es simplemente un conjunto de operaciones sobre algunos valores.

C ++ almacena x en un registro, como C #. Es solo que C # lo almacena antes de evaluar la llamada al método, mientras que C ++ lo hace después . Si cambias el código de C ++ para hacer x = f() + x lugar, al igual que he hecho en C #, espero que obtengas el 1 en la salida.

La parte más importante es que C ++ (y C) simplemente no especificó un orden explícito de operaciones, probablemente porque quería explotar las architectures y plataformas que hacen cualquiera de esos pedidos. Como C # y Java se desarrollaron en un momento en que ya no importa, y dado que podían aprender de todas esas fallas de C / C ++, especificaron un orden explícito de evaluación.

De acuerdo con la especificación del lenguaje Java:

JLS 15.26.2, operadores de asignación de compuestos

Una expresión de asignación compuesta de la forma E1 op= E2 es equivalente a E1 = (T) ((E1) op (E2)) , donde T es el tipo de E1 , excepto que E 1 se evalúa solo una vez.

Este pequeño progtwig demuestra la diferencia y exhibe un comportamiento esperado basado en este estándar.

 public class Start { int X = 0; int f() { X = X + 10; return 1; } public static void main (String[] args) throws java.lang.Exception { Start actualStart = new Start(); Start expectedStart = new Start(); int actual = actualStart.X += actualStart.f(); int expected = (int)(expectedStart.X + expectedStart.f()); int diff = (int)(expectedStart.f() + expectedStart.X); System.out.println(actual == expected); System.out.println(actual == diff); } } 

En orden,

  1. actual se asigna al valor de actualStart.X += actualStart.f() .
  2. expected se asigna al valor de la
  3. resultado de recuperar actualStart.X , que es 0 , y
  4. aplicar el operador de adición a actualStart.X con
  5. el valor de retorno de invocar actualStart.f() , que es 1
  6. y asignando el resultado de 0 + 1 a lo expected .

También declaro diff para mostrar cómo el cambio del orden de invocación cambia el resultado.

  1. diff se asigna al valor de la
  2. el valor de retorno de invocar diffStart.f() , con es 1 , y
  3. aplicar el operador de sum a ese valor con
  4. el valor de diffStart.X (que es 10, un efecto secundario de diffStart.f()
  5. y asignando el resultado de 1 + 10 a diff .

En Java, este no es un comportamiento indefinido.

Editar:

Para abordar su punto con respecto a copias locales de variables. Eso es correcto, pero no tiene nada que ver con la static . Java guarda el resultado de evaluar cada lado (el lado izquierdo primero), luego evalúa el resultado de realizar el operador en los valores guardados.