precedencia operadores apuntadores c++ c operator-precedence order-of-evaluation

c++ - operadores - Precedencia del operador versus orden de evaluación



precedencia de operadores de apuntadores (6)

Menciona "Las expresiones con operadores de mayor precedencia se evalúan primero".

Voy a repetir lo que dije here . En lo que respecta a los estándares C y C ++, el artículo tiene defectos. La precedencia solo afecta qué tokens se consideran los operandos de cada operador, pero no afecta en modo alguno el orden de evaluación.

Entonces, el enlace solo explica cómo Microsoft implementó las cosas, no cómo funciona el lenguaje en sí.

Los términos "precedencia del operador" y "orden de evaluación" son términos muy comúnmente utilizados en la programación y extremadamente importantes para que el programador lo sepa. Y, por lo que yo los entiendo, los dos conceptos están estrechamente vinculados; uno no puede prescindir del otro cuando habla de expresiones.

Tomemos un ejemplo simple:

int a=1; // Line 1 a = a++ + ++a; // Line 2 printf("%d",a); // Line 3

Ahora, es evidente que la Line 2 conduce al Comportamiento Indefinido, ya que los puntos de Secuencia en C y C ++ incluyen:

  1. Entre la evaluación de los operandos izquierdo y derecho de && (AND lógico), || (O lógico) y operadores de coma. Por ejemplo, en la expresión *p++ != 0 && *q++ != 0 , todos los efectos secundarios de la sub-expresión *p++ != 0 se completan antes de cualquier intento de acceder a q .

  2. Entre la evaluación del primer operando del operador ternario de "interrogación" y el segundo o tercer operando. Por ejemplo, en la expresión a = (*p++) ? (*p++) : 0 a = (*p++) ? (*p++) : 0 hay un punto de secuencia después del primer *p++ , lo que significa que ya se ha incrementado en el momento en que se ejecuta la segunda instancia.

  3. Al final de una expresión completa. Esta categoría incluye instrucciones de expresión (como la asignación a=b; ), declaraciones de devolución, las expresiones de control de las instrucciones if, switch, while o do-while y las tres expresiones en una instrucción for.

  4. Antes de que se ingrese una función en una llamada de función. El orden en que se evalúan los argumentos no se especifica, pero este punto de secuencia significa que todos sus efectos secundarios están completos antes de que se ingrese la función. En la expresión f(i++) + g(j++) + h(k++) , f se invoca con un parámetro del valor original de i , pero i se incrementa antes de ingresar al cuerpo de f . De forma similar, j y k se actualizan antes de ingresar g y h respectivamente. Sin embargo, no se especifica en qué orden f() , g() , h() se ejecutan, ni en qué orden i , j , k se incrementan. Los valores de j y k en el cuerpo de f son, por lo tanto, indefinidos. 3 Tenga en cuenta que una llamada de función f(a,b,c) no es un uso del operador de coma y el orden de evaluación para a , b y c no está especificado.

  5. En un retorno de función, después de que el valor de retorno se copia en el contexto de llamada. (Este punto de secuencia solo se especifica en el estándar de C ++, está presente solo implícitamente en C.)

  6. Al final de un inicializador; por ejemplo, después de la evaluación de 5 en la declaración int a = 5; .

Por lo tanto, yendo por el Punto # 3:

Al final de una expresión completa. Esta categoría incluye instrucciones de expresión (como la asignación a = b;), declaraciones de devolución, las expresiones de control de las instrucciones if, switch, while o do-while y las tres expresiones en una instrucción for.

Line 2 claramente conduce a un comportamiento indefinido. Esto muestra cómo el comportamiento indefinido está estrechamente relacionado con los puntos de secuencia .

Ahora tomemos otro ejemplo:

int x=10,y=1,z=2; // Line 4 int result = x<y<z; // Line 5

Ahora es evidente que la Line 5 hará que el result variable guarde 1 .

Ahora la expresión x<y<z en la Line 5 se puede evaluar como:

x<(y<z) o (x<y)<z . En el primer caso, el valor del result será 0 y en el segundo caso, el result será 1 . Pero sabemos que cuando la Operator Precedence del Operator Precedence es Equal/Same : la Associativity entra en juego, por lo tanto, se evalúa como (x<y)<z .

Esto es lo que se dice en este artículo de MSDN :

La precedencia y la asociatividad de los operadores C afectan la agrupación y evaluación de operandos en expresiones. La precedencia de un operador solo tiene sentido si están presentes otros operadores con mayor o menor prioridad. Las expresiones con operadores de mayor precedencia se evalúan primero. La precedencia también se puede describir con la palabra "vinculante". Se dice que los operadores con una precedencia más alta tienen un enlace más estricto.

Ahora, sobre el artículo anterior:

Menciona "Las expresiones con operadores de mayor precedencia se evalúan primero".

Puede sonar incorrecto. Pero, creo que el artículo no está diciendo algo malo si consideramos que () también es un operador x<y<z es igual que (x<y)<z . Mi razonamiento es que si la asociatividad no entra en juego, entonces la evaluación de expresiones completas se volverá ambigua ya que < no es un Punto de Secuencia .

Además, otro enlace que encontré dice esto sobre Precedencia del operador y Asociatividad :

Esta página enumera los operadores C por orden de precedencia (de mayor a menor). Su asociatividad indica en qué orden se aplican operadores de igual precedencia en una expresión.

Entonces, tomando el segundo ejemplo de int result=x<y<z , podemos ver aquí que hay en las 3 expresiones, x , y y z , ya que, la forma más simple de una expresión consiste en una sola constante literal u objeto . Por lo tanto, el resultado de las expresiones x , y , z estaría allí en valores , es decir, 10 , 1 y 2 respectivamente. Por lo tanto, ahora podemos interpretar que x<y<z es 10<1<2 .

Ahora, ¿no entra en juego la asociatividad ya que ahora tenemos 2 expresiones para evaluar, ya sea 10<1 o 1<2 y dado que la precedencia del operador es la misma, se evalúan de izquierda a derecha ?

Tomando este último ejemplo como mi argumento:

int myval = ( printf("Operator/n"), printf("Precedence/n"), printf("vs/n"), printf("Order of Evaluation/n") );

Ahora en el ejemplo anterior, dado que el operador de comma tiene la misma precedencia, las expresiones se evalúan de left-to-right y el valor de retorno de la última printf() se almacena en myval .

En SO / IEC 9899: 201x en J.1 Comportamiento no especificado , menciona:

El orden en el que se evalúan las subexpresiones y el orden en que se producen los efectos secundarios, excepto lo especificado para los operadores function-call (), &&, ||,?: Y comma (6.5).

Ahora me gustaría saber si sería incorrecto decir:

El orden de evaluación depende de la precedencia de los operadores, dejando casos de comportamiento no especificado.

Me gustaría ser corregido si se cometieran errores en algo que dije en mi pregunta. La razón por la que publiqué esta pregunta es debido a la confusión creada en mi mente por el artículo de MSDN. ¿Está en error o no?


Creo que es solo el

a++ + ++a

epxression problemática, porque

a = a++ + ++a;

encaja primero en 3. pero luego en la regla 6.: evaluación completa antes de la asignación.

Asi que,

a++ + ++a

obtiene para a = 1 totalmente evaluado para:

1 + 3 // left to right, or 2 + 2 // right to left

El resultado es el mismo = 4.

Un

a++ * ++a // or a++ == ++a

Tendría resultados indefinidos. ¿No es así?


La única forma en que la precedencia influye en el orden de la evaluación es que crea dependencias; de lo contrario, los dos son ortogonales. Ha elegido cuidadosamente ejemplos triviales donde las dependencias creadas por precedencia terminan definiendo completamente el orden de evaluación, pero esto no es generalmente cierto. Y tampoco olvide que muchas expresiones tienen dos efectos: dan como resultado un valor y tienen efectos secundarios. No es necesario que estas dos ocurran juntas, de modo que incluso cuando las dependencias fuerzan un orden específico de evaluación, este es solo el orden de evaluación de los valores; no tiene efecto sobre los efectos secundarios.


La precedencia no tiene nada que ver con el orden de evaluación y viceversa.

Precedence reglas de Precedence describen cómo una expresión sin correspondencia debe estar entre paréntesis cuando la expresión mezcla diferentes tipos de operadores. Por ejemplo, la multiplicación es de mayor precedencia que la suma, por lo que 2 + 3 x 4 es equivalente a 2 + (3 x 4) , no a (2 + 3) x 4 .

El orden de las reglas de evaluación describe el orden en que se evalúa cada operando en una expresión.

Toma un ejemplo

y = ++x || --y;

Por regla de precedencia del operador, será entre paréntesis ya que ( ++/-- tiene una precedencia mayor que || que tiene una precedencia mayor que = ):

y = ( (++x) || (--y) )

El orden de evaluación de OR lógico || afirma que (C11 6.5.14)

el || operador garantiza evaluación de izquierda a derecha .

Esto significa que el operando de la izquierda, es decir, la subexpresión (x++) se evaluará primero. Debido al comportamiento de cortocircuito; Si el primer operando compara desigual a 0 , el segundo operando no se evalúa , el operando derecho no se evaluará aunque esté entre paréntesis antes que (++x) || (--y) (++x) || (--y) .


Sí, el artículo de MSDN está en error, al menos con respecto al estándar C y C ++ 1 .

Habiendo dicho eso, permítanme comenzar con una nota sobre terminología: en el estándar de C ++, (en su mayoría, hay algunos errores) usan "evaluación" para referirse a la evaluación de un operando, y "cálculo de valor" para referirse a llevando a cabo una operación. Entonces, cuando (por ejemplo) haces a + b , cada uno de a y b es evaluado, entonces el cálculo del valor se lleva a cabo para determinar el resultado.

Está claro que el orden de los cómputos de valor está (mayormente) controlado por precedencia y asociatividad: el control de los cálculos de valores es básicamente la definición de lo que son la precedencia y la asociatividad. El resto de esta respuesta usa "evaluación" para referirse a la evaluación de operandos, no para valorar los cálculos.

Ahora, en cuanto a que el orden de evaluación está determinado por la precedencia, ¡no, no lo es! Es tan simple como eso. Solo por ejemplo, consideremos su ejemplo de x<y<z . De acuerdo con las reglas de asociatividad, esto analiza como (x<y)<z . Ahora, considere evaluar esta expresión en una máquina de pila. Está perfectamente permitido que haga algo como esto:

push(z); // Evaluates its argument and pushes value on stack push(y); push(x); test_less(); // compares TOS to TOS(1), pushes result on stack test_less();

Esto evalúa z antes de x o y , pero aún evalúa (x<y) , luego compara el resultado de esa comparación con z , tal como se supone que debe hacerlo.

Resumen: el orden de evaluación es independiente de la asociatividad.

La precedencia es de la misma manera. Podemos cambiar la expresión a x*y+z , y todavía evaluar z antes de x o y :

push(z); push(y); push(x); mul(); add();

Resumen: el orden de evaluación es independiente de la precedencia.

Cuando / si agregamos efectos secundarios, esto sigue siendo el mismo. Creo que es educativo pensar que los efectos secundarios se llevan a cabo mediante un hilo de ejecución separado, con una join en el siguiente punto de secuencia (por ejemplo, el final de la expresión). Entonces algo así como a=b++ + ++c; podría ser ejecutado algo como esto:

push(a); push(b); push(c+1); side_effects_thread.queue(inc, b); side_effects_thread.queue(inc, c); add(); assign(); join(side_effects_thread);

Esto también muestra por qué una aparente dependencia no necesariamente afecta el orden de evaluación. Aunque a es el objetivo de la tarea, esto todavía evalúa a antes de evaluar ya sea c . También tenga en cuenta que aunque lo he escrito como "hilo" más arriba, esto también podría ser un conjunto de hilos, todos ejecutándose en paralelo, por lo que tampoco obtiene ninguna garantía sobre el orden de un incremento frente a otro.

A menos que el hardware tenga soporte directo (y barato ) para colas seguras para subprocesos, esto probablemente no se usaría en una implementación real (y aun así no es muy probable). Poner algo en una cola segura para subprocesos normalmente tendrá bastante más sobrecarga que hacer un solo incremento, por lo que es difícil imaginar que alguien lo haga en realidad. Conceptualmente, sin embargo, la idea se ajusta a los requisitos del estándar: cuando utiliza una operación de incremento / decremento pre / post, está especificando una operación que ocurrirá en algún momento después de que se evalúe esa parte de la expresión, y se completará en el siguiente punto de secuencia.

Edición: aunque no está enhebrando exactamente, algunas arquitecturas permiten tal ejecución en paralelo. Por un par de ejemplos, los procesadores Intel Itanium y VLIW como algunos DSP permiten que un compilador designe varias instrucciones para ser ejecutadas en paralelo. La mayoría de las máquinas VLIW tienen un tamaño específico de "paquete" de instrucciones que limita el número de instrucciones ejecutadas en paralelo. El Itanium también usa paquetes de instrucciones, pero designa un bit en un paquete de instrucciones para decir que las instrucciones en el paquete actual se pueden ejecutar en paralelo con las del paquete siguiente. Al utilizar mecanismos como este, obtienes instrucciones para ejecutar en paralelo, como si hubieras utilizado varios hilos en arquitecturas con las que la mayoría de nosotros estamos más familiarizados.

Resumen: el orden de evaluación es independiente de las aparentes dependencias

Cualquier intento de usar el valor antes del siguiente punto de secuencia proporciona un comportamiento indefinido, en particular, el "otro subproceso" modifica (potencialmente) esa información durante ese tiempo, y no tiene forma de sincronizar el acceso con el otro subproceso. Cualquier intento de usarlo conduce a un comportamiento indefinido.

Solo por un ejemplo (por cierto, ahora bastante descabellado), piense que su código se ejecuta en una máquina virtual de 64 bits, pero el hardware real es un procesador de 8 bits. Cuando incrementa una variable de 64 bits, ejecuta una secuencia como:

load variable[0] increment store variable[0] for (int i=1; i<8; i++) { load variable[i] add_with_carry 0 store variable[i] }

Si lees el valor en algún lugar en el medio de esa secuencia, puedes obtener algo con solo algunos de los bytes modificados, entonces lo que obtienes no es el valor anterior ni el nuevo.

Este ejemplo exacto puede ser bastante descabellado, pero una versión menos extrema (por ejemplo, una variable de 64 bits en una máquina de 32 bits) es bastante común.

Conclusión

El orden de evaluación no depende de la precedencia, la asociatividad o (necesariamente) de las dependencias aparentes. Intentar utilizar una variable a la que se le haya aplicado un incremento / decremento pre / post en cualquier otra parte de una expresión realmente da un comportamiento completamente indefinido. Si bien es poco probable que se produzca un bloqueo real, definitivamente no se garantiza que obtenga el valor antiguo o el nuevo; podría obtener algo completamente distinto.

1 No he revisado este artículo en particular, pero bastantes artículos de MSDN hablan sobre Managed C ++ y / o C ++ / CLI de Microsoft, pero hacen poco o nada para señalar que no se aplican a C o C ++ estándar. Esto puede dar la falsa apariencia de que afirman que las reglas que han decidido aplicar a sus propios idiomas se aplican realmente a los idiomas estándar. En estos casos, los artículos no son técnicamente falsos, simplemente no tienen nada que ver con C o C ++ estándar. Si intenta aplicar esas declaraciones al estándar C o C ++, el resultado es falso.


Una buena forma de ver esto es tomar el árbol de expresiones.

Si tiene una expresión, digamos x+y*z , puede reescribirla en un árbol de expresiones:

Aplicando las reglas de prioridad y asociatividad:

x + ( y * z )

Después de aplicar las reglas de prioridad y asociatividad, puede olvidarse de ellas con seguridad.

En forma de árbol:

x + y * z

Ahora las hojas de esta expresión son x , y y z . Lo que esto significa es que puede evaluar x , y y z en el orden que desee, y también significa que puede evaluar el resultado de * y x en cualquier orden.

Ahora que estas expresiones no tienen efectos secundarios, realmente no te importa. Pero si lo hacen, el orden puede cambiar el resultado, y dado que el orden puede ser cualquier cosa que el compilador decida, usted tiene un problema.

Ahora, los puntos de secuencia traen un poco de orden a este caos. Efectivamente cortan el árbol en secciones.

x + y * z, z = 10, x + y * z

después de prioridad y asociatividad

x + ( y * z ) , z = 10, x + ( y * z)

el árbol:

x + y * z , ------------ z = 10 , ------------ x + y * z

La parte superior del árbol se evaluará antes de la mitad, y el medio antes de la parte inferior.