java c# php c++ expression-evaluation

java vs c# 2018



C++ y PHP vs C#y Java-resultados desiguales (4)

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

#include <iostream> 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 here ).

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

<?php class Simple { public static $X = 0; public static function f() { self::$X = self::$X + 10; return 1; } } $simple = new Simple(); echo "X = " . $simple::$X += $simple::f(); ?>

El resultado es 11 (mira el resultado here ).

Tengo un poco de teoría: estos lenguajes (C # y Java) están haciendo una copia local de la variable estática X en la pila (¿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?


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 de la otra manera?

En este caso (que puede ser diferente para diferentes compiladores 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 llamada predeterminadas que ambos usan, pero nuevamente: para C ++, esta es una operación indefinida, 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", por lo que en su ejemplo, primero lee x y luego llama a f() , mientras que en la mía, primero llama a f() y luego lee x .

Ahora, esto todavía podría ser irrealizable. IL (.NET''s bytecode) tiene + como prácticamente cualquier otro método, pero las optimizaciones realizadas por el compilador 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 compilador compatible siempre debe producir este resultado.

En cualquier caso, ese es un resultado inesperado que has encontrado, y una advertencia: los efectos secundarios en los métodos pueden ser un problema incluso en 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 montón de operaciones sobre algunos valores.

C ++ almacena x en un registro, al igual que C #. Es solo que C # lo almacena antes de evaluar la llamada al método, mientras que C ++ lo hace después . Si cambia el código de C ++ para hacer x = f() + x lugar, al igual que he hecho en C #, espero que obtenga 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 arquitecturas y plataformas que hacen cualquiera de esos pedidos. Dado que C # y Java se desarrollaron en un momento en que esto ya no importa, y como podían aprender de todos esos fallos 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 programa 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. Se asigna diff al valor de la
  2. el valor de retorno de invocar diffStart.f() , con es 1 , y
  3. aplicar el operador de suma 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, esto 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 (primero el lado izquierdo), luego evalúa el resultado de realizar el operador en los valores guardados.


El estándar C ++ establece:

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

§5.17 [expr.ass]

Por lo tanto, como en la misma evaluación, usa 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 compiladores, pero no hay ninguna garantía de que un compilador 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 programa 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 tu compilador 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 que el primero permite que un programa haga cualquier cosa (incluido el bloqueo), mientras que el segundo le permite elegir entre un conjunto de comportamientos permitidos particulares sin dictar qué opción es la correcta.

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

Un buen punto de partida para comprender 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 trato 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 "ciertos puntos específicos" que se llaman puntos de secuencia son (...) después de evaluar 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, la modificación de una variable dos veces entre dos puntos de secuencia consecutivos produce un comportamiento indefinido, pero una llamada de función introduce un punto de secuencia intermedia (en realidad, dos puntos de secuencia intermedia, porque la declaración de retorno crea otro).

Esto significa que el hecho de tener 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 <iostream> 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 compilador está permitido por la especificación del lenguaje C ++ para crear un programa 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 intento humorístico muy común de explicar lo indefinido que realmente significa que todo vale . Los choques son a menudo un resultado más realista de un comportamiento indefinido).

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

#include <iostream> 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 de operadores cambia el comportamiento a especificado , porque los operadores sobrecargados copian la sintaxis de los incorporados pero tienen la semántica de funciones, lo que significa que un operador sobrecargado += de una costumbre el tipo que aparece en una expresión es en realidad una llamada de función . Por lo tanto, no solo se introducen puntos de secuencia sino que desaparece toda la ambigüedad, la expresión se convierte en equivalente a x.operator+=(x.operator+=(10)); orden de evaluación de los argumentos. Esto probablemente 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 programació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é nada de C # y PHP, pero creo que ambos también tienen un orden de evaluación garantizado. C ++, a diferencia de la mayoría de los otros lenguajes de programació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 un intercambio entre robustez y eficiencia . Elegir el lenguaje de programació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 programación en los cuatro idiomas .

Una última palabra:

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

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