standard - O que fez i=i+++1; legal em C++ 17?




wiki c++ 20 (2)

Em C ++ 11, o ato de "atribuição", ou seja, o efeito colateral de modificar o LHS, é sequenciado após o cálculo do valor do operando direito. Observe que essa é uma garantia relativamente "fraca": ela produz sequenciamento apenas com relação ao cálculo do valor do RHS. Não diz nada sobre os efeitos colaterais que podem estar presentes no RHS, já que a ocorrência de efeitos colaterais não faz parte da computação de valor . Os requisitos do C ++ 11 não estabelecem sequenciamento relativo entre o ato de atribuição e quaisquer efeitos colaterais do RHS. É isso que cria o potencial para o UB.

A única esperança neste caso é qualquer garantia adicional feita por operadores específicos utilizados no RHS. Se o RHS usasse um prefixo ++ , as propriedades de sequenciamento específicas para o formato de prefixo ++ teriam salvo o dia neste exemplo. Mas postfix ++ é uma história diferente: não faz tais garantias. Em C ++ 11, os efeitos colaterais de = e postfix ++ acabam não sequenciados com relação um ao outro neste exemplo. E isso é UB.

Em C ++ 17, uma sentença extra é adicionada à especificação do operador de atribuição:

O operando direito é sequenciado antes do operando esquerdo.

Em combinação com o que precede, contribui para uma garantia muito forte. Ele seqüencia tudo o que acontece no RHS (incluindo quaisquer efeitos colaterais) antes de tudo que acontece no LHS. Uma vez que a atribuição real é sequenciada após LHS (e RHS), esse sequenciamento extra isola completamente o ato de atribuição de quaisquer efeitos colaterais presentes no RHS. Esse sequenciamento mais forte é o que elimina o UB acima.

(Atualizado para levar em consideração os comentários de @John Bollinger.)

Antes de começar a gritar comportamento indefinido, isto está explicitamente listado em N4659 (C ++ 17)

  i = i++ + 1;        // the value of i is incremented

Ainda no N3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

O que mudou?

Pelo que eu posso reunir, de [N4659 basic.exec]

Exceto onde indicado, as avaliações de operandos de operadores individuais e de subexpressões de expressões individuais são desassociadas. [...] Os cálculos de valor dos operandos de um operador são seqüenciados antes do cálculo do valor do resultado do operador. Se um efeito colateral em um local de memória for desassociado em relação a outro efeito colateral no mesmo local de memória ou um cálculo de valor usando o valor de qualquer objeto no mesmo local de memória e eles não forem potencialmente concorrentes, o comportamento será indefinido.

Onde o valor é definido em [N4659 basic.type]

Para tipos trivialmente copiáveis, a representação de valor é um conjunto de bits na representação do objeto que determina um valor , que é um elemento discreto de um conjunto de valores definido pela implementação.

De [N3337 basic.exec]

Exceto onde indicado, as avaliações de operandos de operadores individuais e de subexpressões de expressões individuais são desassociadas. [...] Os cálculos de valor dos operandos de um operador são seqüenciados antes do cálculo do valor do resultado do operador. Se um efeito colateral em um objeto escalar não for sequenciado em relação a outro efeito colateral no mesmo objeto escalar ou a um cálculo de valor usando o valor do mesmo objeto escalar, o comportamento será indefinido.

Da mesma forma, o valor é definido em [N3337 basic.type]

Para tipos trivialmente copiáveis, a representação de valor é um conjunto de bits na representação do objeto que determina um valor , que é um elemento discreto de um conjunto de valores definido pela implementação.

Eles são idênticos, exceto menção de simultaneidade que não importa, e com o uso de localização de memória em vez de objeto escalar , onde

Tipos aritméticos, tipos de enumeração, tipos de ponteiro, ponteiro para tipos de membros, std::nullptr_t e versões qualificadas para cv desses tipos são chamados coletivamente de tipos escalares.

Qual não afeta o exemplo.

De [N4659 expr.ass]

O operador de atribuição (=) e os operadores de atribuição compostos, 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. O operando direito é sequenciado antes do operando esquerdo.

De [N3337 expr.ass]

O operador de atribuição (=) e os operadores de atribuição compostos, 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.

A única diferença é que a última sentença está ausente em N3337.

A última frase, no entanto, não deve ter qualquer importância, pois o operando esquerdo i não é "outro efeito colateral" nem "usando o valor do mesmo objeto escalar", pois a expressão id é um lvalue.


Em padrões C ++ mais antigos e em C11, a definição do texto do operador de atribuição termina com o texto:

As avaliações dos operandos são desassociadas.

Isso significa que efeitos colaterais nos operandos são desassociados e, portanto, definitivamente indefinidos, se eles usarem a mesma variável.

Este texto foi simplesmente removido em C ++ 11, deixando-o um pouco ambíguo. É UB ou não é? Isso foi esclarecido em C ++ 17, onde eles acrescentaram:

O operando direito é sequenciado antes do operando esquerdo.

Como uma nota lateral, em padrões ainda mais antigos, tudo isso ficou muito claro, exemplo da C99:

A ordem de avaliação dos operandos não é especificada. Se for feita uma tentativa de modificar o resultado de um operador de atribuição ou acessá-lo após o próximo ponto de sequência, o comportamento é indefinido.

Basicamente, no C11 / C ++ 11, eles bagunçaram quando removeram este texto.





c++17