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

186

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

De [N3337 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 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_te versões qualificadas para cv desses tipos são chamados coletivamente de tipos escalares.

O que não afeta o exemplo.

De [N4659 expr.ass]

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.

De [N3337 expr.ass]

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 inão é "outro efeito colateral" nem "usa o valor do mesmo objeto escalar", pois a expressão id é um valor l.

Passer By
fonte
23
Você identificou o motivo: No C ++ 17, o operando direito é sequenciado antes do operando esquerdo. No C ++ 11, não havia esse seqüenciamento. Qual é exatamente a sua pergunta?
Rob
4
@ Robᵩ Veja a última frase.
Passer Por
7
Alguém tem um link para a motivação para essa mudança? Eu gostaria que um analisador estático pudesse dizer "você não quer fazer isso" quando confrontado com códigos como esse i = i++ + 1;.
7
@NeilButterworth, é do artigo p0145r3.pdf : "Refinando a Ordem de Avaliação de Expressão para Idiomatic C ++".
precisa saber é o seguinte
9
@ NeilButterworth, a seção número 2 diz que isso é contra-intuitivo e até mesmo os especialistas não conseguem fazer a coisa certa em todos os casos. Isso é praticamente toda a motivação deles.
precisa saber é o seguinte

Respostas:

144

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:

O operando direito é sequenciado antes do operando esquerdo.

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.)

Formiga
fonte
3
É realmente correto incluir "o ato real de atribuição" nos efeitos cobertos pelo "operando da mão esquerda" nesse trecho? O padrão possui linguagem separada sobre o seqüenciamento da atribuição real. Considero que o trecho que você apresentou tem escopo limitado ao seqüenciamento das subexpressões da mão esquerda e da mão direita, o que não parece suficiente, em combinação com o restante dessa seção, para apoiar definição da declaração do OP.
John Bollinger
11
Correção: a atribuição real ainda é sequenciada após o cálculo do valor do operando esquerdo, e a avaliação do operando esquerdo é sequenciada após a avaliação (completa) do operando direito, portanto, sim, essa alteração é suficiente para suportar a boa definição do OP perguntou sobre. Estou apenas discutindo os detalhes, mas eles são importantes, pois podem ter implicações diferentes para códigos diferentes.
John Bollinger
3
@ JohnBollinger: Acho curioso que os autores da Norma façam uma alteração que prejudique a eficiência da geração direta de código e que historicamente não seja necessária, e que se oponham a definir outros comportamentos cuja ausência é um problema muito maior e que dificilmente representaria qualquer impedimento significativo à eficiência.
Supercat 8/17
1
@Kaz: Para atribuições compostas, realizando a avaliação valor do lado esquerdo após o lado direito permite que algo como x -= y;a ser processado como mov eax,[y] / sub [x],eaxem vez de mov 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.
Supercat
1
@ Kaz: Se xe yfosse volatile, isso teria efeitos colaterais. Além disso, as mesmas considerações se aplicam a x += f();, onde f()modifica x.
Supercat
33

Você identificou a nova frase

O operando direito é sequenciado antes do operando esquerdo.

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
7

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

As avaliações dos operandos não são seguidas.

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:

O operando direito é sequenciado antes do operando esquerdo.


Como uma observação lateral, em padrões ainda mais antigos, tudo isso ficou muito claro, como no 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 de acessá-lo após o próximo ponto de sequência, o comportamento será indefinido.

Basicamente, no C11 / C ++ 11, eles erraram ao remover este texto.

Lundin
fonte
1

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):

i = 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 + 1feito 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 ++).

MILÍMETROS
fonte