Ordem de avaliação da declaração de atribuição em C++




language-lawyer operator-precedence (3)

map<int, int> mp;
printf("%d ", mp.size());
mp[10]=mp.size();
printf("%d\n", mp[10]);

Este código produz uma resposta que não é muito intuitiva:

0 1

Entendo por que isso acontece - o lado esquerdo da atribuição retorna referência ao valor subjacente do mp[10] e, ao mesmo tempo, cria o valor acima mencionado, e somente então o lado direito é avaliado, usando o size() recém-calculado size() do mapa.

Esse comportamento é declarado em algum lugar no padrão C ++? Ou a ordem da avaliação é indefinida?

O resultado foi obtido usando g ++ 5.2.1.


Do padrão C ++ 11 (ênfase minha):

5.17 Operadores de atribuição e atribuição composta

1 O operador de atribuição (=) e os operadores de atribuição composta todos agrupam da direita para a esquerda. Todos exigem um lvalue modificável como seu operando esquerdo e retornam um lvalue referente ao operando esquerdo. O resultado em todos os casos é um campo de bits se o operando esquerdo for um campo de bits. Em todos os casos, a atribuição é sequenciada após o cálculo do valor dos operandos direito e esquerdo e antes do cálculo do valor da expressão de atribuição.

Se o operando esquerdo é avaliado primeiro ou o operando direito é avaliado primeiro não é especificado pelo idioma. Um compilador pode escolher avaliar primeiro qualquer operando. Como o resultado final do seu código depende da ordem de avaliação dos operandos, eu diria que é um comportamento não especificado e não um comportamento indefinido.

1.3.25 comportamento não especificado

comportamento, para uma construção de programa bem formada e dados corretos, que depende da implementação


Sim, isso é coberto pelo padrão e é um comportamento não especificado. Esse caso específico é abordado em uma recente proposta de padrões de C ++: N4228: Refinando a Ordem de Avaliação de Expressão para o Idiomatic C ++, que busca refinar a ordem das regras de avaliação para torná-la bem especificada em certos casos.

Ele descreve esse problema da seguinte maneira:

A ordem de avaliação de expressão é um tópico de discussão recorrente na comunidade C ++. Em poucas palavras, dada uma expressão como f (a, b, c) , a ordem na qual as subexpressões f, a, b, c são avaliadas é deixada não especificada pelo padrão. Se duas dessas subexpressões modificarem o mesmo objeto sem interferir nos pontos de sequência, o comportamento do programa será indefinido. Por exemplo, a expressão f (i ++, i) onde i é uma variável inteira leva a um comportamento indefinido, assim como v [i] = i ++ . Mesmo quando o comportamento não é indefinido, o resultado da avaliação de uma expressão ainda pode ser uma incógnita. Considere o seguinte fragmento de programa:

#include <map>

int main() {
  std::map<int, int>  m;
  m[0] = m.size(); // #1
}

Como deve ser o objeto do mapa após a avaliação da declaração marcada como 1? {{0, 0}} ou {{0, 1}}?

Sabemos que, a menos que seja especificada, as avaliações das subexpressões não são necessárias, isso é do rascunho da seção 1.9 norma 1.9 C ++ 11 , que diz:

Exceto quando indicado, as avaliações de operandos de operadores individuais e de subexpressões de expressões individuais são sem seqüência. [...]

e toda a seção 5.17 Operadores de atribuição e atribuição composta [expr.ass] diz:

[...] Em todos os casos, a atribuição é sequenciada após o cálculo do valor dos operandos direito e esquerdo e antes do cálculo do valor da expressão de atribuição. [...]

Portanto, esta seção não define a ordem da avaliação, mas sabemos que esse não é um comportamento indefinido, pois operator [] e size() são chamadas de função e a seção 1.9 nos diz ( ênfase minha ):

[...] Ao chamar uma função (esteja ou não embutida), todo cálculo de valor e efeito colateral associado a qualquer expressão de argumento, ou à expressão postfix que designa a função chamada, são sequenciados antes da execução de cada expressão ou instrução no corpo da função chamada. [Nota: cálculos de valor e efeitos colaterais associados a diferentes expressões de argumento não têm seqüência. - end note] Todas as avaliações na função de chamada (incluindo outras chamadas de função) que não sejam especificamente sequenciadas antes ou depois da execução do corpo da função chamada são indeterminadas em relação à execução da função chamada .9 [. ..]

Observe, eu N4228 o segundo exemplo interessante da proposta N4228 na pergunta . .

Atualizar

Parece que uma versão revisada do N4228 foi aceita pelo Evolution Working Group na última reunião do WG21, mas o documento ( P0145R0 ) ainda não está disponível. Portanto, isso não pode mais ser não especificado no C ++ 17.

Atualização 2

A revisão 3 da p0145 fez isso especificado e atualizou [expr.ass]p1 :

O operador de atribuição (=) e os operadores de atribuição composta agrupam todos da direita para a esquerda. Todos exigem um valor modificável como seu operando esquerdo; o resultado é um valor l que se refere ao operando esquerdo. O resultado em todos os casos é um campo de bits se o operando esquerdo for um campo de bits. Em todos os casos, a atribuição é sequenciada após o cálculo do valor dos operandos direito e esquerdo e antes do cálculo do valor da expressão de atribuição. O operando direito é sequenciado antes do operando esquerdo. ...


Vamos dar uma olhada no que seu código divide:

mp.operator[](10).operator=(mp.size());

que praticamente conta a história de que, na primeira parte, uma entrada para 10 é criada e na segunda parte o tamanho do contêiner é atribuído à referência inteira na posição 10.

Mas agora você entra na ordem do problema de avaliação que não é especificado. Aqui está um example muito mais simples.

Quando o map::size() ser chamado, antes ou depois do map::operator(int const &); ?

Ninguém realmente sabe.





operator-precedence