Qual é a resposta correta para cout << a ++ << a ;?

98

Recentemente, em uma entrevista, houve o seguinte tipo de questão objetiva.

int a = 0;
cout << a++ << a;

Respostas:

uma. 10
b. 01
c. comportamento indefinido

Respondi a opção b, ou seja, a saída seria "01".

Mas, para minha surpresa, mais tarde, um entrevistador me disse que a resposta correta é a opção c: indefinida.

Agora, eu conheço o conceito de pontos de sequência em C ++. O comportamento é indefinido para a seguinte declaração:

int i = 0;
i += i++ + i++;

mas de acordo com meu entendimento para a declaração cout << a++ << a, o ostream.operator<<()seria chamado duas vezes, primeiro com ostream.operator<<(a++)e depois ostream.operator<<(a).

Eu também verifiquei o resultado no compilador VS2010 e sua saída também é '01'.

pravs
fonte
30
Você pediu uma explicação? Costumo entrevistar candidatos em potencial e estou bastante interessado em receber perguntas, mostra interesse.
Brady
3
@jrok É um comportamento indefinido. Qualquer coisa que a implementação fizer (incluindo enviar um e-mail insultuoso em seu nome para seu chefe) está em conformidade.
James Kanze
2
Esta questão clama por uma resposta do C ++ 11 (a versão atual do C ++) que não mencione os pontos de sequência. Infelizmente, não tenho conhecimento suficiente sobre a substituição de pontos de sequência no C ++ 11.
CB Bailey
3
Se não fosse indefinido, definitivamente não poderia ser 10, seria 01ou 00. ( c++sempre avaliará o valor cobtido antes de ser incrementado). E mesmo que não fosse indefinido, ainda seria terrivelmente confuso.
esquerda por volta de
2
Você sabe, quando li o título “cout << c ++ << c”, momentaneamente pensei nisso como uma declaração sobre a relação entre as linguagens C e C ++, e alguma outra chamada “cout”. Você sabe, como se alguém estivesse dizendo que pensava que “cout” era muito inferior a C ++ e que C ++ era muito inferior a C - e provavelmente por transitividade que “cout” era muito, muito inferior a C. :)
tchrist

Respostas:

145

Você pode pensar em:

cout << a++ << a;

Como:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++ garante que todos os efeitos colaterais de avaliações anteriores foram executados em pontos de sequência . Não há pontos de sequência entre a avaliação dos argumentos da função, o que significa que o argumento apode ser avaliado antes std::operator<<(std::cout, a++)ou depois do argumento . Portanto, o resultado do acima é indefinido.


Atualização C ++ 17

No C ++ 17, as regras foram atualizadas. Em particular:

Em uma expressão do operador shift E1<<E2e E1>>E2, cada cálculo de valor e efeito colateral de E1é sequenciado antes de cada cálculo de valor e efeito colateral de E2.

O que significa que requer o código para produzir o resultado b, que produz 01.

Consulte P0145R3 Refining Expression Evaluation Order para Idiomatic C ++ para obter mais detalhes.

Maxim Egorushkin
fonte
@Maxim: Obrigado pela explicação. Com as chamadas que você expôs, seria um comportamento indefinido. Mas agora, eu tenho mais uma pergunta (pode ser mais uma questão, e estou perdendo algo básico e pensando alto) Como você deduziu que a versão global de std :: operator << () seria chamada em vez de ostream :: operator < <() versão do membro. Na depuração, estou chegando a uma versão de membro da chamada ostream :: operator << () em vez da versão global e essa é a razão pela qual inicialmente pensei que a resposta seria 01
pravs
@Maxim Não que seja diferente, mas como ctem tipo int, operator<<aqui estão as funções-membro.
James Kanze
2
@pravs: se operator<<é uma função membro ou uma função autônoma não afeta os pontos de sequência.
Maxim Egorushkin
11
O 'ponto de sequência' não é mais usado no padrão C ++. Era impreciso e foi substituído pela relação 'sequenciado antes / sequenciado depois'.
Rafał Dowgird
2
So the result of the above is undefined.Sua explicação é válida apenas para não especificado , não para indefinido . JamesKanze explicou como ficou ainda mais indefinido em sua resposta .
Deduplicator
68

Tecnicamente, no geral, esse é o comportamento indefinido .

Mas, existem dois aspectos importantes para a resposta.

A declaração do código:

std::cout << a++ << a;

é avaliado como:

std::operator<<(std::operator<<(std::cout, a++), a);

O padrão não define a ordem de avaliação dos argumentos para uma função.
Ou então:

  • std::operator<<(std::cout, a++) é avaliado primeiro ou
  • aé avaliado primeiro ou
  • pode ser qualquer ordem definida pela implementação.

Este pedido não é especificado [Ref 1] de acordo com o padrão.

[Ref 1] C ++ 03 5.2.2 Chamada de função
Para 8

A ordem de avaliação dos argumentos não é especificada . Todos os efeitos colaterais das avaliações de expressão de argumento têm efeito antes que a função seja inserida. A ordem de avaliação da expressão pós-fixada e da lista de expressões de argumento não é especificada.

Além disso, não há ponto de sequência entre a avaliação de argumentos para uma função, mas um ponto de sequência existe apenas após a avaliação de todos os argumentos [Ref 2] .

[Ref 2] C ++ 03 1.9 Execução do programa [introdução.execução]:
Parágrafo 17:

Ao chamar uma função (seja a função embutida ou não), há um ponto de sequência após a avaliação de todos os argumentos da função (se houver) que ocorre antes da execução de quaisquer expressões ou instruções no corpo da função.

Observe que, aqui o valor de cestá sendo acessado mais de uma vez sem um ponto de sequência interveniente, em relação a isso o padrão diz:

[Ref 3] C ++ 03 5 Expressões [expr]:
Para 4:

....
Entre o ponto de sequência anterior e seguinte, um objeto escalar deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. Além disso, o valor anterior deve ser acessado apenas para determinar o valor a ser armazenado . Os requisitos deste parágrafo devem ser atendidos para cada ordenação permitida das subexpressões de uma expressão completa; caso contrário, o comportamento é indefinido .

O código se modifica cmais de uma vez sem ponto de sequência intermediário e não está sendo acessado para determinar o valor do objeto armazenado. Esta é uma violação clara da cláusula acima e, portanto, o resultado conforme exigido pela norma é Comportamento indefinido [Ref 3] .

Alok Save
fonte
1
Tecnicamente, o comportamento é indefinido, pois há modificação de um objeto, e o acesso a ele em outro lugar sem um ponto de sequência interveniente. Indefinido não é não especificado; isso deixa a implementação ainda mais margem de manobra.
James Kanze
1
@Als Sim. Eu não tinha visto suas edições (embora estivesse reagindo à declaração de jrok de que o programa não pode fazer algo estranho --- pode). Sua versão editada é boa até o fim, mas, em minha opinião, a palavra-chave é ordenação parcial ; os pontos de sequência introduzem apenas uma ordenação parcial.
James Kanze
1
@Als obrigado por uma descrição elaborada, realmente muito útil !!
pravs
4
O novo padrão C ++ 0x diz essencialmente o mesmo, mas em seções diferentes e em palavras diferentes :) Citação: (1.9 Execução do Programa [introdução.execução], par. 15): "Se um efeito colateral em um objeto escalar não tiver seqüência em relação a seja outro efeito colateral no mesmo objeto escalar ou um cálculo de valor usando o valor do mesmo objeto escalar, o comportamento é indefinido. "
Rafał Dowgird
2
Eu acredito que há um bug nesta resposta. "std :: cout << c ++ << c;" não pode ser traduzido para "std :: operator << (std :: operator << (std :: cout, c ++), c)", porque std :: operator << (std :: ostream &, int) não existe. Em vez disso, ele se traduz em "std :: cout.operator << (c ++). Operator (c);", que na verdade tem um ponto de sequência entre a avaliação de "c ++" e "c" (um operador sobrecarregado é considerado um chamada de função e, portanto, há um ponto de sequência quando a chamada de função retorna). Consequentemente, o comportamento e a ordem de execução são especificados.
Christopher Smith
20

Os pontos de sequência definem apenas uma ordem parcial . No seu caso, você tem (assim que a resolução da sobrecarga for concluída):

std::cout.operator<<( a++ ).operator<<( a );

Há um ponto de sequência entre a++e a primeira chamada para std::ostream::operator<<e há um ponto de sequência entre a segunda ae a segunda chamada para std::ostream::operator<<, mas não há ponto de sequência entre a++e a; as únicas restrições de ordenação são que a++sejam totalmente avaliadas (incluindo efeitos colaterais) antes da primeira chamada para operator<<e que a segunda aseja totalmente avaliada antes da segunda chamada para operator<<. (Existem também restrições de ordem causual: a segunda chamada para operator<<não pode preceder a primeira, uma vez que requer os resultados do primeiro como um argumento.) §5 / 4 (C ++ 03) afirma:

Exceto onde indicado, a ordem de avaliação dos operandos de operadores individuais e subexpressões de expressões individuais, e a ordem em que os efeitos colaterais ocorrem, não é especificada. Entre o ponto de sequência anterior e o próximo, um objeto escalar deve obrigatoriamente ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. Além disso, o valor anterior deve ser acessado apenas para determinar o valor a ser armazenado. Os requisitos deste parágrafo devem ser atendidos para cada ordenação permitida das subexpressões de uma expressão completa; caso contrário, o comportamento é indefinido.

Uma das ordenações permitidos de sua expressão é a++, aprimeiro chamada para operator<<, segunda chamada para operator<<; isso modifica o valor armazenado de a( a++) e acessa-o de outra forma que não para determinar o novo valor (o segundo a), o comportamento é indefinido.

James Kanze
fonte
Uma pegadinha de sua citação do padrão. O "exceto onde indicado", IIRC, inclui uma exceção ao lidar com um operador sobrecarregado, que trata o operador como uma função e, portanto, cria um ponto de sequência entre a primeira e a segunda chamada para std :: ostream :: operator << (int ) Por favor corrija-me se eu estiver errado.
Christopher Smith
@ChristopherSmith Um operador sobrecarregado se comporta como uma chamada de função. Se cfosse um tipo de usuário com um usuário definido ++, em vez de int, os resultados seriam não especificados, mas não haveria comportamento indefinido.
James Kanze de
1
@ChristopherSmith Onde você vê um ponto de sequência entre os dois cem foo(foo(bar(c)), c)? Há um ponto de sequência quando as funções são chamadas e quando retornam, mas não há nenhuma chamada de função necessária entre as avaliações dos dois c.
James Kanze
1
@ChristopherSmith Se cfosse um UDT, os operadores sobrecarregados seriam chamadas de função e introduziriam um ponto de sequência, de modo que o comportamento não seria indefinido. Mas ainda não seria especificado se a subexpressão cfoi avaliada antes ou depois c++, portanto, se você obteve a versão incrementada ou não, não seria especificado (e, em teoria, não teria que ser o mesmo todas as vezes).
James Kanze
1
@ChristopherSmith Tudo antes do ponto de sequência acontecerá antes de qualquer coisa depois do ponto de sequência. Mas os pontos de sequência definem apenas uma ordem parcial. Na expressão em questão, por exemplo, não há ponto de sequência entre as subexpressões ce c++, portanto, as duas podem ocorrer em qualquer ordem. Quanto aos pontos e vírgulas ... Eles só causam um ponto de sequência na medida em que são expressões completas. Outros pontos de sequência importantes são a chamada de função: f(c++)verá o incrementado cem fe o operador vírgula &&, ||e ?:também causará os pontos de sequência.
James Kanze
4

A resposta correta é questionar a pergunta. A afirmação é inaceitável porque o leitor não consegue ver uma resposta clara. Outra maneira de ver isso é que introduzimos os efeitos colaterais (c ++) que tornam a declaração muito mais difícil de interpretar. O código conciso é ótimo, desde que seu significado seja claro.

Paul Marrington
fonte
4
A questão pode exibir uma prática de programação pobre (e até mesmo C ++ inválido). Mas uma resposta deve responder à pergunta indicando o que está errado e por que está errado. Um comentário sobre a pergunta não é uma resposta, mesmo que seja perfeitamente válido. Na melhor das hipóteses, isso pode ser um comentário, não uma resposta.
PP de