Antes de começar a gritar comportamento indefinido, isso está explicitamente listado em N4659 (C ++ 17)
i = i++ + 1; // the value of i is incremented
Ainda em N3337 (C ++ 11)
i = i++ + 1; // the behavior is undefined
O que mudou?
Pelo que posso entender , de [N4659 basic.exec]
Exceto quando indicado, as avaliações de operandos de operadores individuais e de subexpressões de expressões individuais são sem seqüência. [...] Os cálculos de valores dos operandos de um operador são sequenciados antes do cálculo de valores do resultado do operador. Se um efeito colateral em um local de memória não for conseqüente em relação a outro efeito colateral no mesmo local de memória ou em uma computação de valor usando o valor de qualquer objeto no mesmo local de memória, e eles não forem potencialmente simultâneos, o comportamento será indefinido.
Onde o valor é definido em [N4659 basic.type]
Para tipos trivialmente copiáveis, a representação do valor é um conjunto de bits na representação do objeto que determina um valor , que é um elemento distinto de um conjunto de valores definido pela implementação
Exceto quando indicado, as avaliações de operandos de operadores individuais e de subexpressões de expressões individuais são sem seqüência. [...] Os cálculos de valores dos operandos de um operador são sequenciados antes do cálculo de valores do resultado do operador. Se um efeito colateral em um objeto escalar for sem precedentes em relação a outro efeito colateral no mesmo objeto escalar ou a uma computação 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 do valor é um conjunto de bits na representação do objeto que determina um valor , que é um elemento distinto de um conjunto de valores definido pela implementação.
Eles são idênticos, exceto a menção de simultaneidade que não importa, e com o uso da localização da memória em vez do objeto escalar , onde
Tipos aritméticos, tipos de enumeração, tipos de ponteiro, tipos de ponteiro para membros
std::nullptr_t
e versões qualificadas para cv desses tipos são chamados coletivamente de tipos escalares.
O que não afeta o exemplo.
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. O operando direito é sequenciado antes do operando esquerdo.
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.
A única diferença é que a última frase está ausente no N3337.
A última frase, no entanto, não deve ter importância, pois o operando esquerdo i
não é "outro efeito colateral" nem "usa o valor do mesmo objeto escalar", pois a expressão id é um valor l.
i = i++ + 1;
.