Cálculos de valor não seqüenciados (também conhecidos como pontos de seqüência)

Desculpe por abrir este tópico novamente, mas pensar sobre este tópico em si começou a me dar um comportamento indefinido. Quer mudar para a zona de comportamento bem definido.

Dado

int i = 0; int v[10]; i = ++i; //Expr1 i = i++; //Expr2 ++ ++i; //Expr3 i = v[i++]; //Expr4 

Penso nas expressões acima (nessa ordem) como

 operator=(i, operator++(i)) ; //Expr1 equivalent operator=(i, operator++(i, 0)) ; //Expr2 equivalent operator++(operator++(i)) ; //Expr3 equivalent operator=(i, operator[](operator++(i, 0)); //Expr4 equivalent 

Agora chegando aos comportamentos aqui estão as citações importantes de C ++ 0x .

$ 1.9 / 12- “A avaliação de uma expressão (ou uma sub-expressão) em geral inclui cálculos de valor (incluindo determinar a identidade de um object para avaliação de lvalue e obter um valor previamente atribuído a um object para avaliação de valor) e início de efeitos colaterais ”

$ 1.9 / 15- “Se um efeito colateral em um object escalar não é sequenciado em relação a outro efeito colateral no mesmo object escalar ou um cálculo de valor usando o valor do mesmo object escalar, o comportamento é indefinido.”

[Nota: Cálculos de valor e efeitos colaterais associados a expressões de argumentos diferentes não são sequenciados. – end note]

$ 3.9 / 9- “Tipos aritméticos (3.9.1), tipos de enumeração, tipos de ponteiro, ponteiro para tipos de membro (3.9.2), std :: nullptr_t e versões qualificadas em cv desses tipos (3.9.3) são coletivamente chamados tipos escalares “.

  • Em Expr1, a avaliação da expressão i (primeiro argumento), não é sequenciada em relação à avaliação do operator++(i) exposição operator++(i) (que tem um efeito colateral).

    Daí Expr1 tem comportamento indefinido.

  • Em Expr2, a avaliação da expressão i (primeiro argumento) é desassociada em relação à avaliação do operator++(i, 0) exposição operator++(i, 0) (que tem um efeito colateral) ‘.

    Daí Expr2 tem comportamento indefinido.

  • Em Expr3, a avaliação do operator++(i) argumento solitário operator++(i) deve ser concluída antes que o operator++ externo operator++ seja chamado.

    Daí Expr3 tem um comportamento bem definido.

  • No Expr4, a avaliação da expressão i (primeiro argumento) não é sequenciada em relação à avaliação do operator[](operator++(i, 0) (que tem um efeito colateral).

    Daí Expr4 tem comportamento indefinido.

Esse entendimento está correto?


PS O método de analisar as expressões como em OP não está correto. Isso ocorre porque, como observa @Potatoswatter, “a cláusula 13.6 não se aplica. Consulte a exoneração de responsabilidade de 13.6 / 1,” Essas funções candidatas participam do processo de resolução de sobrecarga do operador conforme descrito em 13.3.1.2 e não são usadas para nenhuma outra finalidade. “Eles são apenas declarações falsas; não existe semântica de chamada de function em relação aos operadores internos.”

    Expressões de operadores nativos não são equivalentes a expressões de operador sobrecarregadas. Existe um ponto de sequência na binding dos valores aos argumentos da function, o que torna as versões do operator++() bem definidas. Mas isso não existe para o caso do tipo nativo.

    Nos quatro casos, i mudo duas vezes dentro da expressão completa. Desde que não, || ou && aparecem nas expressões, isso é UB instantâneo.

    §5 / 4:

    Entre o ponto de seqüência anterior e o seguinte, um object escalar deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão.

    Editar para C ++ 0x (atualizado)

    § 1.9 / 15:

    Os cálculos de valor dos operandos de um operador são sequenciados antes do cálculo do valor do resultado do operador. Se um efeito colateral em um object escalar não for sequenciado em relação a outro efeito colateral no mesmo object escalar ou a um cálculo de valor usando o valor do mesmo object escalar, o comportamento será indefinido.

    Note, no entanto, que um cálculo de valor e um efeito colateral são duas coisas distintas. Se ++i é equivalente a i = i+1 , então + é o valor computado e = é o efeito colateral. A partir de 1.9 / 12:

    A avaliação de uma expressão (ou de uma sub-expressão) em geral inclui cálculos de valor (incluindo a determinação da identidade de um object para avaliação glvalue e busca de um valor previamente atribuído a um object para avaliação de valor) e início de efeitos colaterais.

    Portanto, embora os cálculos de valor sejam mais fortemente sequenciados em C ++ 0x que C ++ 03, os efeitos colaterais não são. Dois efeitos colaterais na mesma expressão, a menos que seqüenciados de outra forma, produzem UB.

    Cálculos de valor são ordenados por suas dependencies de dados de qualquer maneira e, efeitos colaterais ausentes, sua ordem de avaliação é inobservável, então eu não tenho certeza porque C ++ 0x se dá ao trabalho de dizer qualquer coisa, mas isso significa que eu preciso ler mais dos trabalhos de Boehm e amigos escreveu.

    Editar # 3:

    Obrigado Johannes por lidar com a minha preguiça de digitar “sequenciado” na minha barra de pesquisa de leitores de PDF. Eu estava indo para a cama e levantando as últimas duas edições de qualquer maneira … certo; v).

    §5.17 / 1 definindo os operadores de atribuição 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.

    Também o §5.3.2 / 1 no operador de pré-incremento diz

    Se x não for do tipo bool, a expressão ++ x é equivalente a x + = 1 [Nota: ver… adição (5.7) e operadores de atribuição (5.17)…].

    Por essa identidade, ++ ++ x é uma abreviação para (x +=1) +=1 . Então, vamos interpretar isso.

    • Avalie o 1 no RHS distante e desça para os parênteses.
    • Avalie o valor 1 e o valor interno (valor) e o endereço (valor gl) de x .
    • Agora precisamos do valor da subexpressão + =.
      • Nós terminamos com os cálculos de valor para essa subexpressão.
      • O efeito colateral da atribuição deve ser sequenciado antes que o valor da atribuição esteja disponível!
    • Atribua o novo valor a x , que é idêntico ao resultado glvalue e prvalue da subexpressão.
    • Estamos fora da floresta agora. A expressão inteira foi agora reduzida para x +=1 .

    Então, 1 e 3 são bem definidos e 2 e 4 são comportamentos indefinidos, o que você esperaria.

    A única outra surpresa que encontrei pesquisando “sequenciado” no N3126 foi 5.3.4 / 16, onde a implementação pode chamar o operator new antes de avaliar os argumentos do construtor. Isso é legal.

    Edite # 4: (Oh, que teia emaranhada nós tecemos)

    Johannes observa novamente que em i == ++i; o glvalue (aka o endereço) de i é ambiguamente dependente de ++i . O glvalue é certamente um valor de i , mas eu não acho que 1.9 / 15 se destina a incluí-lo pela simples razão de que o glvalue de um object nomeado é constante, e não pode realmente ter dependencies.

    Para um palhaço informativo, considere

     ( i % 2? i : j ) = ++ i; // certainly undefined 

    Aqui, o glvalue do LHS de = depende de um efeito colateral no valor de i . O endereço de i não está em questão; o resultado do ?: é.

    Talvez um bom contra-exemplo seja

     int i = 3, &j = i; j = ++ i; 

    Aqui j tem um glvalue distinto de (mas idêntico a) i . Isso é bem definido, mas i = ++i não é? Isso representa uma transformação trivial que um compilador poderia aplicar a qualquer caso.

    1.9 / 15 diria

    Se um efeito colateral em um object escalar não for sequenciado em relação a outro efeito colateral no mesmo object escalar ou a um cálculo de valor usando o prvalor do mesmo object escalar, o comportamento será indefinido.

    Pensando em expressões como as mencionadas, acho útil imaginar uma máquina em que a memory tenha intertravamentos, de modo que a leitura de um local de memory como parte de uma sequência de leitura-modificação-gravação cause qualquer tentativa de leitura ou gravação, além da gravação final de a sequência, para ser interrompida até a sequência terminar. Tal máquina dificilmente seria um conceito absurdo; Na verdade, esse design poderia simplificar muitos cenários de código multi-threaded. Por outro lado, uma expressão como “x = y ++;” poderia falhar em tal máquina se ‘x’ e ‘y’ fossem referências à mesma variável, e o código gerado pelo compilador fizesse algo como read-and-lock reg1 = y; reg2 = reg1 + 1; escreva x = reg1; escrever e desbloquear y = reg2. Isso seria uma seqüência de código muito razoável em processadores onde a gravação de um valor recém-calculado imporia um atraso de pipeline, mas a gravação em x trancaria o processador se y fosse alias à mesma variável.