¿Por qué el operador ternario con comas evalúa solo una expresión en el caso verdadero?

119

Actualmente estoy aprendiendo C ++ con el libro C ++ Primer y uno de los ejercicios del libro es:

Explica qué hace la siguiente expresión: someValue ? ++x, ++y : --x, --y

¿Qué sabemos? Sabemos que el operador ternario tiene mayor precedencia que el operador de coma. Con los operadores binarios esto fue bastante fácil de entender, pero con el operador ternario estoy luchando un poco. Con los operadores binarios "tener mayor precedencia" significa que podemos usar paréntesis alrededor de la expresión con mayor precedencia y no cambiará la ejecución.

Para el operador ternario haría:

(someValue ? ++x, ++y : --x, --y)

dando como resultado efectivamente el mismo código, lo que no me ayuda a comprender cómo el compilador agrupará el código.

Sin embargo, al probar con un compilador de C ++, sé que la expresión se compila y no sé qué :podría representar un operador por sí mismo. Entonces, el compilador parece interpretar correctamente el operador ternario.

Luego ejecuté el programa de dos maneras:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Resultados en:

11 10

Mientras que por otro lado someValue = falseimprime:

9 9

¿Por qué el compilador de C ++ generaría código que para la rama verdadera del operador ternario solo aumenta x, mientras que para la rama falsa del ternario disminuye tanto xy y?

Incluso fui tan lejos como para poner paréntesis alrededor de la rama verdadera de esta manera:

someValue ? (++x, ++y) : --x, --y;

pero todavía resulta en 11 10.

Aufziehvogel
fuente
5
La "precedencia" es solo un fenómeno emergente en C ++. Puede ser más sencillo mirar directamente la gramática del idioma y ver cómo funcionan las expresiones.
Kerrek SB
26
No nos importa que mucho de los principios. :-) El hecho de que tenga que preguntar esto aquí indica que el código nunca pasará una revisión de código por parte de sus compañeros programadores. Eso hace que el conocimiento sobre cómo funciona esto realmente sea menos útil. A menos que quieras participar en el Concurso C ofuscado , por supuesto.
Bo Persson
5
@BoPersson sin ejemplos como este de los que aprender, los futuros revisores nunca sabrán por qué deberían rechazar esto de la producción.
Leushenko
8
@Leushenko - Las campanas de advertencia deberían sonar de todos modos. Múltiples incrementos y decrementos en la misma declaración (¡ding, ding, ding!). Un operador ternario cuando puedes usar if-else (¡ding, ding, ding!). Espera, ¿son esas comas el temido operador de coma? (¡Ding, DING, DING!) Con todos esos operadores, ¿podría haber algo de precedencia? (¡ding, ding, ding!) Así que nunca podremos usar eso. Entonces, ¿por qué perder el tiempo averiguando qué hace, si es que hace algo?
Bo Persson
4
Minor nit: el nombre de ?es el operador condicional . El término operador ternario simplemente significa un operador con tres operandos. El operador condicional es un ejemplo de un operador ternario, pero un lenguaje podría (teóricamente) tener múltiples operadores ternarios.
bta

Respuestas:

122

Como dijo @Rakete en su excelente respuesta, esto es complicado. Me gustaría agregar un poco más a eso.

El operador ternario debe tener la forma:

expresión-lógica-o-expresión ? expresión : -asignación

Entonces tenemos las siguientes asignaciones:

  • someValue: lógica-o-expresión
  • ++x, ++y: expresión
  • ??? ¿es expresión-asignación --x, --y o solo --x?

De hecho, es solo --xporque una expresión de asignación no se puede analizar como dos expresiones separadas por una coma (de acuerdo con las reglas gramaticales de C ++), por --x, --ylo que no se puede tratar como una expresión de asignación .

Lo que da como resultado que la porción de expresión ternaria (condicional) se vea así:

someValue?++x,++y:--x

Puede ser útil en aras de la legibilidad considerar ++x,++yque se calcula como si estuviera entre paréntesis (++x,++y); todo lo contenido entre ?y :se secuenciará después del condicional. (Los pondré entre paréntesis para el resto de la publicación).

y evaluado en este orden:

  1. someValue?
  2. (++x,++y)o --x(dependiendo del boolresultado de 1.)

Esta expresión luego se trata como la subexpresión izquierda de un operador de coma, siendo la subexpresión derecha --y, así:

(someValue?(++x,++y):--x), --y;

Lo que significa que el lado izquierdo es una expresión de valor descartado , lo que significa que definitivamente se evalúa, pero luego evaluamos el lado derecho y lo devolvemos.

Entonces, ¿qué pasa cuando someValuees true?

  1. (someValue?(++x,++y):--x)se ejecuta e incrementa xy ypara ser 11y11
  2. La expresión de la izquierda se descarta (aunque los efectos secundarios del incremento permanecen)
  3. Evaluamos el lado derecho del operador de coma:, --yque luego yregresa a10

Para "corregir" el comportamiento, puede agruparlo --x, --ycon paréntesis para transformarlo en una expresión primaria que sea una entrada válida para una expresión de asignación *:

someValue?++x,++y:(--x, --y);

* Es una cadena larga bastante divertida que conecta una expresión de asignación con una expresión primaria:

asignación-expresión --- (puede consistir en) -> condicional-expresión -> lógico-o-expresión -> lógico-y-expresión -> inclusive-o-expresión -> exclusiva-o-expresión - -> expresión-y -> expresión-igualdad -> expresión-relacional -> expresión-cambio -> expresión-aditiva -> expresión-multiplicativa -> expresión-pm -> expresión-moldeada -> expresión-unaria -> expresión-postfijo -> expresión-primaria

AndyG
fuente
10
Gracias por tomarse la molestia de desentrañar las reglas gramaticales; al hacerlo, se demuestra que hay más en la gramática de C ++ de lo que encontrará en la mayoría de los libros de texto.
sdenham
4
@sdenham: Cuando la gente pregunta por qué los "lenguajes orientados a expresiones" son agradables (es decir, cuándo { ... }pueden tratarse como una expresión), ahora tengo una respuesta => es para evitar tener que introducir un operador de coma que se comporta de formas tan complicadas.
Matthieu M.
¿Podrías darme un enlace para leer sobre la assignment-expressioncadena?
MiP
@MiP: Lo levanté del estándar en sí, lo puede encontrar en gram.expr
AndyG
88

Vaya, eso es complicado.

El compilador ve su expresión como:

(someValue ? (++x, ++y) : --x), --y;

El operador ternario necesita a :, no puede sostenerse por sí mismo en ese contexto, pero después de él, no hay ninguna razón por la que la coma deba pertenecer al caso falso.

Ahora podría tener más sentido por qué obtiene ese resultado. Si someValuees cierto, entonces ++x, ++yy --yejecute, que no cambia efectivamente ypero agrega uno a x.

Si someValuees falso, entonces --xy --yse ejecutan, disminuyendo ambos en uno.

Rakete1111
fuente
42

¿Por qué el compilador de C ++ generaría código que para la rama verdadera del operador ternario solo incrementa x

Usted malinterpretó lo que sucedió. La rama verdadera incrementa tantox y y. Sin embargo, yse decrementa inmediatamente después de eso, incondicionalmente.

Así es como sucede esto: dado que el operador condicional tiene mayor precedencia que el operador de coma en C ++ , el compilador analiza la expresión de la siguiente manera:

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Tenga en cuenta el "huérfano" --ydespués de la coma. Esto es lo que lleva a decrementary inicialmente incrementado.

Incluso fui tan lejos como para poner paréntesis alrededor de la rama verdadera de esta manera:

someValue ? (++x, ++y) : --x, --y;

Estaba en el camino correcto, pero puso entre paréntesis una rama incorrecta: puede solucionar esto entre paréntesis en la rama else, así:

someValue ? ++x, ++y : (--x, --y);

Demostración (impresiones 11 11)

dasblinkenlight
fuente
5

Su problema es que la expresión ternaria no tiene mayor precedencia que la coma. De hecho, C ++ no se puede describir con precisión simplemente por la precedencia, y es exactamente la interacción entre el operador ternario y la coma donde se descompone.

a ? b++, c++ : d++

se trata como:

a ? (b++, c++) : d++

(la coma se comporta como si tuviera mayor precedencia). Por otra parte,

a ? b++ : c++, d++

se trata como:

(a ? b++ : c++), d++

y el operador ternario tiene mayor precedencia.

Martin Bonner apoya a Monica
fuente
Creo que esto todavía está dentro del ámbito de la precedencia, ya que solo hay un análisis válido para la línea media, ¿verdad? Sin embargo, sigue siendo un ejemplo útil
sudo rm -rf slash
2

Un punto que se ha pasado por alto en las respuestas (aunque se ha mencionado en los comentarios) es que el operador condicional se usa invariablemente (¿previsto por diseño?) En código real como un atajo para asignar uno de dos valores a una variable.

Entonces, el contexto más amplio sería:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Lo cual es absurdo a primera vista, por lo que los crímenes son múltiples:

  • El lenguaje permite efectos secundarios ridículos en una tarea.
  • El compilador no le advirtió que estaba haciendo cosas extrañas.
  • El libro parece centrarse en preguntas "trampa". Uno solo puede esperar que la respuesta en la parte de atrás sea "Lo que hace esta expresión es depender de casos extremos extraños en un ejemplo artificial para producir efectos secundarios que nadie espera. Nunca haga esto".
Taryn
fuente
1
Asignación de una de las dos variables es el caso habitual de que el operador ternario, pero no son ocasiones en que es útil tener una forma de expresión de if(por ejemplo, la expresión de incremento en un bucle for). El contexto más amplio bien podría ser for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))con un bucle que puede modificar xy yde forma independiente.
Martin Bonner apoya a Monica el
@MartinBonner No estoy convencido, y este ejemplo parece demostrar bastante bien el punto de bo-perrson, como citó a Tony Hoare.
Taryn