Por que o operador ternário com vírgulas avalia apenas uma expressão no caso verdadeiro?


119

Atualmente, estou aprendendo C ++ com o livro C ++ Primer e um dos exercícios do livro é:

Explique o que a seguinte expressão faz: someValue ? ++x, ++y : --x, --y

O que nós sabemos? Sabemos que o operador ternário tem uma precedência mais alta que o operador de vírgula. Com operadores binários, isso era muito fácil de entender, mas com o operador ternário eu estou lutando um pouco. Com operadores binários "ter maior precedência" significa que podemos usar parênteses em torno da expressão com maior precedência e isso não mudará a execução.

Para o operador ternário, eu faria:

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

resultando efetivamente no mesmo código que não me ajuda a entender como o compilador agrupará o código.

No entanto, ao testar com um compilador C ++, sei que a expressão é compilada e não sei o que um :operador pode representar por si só. Portanto, o compilador parece interpretar o operador ternário corretamente.

Então eu executei o programa de duas maneiras:

#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;
}

Resulta em:

11 10

Enquanto, por outro lado someValue = false, imprime:

9 9

Por que o compilador C ++ geraria código que apenas para o ramo verdadeiro do operador ternário é incrementado x, enquanto para o ramo falso do ternário ele diminui tanto xe y?

Até cheguei a colocar parênteses em torno do ramo verdadeiro assim:

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

mas ainda resulta em 11 10.


5
"Precedência" é apenas um fenômeno emergente em C ++. Pode ser mais simples olhar diretamente para a gramática da linguagem e ver como as expressões funcionam.
Kerrek SB

26
Nós não nos importamos que muito sobre os princípios. :-) O fato de você precisar fazer isso aqui indica que o código nunca será aprovado em uma revisão de código feita por seus colegas programadores. Isso torna o conhecimento sobre como isso realmente funciona menos que útil. A menos que você queira participar do concurso C ofuscado , é claro.
Bo Persson

5
@BoPersson sem exemplos como esse para aprender, futuros revisores nunca descobrirão por que devem rejeitar isso na produção.
Leushenko

8
@ Leushenko - Os sinos de aviso devem estar tocando de qualquer maneira. Múltiplos incrementos e decréscimos na mesma instrução (ding, ding, ding!). Um operador ternário quando você pode usar o if-else (ding, ding, ding!). Espere, essas vírgulas são o temido operador de vírgula? (ding, DING, DING!) Com todos esses operadores, poderia haver alguma coisa de precedência? (ding, ding, ding!) Portanto, nunca poderemos usar isso. Então, por que perder tempo tentando descobrir o que faz, se é que existe alguma coisa?
Bo Persson

4
Nit menor: o nome para ?é o operador condicional . O termo operador ternário significa simplesmente um operador com três operandos. O operador condicional é um exemplo de operador ternário, mas uma linguagem pode (teoricamente) ter vários operadores ternários.
BTA

Respostas:


122

Como o @Rakete disse em sua excelente resposta, isso é complicado. Eu gostaria de acrescentar um pouco a isso.

O operador ternário deve ter a forma:

expressão lógica ou ? expressão : atribuição-expressão

Portanto, temos os seguintes mapeamentos:

  • someValue: expressão lógica ou
  • ++x, ++y: expressão
  • ??? é expressão de atribuição --x, --y ou apenas --x?

De fato, é apenas --xporque uma expressão de atribuição não pode ser analisada como duas expressões separadas por vírgula (de acordo com as regras gramaticais do C ++), portanto, --x, --ynão pode ser tratada como uma expressão de atribuição .

O que resulta na parte da expressão ternária (condicional) da seguinte forma:

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

Para facilitar a leitura, ++x,++ypode ser útil calcular como entre parênteses (++x,++y); qualquer coisa contida entre ?e :será sequenciada após a condicional. (Vou colocá-los entre parênteses no restante da postagem).

e avaliados nesta ordem:

  1. someValue?
  2. (++x,++y)ou --x(dependendo do boolresultado de 1.)

Essa expressão é então tratada como a subexpressão esquerda de um operador de vírgula, com a subexpressão direita sendo --yassim:

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

O que significa que o lado esquerdo é uma expressão de valor descartado , o que significa que é definitivamente avaliada, mas avaliamos o lado direito e o retornamos.

Então, o que acontece quando someValueé true?

  1. (someValue?(++x,++y):--x)executa e incrementa xe ypara ser 11e11
  2. A expressão esquerda é descartada (embora os efeitos colaterais do incremento permaneçam)
  3. Avaliamos o lado direito do operador de vírgula:, --yque depois diminui de yvolta para10

Para "consertar" o comportamento, você pode agrupar --x, --yparênteses para transformá-lo em uma expressão principal, que é uma entrada válida para uma expressão de atribuição *:

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

* É uma cadeia longa bastante engraçada que conecta uma expressão de atribuição a uma expressão primária:

expressão de atribuição --- (pode consistir em) -> expressão condicional -> expressão ou lógica -> expressão e lógica -> expressão ou inclusão -> expressão ou inclusão -> expressão ou exclusiva - -> and-expression -> igualdade-expressão -> expressão relacional -> shift-expression -> expressão aditiva -> expressão aditiva -> expressão multiplicativa -> expressão pm -> expressão express -> expressão unária -> expressão postfix -> expressão primária


10
Obrigado por se dar ao trabalho de desvendar as regras gramaticais; isso mostra que há mais na gramática do C ++ do que você encontrará na maioria dos livros.
sdenham

4
@sdenham: Quando as pessoas perguntam por que "linguagens orientadas a expressões" são legais (ou seja, quando { ... }podem ser tratadas como expressões), agora eu tenho uma resposta => é para evitar ter que introduzir um operador de vírgula que se comporte de maneiras tão complicadas.
Matthieu M.

Você poderia me dar um link para ler sobre a assignment-expressioncadeia?
MiP

@MiP: Eu o tirei do padrão em si, você pode encontrá-lo em gram.expr #
AndyG

88

Uau, isso é complicado.

O compilador vê sua expressão como:

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

O operador ternário precisa de a :, não pode permanecer por si só nesse contexto, mas depois disso, não há razão para que a vírgula deva pertencer ao caso falso.

Agora, pode fazer mais sentido o motivo pelo qual você obtém essa saída. Se someValuefor verdade, então ++x, ++ye --yseja executado, o que não muda efetivamente, ymas adiciona um a x.

Se someValuefor falso, então --xe --ysão executados, diminuindo os dois por um.


42

Por que o compilador C ++ geraria código que, para a ramificação verdadeira do operador ternário, apenas incrementa x

Você interpretou mal o que aconteceu. O ramo verdadeiro incrementa ambos xe y. Contudo,y é diminuído imediatamente depois disso, incondicionalmente.

Aqui está como isso acontece: como o operador condicional tem maior precedência do que o operador vírgula em C ++ , o compilador analisa a expressão da seguinte maneira:

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

Observe o "órfão" --yapós a vírgula. Isso é o que leva à diminuição yque foi inicialmente incrementada.

Até cheguei a colocar parênteses em torno do ramo verdadeiro assim:

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

Você estava no caminho certo, mas colocou um ramo errado entre parênteses: você pode corrigi-lo entre parênteses do ramo else, assim:

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

Demo (impressões 11 11)


5

Seu problema é que a expressão ternária realmente não tem precedência mais alta que vírgula. De fato, o C ++ não pode ser descrito com precisão simplesmente por precedência - e é exatamente a interação entre o operador ternário e a vírgula onde ocorre a quebra.

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

é tratado como:

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

(vírgula se comporta como se tivesse maior precedência). Por outro lado,

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

é tratado como:

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

e o operador ternário tem maior precedência.


Eu acho que isso ainda está dentro do domínio da precedência, pois existe apenas uma análise válida para a linha do meio, certo? Ainda um exemplo útil embora
sudo rm -rf cortar

2

Um ponto que foi esquecido nas respostas (embora abordado nos comentários) é que o operador condicional é invariavelmente usado (destinado ao design?) No código real como um atalho para atribuir um dos dois valores a uma variável.

Portanto, o contexto maior seria:

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

O que é absurdo, por isso os crimes são múltiplos:

  • O idioma permite efeitos colaterais ridículos em uma tarefa.
  • O compilador não avisou que você estava fazendo coisas bizarras.
  • O livro parece estar se concentrando em perguntas 'truques'. Só podemos esperar que a resposta na parte de trás seja "O que essa expressão faz é depender de casos extremos estranhos em um exemplo artificial para produzir efeitos colaterais que ninguém espera. Nunca faça isso".

1
A atribuição de uma de duas variáveis é o caso habitual do operador ternário, mas não são ocasiões em que é útil ter uma forma de expressão if(por exemplo, a expressão de incremento em um loop). O contexto maior pode muito bem estar for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))com um loop que pode modificar xe de forma yindependente.
Martin Bonner apoia Monica

@MartinBonner Não estou convencido, e este exemplo parece fazer muito bem o argumento de bo-perrson, como ele citou Tony Hoare.
quer
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.