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.
fonte
i = i++ + 1;
.Respostas:
No C ++ 11, o ato de "atribuição", ou seja, o efeito colateral da modificação do LHS, é sequenciado após o cálculo do valor do operando certo. Observe que esta é uma garantia relativamente "fraca": produz seqüenciamento apenas em relação ao cálculo de valor do RHS. Não diz nada sobre os efeitos colaterais que podem estar presentes no RHS, uma vez que a ocorrência de efeitos colaterais não faz parte da computação de valor . Os requisitos do C ++ 11 não estabelecem seqüenciamento 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, são quaisquer garantias adicionais feitas por operadores específicos usados no RHS. Se o RHS usasse um prefixo
++
, as propriedades de seqüenciamento específicas para a forma de prefixo++
teriam salvado o dia neste exemplo. Mas o postfix++
é uma história diferente: não oferece tais garantias. No C ++ 11, os efeitos colaterais=
e o postfix++
acabam sem seqüência em relação um ao outro neste exemplo. E isso é UB.No C ++ 17, uma frase extra é adicionada à especificação do operador de atribuição:
Em combinação com o exposto, garante uma garantia muito forte. Sequencia tudo o que acontece no RHS (incluindo quaisquer efeitos colaterais) antes de tudo o que acontece no LHS. Como a atribuição real é sequenciada após o LHS (e o RHS), esse sequenciamento extra isola completamente o ato da atribuição de qualquer efeito colateral presente no RHS. Esse sequenciamento mais forte é o que elimina o UB acima.
(Atualizado para levar em conta os comentários de @John Bollinger.)
fonte
x -= y;
a ser processado comomov eax,[y] / sub [x],eax
em vez demov eax,[x] / neg eax / add eax,[y] / mov [x],eax
. Não vejo nada de idiota nisso. Se alguém tivesse que especificar uma ordem, a ordem mais eficiente seria provavelmente realizar todos os cálculos necessários para identificar o objeto do lado esquerdo primeiro, depois avaliar o operando direito e, em seguida, o valor do objeto esquerdo, mas isso exigiria um termo pelo ato de resolver a identidade do objeto esquerdo.x
ey
fossevolatile
, isso teria efeitos colaterais. Além disso, as mesmas considerações se aplicam ax += f();
, ondef()
modificax
.Você identificou a nova frase
e você identificou corretamente que a avaliação do operando esquerdo como um valor l é irrelevante. No entanto, a sequência anterior é especificada para ser uma relação transitiva. O operando direito completo (incluindo o pós-incremento) é, portanto, também sequenciado antes da atribuição. No C ++ 11, apenas o cálculo do valor do operando direito foi sequenciado antes da atribuição.
fonte
Nos padrões C ++ mais antigos e no C11, a definição do texto do operador de atribuição termina com o texto:
Isso significa que os efeitos colaterais nos operandos não têm consequências e, portanto, um comportamento definitivamente indefinido se eles usarem a mesma variável.
Este texto foi simplesmente removido no C ++ 11, deixando-o um tanto ambíguo. É UB ou não é? Isso foi esclarecido no C ++ 17, onde eles adicionaram:
Como uma observação lateral, em padrões ainda mais antigos, tudo isso ficou muito claro, como no C99:
Basicamente, no C11 / C ++ 11, eles erraram ao remover este texto.
fonte
Essas são outras informações para as outras respostas, e eu as estou postando, pois o código abaixo também é frequentemente solicitado .
A explicação nas outras respostas está correta e também se aplica ao código a seguir, que agora está bem definido (e não altera o valor armazenado de
i
):O
+ 1
é um arenque vermelho e não está muito claro por que o Padrão o usou em seus exemplos, embora eu me lembre de pessoas discutindo em listas de discussão anteriores ao C ++ 11 que talvez tenham+ 1
feito diferença devido ao forçar a conversão precoce de valores à direita. lado da mão. Certamente nada disso se aplica ao C ++ 17 (e provavelmente nunca se aplica a nenhuma versão do C ++).fonte