É indefinido porque modifica x
duas vezes entre os pontos de sequência. O padrão diz que é indefinido, portanto é indefinido.
Isso eu sei.
Mas por que?
Meu entendimento é que proibir isso permite que os compiladores otimizem melhor. Isso poderia ter feito sentido quando C foi inventado, mas agora parece um argumento fraco.
Se reinventássemos o C hoje, faríamos dessa maneira ou será melhor?
Ou talvez haja um problema mais profundo, que dificulta a definição de regras consistentes para essas expressões, por isso é melhor proibi-las.
Então, suponha que reinventássemos C hoje. Gostaria de sugerir regras simples para expressões como x=x++
, que me parecem funcionar melhor do que as regras existentes.
Gostaria de obter sua opinião sobre as regras sugeridas em comparação com as existentes ou com outras sugestões.
Regras sugeridas:
- Entre os pontos de sequência, a ordem da avaliação não é especificada.
- Os efeitos colaterais ocorrem imediatamente.
Não há comportamento indefinido envolvido. As expressões são avaliadas com esse valor ou com isso, mas certamente não formatarão seu disco rígido (estranhamente, nunca vi uma implementação em que formate x=x++
o disco rígido).
Expressões de exemplo
x=x++
- Bem definido, não mudax
.
Primeiro,x
é incrementado (imediatamente quandox++
é avaliado) e, em seguida, seu valor antigo é armazenadox
.x++ + ++x
- Incrementax
duas vezes, avalia como2*x+2
.
Embora ambos os lados possam ser avaliados primeiro, o resultado éx + (x+2)
(lado esquerdo primeiro) ou(x+1) + (x+1)
(lado direito primeiro).x = x + (x=3)
- Não especificado,x
defina comox+3
ou6
.
Se o lado direito é avaliado primeiro, éx+3
. Também é possível quex=3
seja avaliado primeiro, por isso é3+3
. Em qualquer um dos casos, ax=3
atribuição acontece imediatamente quandox=3
é avaliada; portanto, o valor armazenado é substituído pela outra atribuição.x+=(x=3)
- Bem definido, definex
como 6.
Você pode argumentar que isso é apenas uma abreviação para a expressão acima.
Mas eu diria que isso+=
deve ser executado depoisx=3
, e não em duas partes (leiax
, avaliex=3
, adicione e armazene novo valor).
Qual é a vantagem?
Alguns comentários levantaram esse ponto positivo.
Eu certamente não acho que expressões como x=x++
devam ser usadas em qualquer código normal.
Na verdade, eu sou muito mais rigorosa do que isso - Eu acho que o único bem de uso para x++
em tão x++;
sozinho.
No entanto, acho que as regras de linguagem devem ser o mais simples possível. Caso contrário, os programadores simplesmente não os entenderão. a regra que proíbe alterar uma variável duas vezes entre os pontos de sequência é certamente uma regra que a maioria dos programadores não entende.
Uma regra muito básica é a seguinte:
se A é válido e B é válido, e eles são combinados de maneira válida, o resultado é válido.
x
é um valor L válido, x++
é uma expressão válida e =
é uma maneira válida de combinar um valor L e uma expressão; então, como x=x++
é que isso não é legal?
O padrão C faz uma exceção aqui, e essa exceção complica as regras. Você pode pesquisar stackoverflow.com e ver o quanto essa exceção confunde as pessoas.
Então eu digo - livre-se dessa confusão.
=== Resumo das respostas ===
Por que fazer isso?
Tentei explicar na seção acima - quero que as regras C sejam simples.Potencial de otimização:
isso requer alguma liberdade do compilador, mas eu não vi nada que me convenceu de que isso poderia ser significativo.
A maioria das otimizações ainda pode ser feita. Por exemplo,a=3;b=5;
pode ser reordenado, mesmo que o padrão especifique o pedido. Expressões comoa=b[i++]
ainda podem ser otimizadas da mesma forma.Você não pode alterar o padrão existente.
Eu admito, não posso. Eu nunca pensei que realmente pudesse ir em frente e mudar padrões e compiladores. Eu só queria pensar se as coisas poderiam ter sido feitas de maneira diferente.
fonte
x
a si próprio e, se você quiser incrementar,x
pode apenas dizerx++;
- não há necessidade da atribuição. Eu diria que não deve ser definido apenas porque seria difícil lembrar o que deveria acontecer.Respostas:
Talvez você deva primeiro responder à pergunta por que ela deve ser definida? Existe alguma vantagem no estilo de programação, legibilidade, capacidade de manutenção ou desempenho ao permitir tais expressões com efeitos colaterais adicionais? É
mais legível que
Dado que essa mudança é extremamente fundamental e está quebrando a base de código existente.
fonte
O argumento de que tornar esse comportamento indefinido permite uma melhor otimização não é fraco hoje. Na verdade, é muito mais forte hoje do que era quando C era novo.
Quando C era novo, as máquinas que podiam tirar proveito disso para uma melhor otimização eram principalmente modelos teóricos. As pessoas falaram sobre a possibilidade de construir CPUs onde o compilador instruiria a CPU sobre quais instruções poderiam / deveriam ser executadas em paralelo com outras instruções. Eles apontaram o fato de que permitir que esse comportamento tivesse indefinido significava que em uma CPU, se ela realmente existisse, você poderia agendar a parte "incremento" da instrução para executar em paralelo com o restante do fluxo de instruções. Enquanto eles estavam certos sobre a teoria, na época havia pouco hardware que realmente poderia tirar proveito dessa possibilidade.
Isso não é mais apenas teórico. Agora, há hardware em produção e amplo uso (por exemplo, DSPs Itanium, VLIW) que podem realmente tirar proveito disso. Eles realmente fazer permitir que o compilador para gerar um fluxo de instrução que especifica que as instruções X, Y e Z podem ser executadas em paralelo. Este não é mais um modelo teórico - é um hardware real em uso real, realizando um trabalho real.
Na IMO, tornar esse comportamento definido está próximo da pior "solução" possível para o problema. Você claramente não deve usar expressões como esta. Para a grande maioria do código, o comportamento ideal seria o compilador simplesmente rejeitar completamente essas expressões. Na época, os compiladores C não faziam a análise de fluxo necessária para detectar isso de forma confiável. Mesmo na época do padrão C original, ainda não era de todo comum.
Também não tenho certeza de que seria aceitável para a comunidade hoje - embora muitos compiladores possam fazer esse tipo de análise de fluxo, eles normalmente o fazem apenas quando você solicita otimização. Duvido que a maioria dos programadores gostaria da idéia de diminuir a compilação de "depuração" apenas para poder rejeitar o código que eles (sendo sensatos) nunca escreveriam em primeiro lugar.
O que C fez é uma segunda melhor opção semi-razoável: diga às pessoas para não fazer isso, permitindo (mas não exigindo) que o compilador rejeite o código. Isso evita (ainda mais) a compilação lenta de pessoas que nunca a usariam, mas ainda permite que alguém escreva um compilador que rejeitará esse código se quiser (e / ou tiver sinalizadores que o rejeitarão que as pessoas possam optar por usar) ou não como entenderem).
Pelo menos na IMO, tomar esse comportamento definido seria (pelo menos próximo) da pior decisão possível. No hardware do estilo VLIW, suas escolhas seriam gerar código mais lento para os usos razoáveis dos operadores de incremento, apenas por causa de códigos ruins que os abusam, ou sempre exigir uma extensa análise de fluxo para provar que você não está lidando com código de baixa qualidade, para que você possa produzir o código lento (serializado) somente quando for realmente necessário.
Conclusão: se você deseja curar esse problema, deve pensar na direção oposta. Em vez de definir o que esse código faz, você deve definir a linguagem para que essas expressões simplesmente não sejam permitidas (e viva com o fato de que a maioria dos programadores provavelmente optará por uma compilação mais rápida do que a imposição desse requisito).
fonte
a=b[i++];
(por exemplo) é bom, e otimizá-lo é uma coisa boa. No entanto, não vejo o ponto de prejudicar um código razoável como esse, para que algo como++i++
tenha um significado definido.++i++
é precisamente é geralmente difícil diferenciá-las de expressões válidas com efeitos colaterais (comoa=b[i++]
). Pode parecer simples o suficiente para nós, mas se me lembro do Livro dos Dragões corretamente, na verdade é um problema difícil de NP. É por isso que esse comportamento é UB, e não proibido.Eric Lippert, designer principal da equipe de compiladores C #, publicou em seu blog um artigo sobre várias considerações que optam por tornar um recurso indefinido no nível de especificação de idioma. Obviamente, o C # é uma linguagem diferente, com diferentes fatores envolvidos no design da linguagem, mas os pontos que ele destaca são relevantes.
Em particular, ele aponta a questão de ter compiladores existentes para um idioma que possui implementações existentes e também ter representantes em um comitê. Não tenho certeza se esse é o caso aqui, mas tende a ser relevante para a maioria das discussões de especificações relacionadas a C e C ++.
Também é importante notar, como você disse, o potencial de desempenho para otimização do compilador. Embora seja verdade que o desempenho das CPUs hoje em dia seja muito maior do que era quando C era jovem, uma grande quantidade de programação em C atualmente é feita especificamente devido ao ganho de desempenho potencial e ao potencial (futuro hipotético). ) As otimizações de instruções da CPU e otimizações de processamento multicore seriam tolas para impedir por causa de um conjunto excessivamente restritivo de regras para lidar com efeitos colaterais e pontos de sequência.
fonte
Primeiro, vamos dar uma olhada na definição de comportamento indefinido:
Portanto, em outras palavras, "comportamento indefinido" significa simplesmente que o compilador é livre para lidar com a situação da maneira que desejar, e qualquer ação desse tipo é considerada "correta".
A raiz do problema em discussão é a seguinte cláusula:
Enfase adicionada.
Dada uma expressão como
os subexpress~oes
a++
,--b
,c
, e++d
pode ser avaliada por qualquer ordem . Além disso, os efeitos colaterais dea++
,--b
e++d
podem ser aplicados a qualquer momento antes do próximo ponto de sequência (IOW, mesmo sea++
for avaliado antes--b
, não há garantia de quea
será atualizado antes da--b
avaliação). Como outros já disseram, a lógica desse comportamento é dar à implementação a liberdade de reordenar as operações da maneira ideal.Por isso, no entanto, expressões como
etc., produzirá resultados diferentes para diferentes implementações (ou para a mesma implementação com diferentes configurações de otimização ou com base no código circundante etc.).
O comportamento é deixado indefinido, de modo que o compilador não tem obrigação de "fazer a coisa certa", seja ela qual for. Os casos acima são fáceis de capturar, mas há um número não trivial de casos que seria difícil ou impossível de capturar em tempo de compilação.
Obviamente, é possível projetar uma linguagem de modo que a ordem da avaliação e a ordem na qual os efeitos colaterais sejam aplicados sejam estritamente definidas, e Java e C # o fazem, em grande parte para evitar os problemas que as definições de C e C ++ levam.
Então, por que essa alteração não foi feita em C após três revisões padrão? Primeiro de tudo, há 40 anos de código C legado por aí, e não é garantido que essa alteração não interrompa esse código. Isso sobrecarrega os gravadores de compiladores, pois essa alteração tornaria todos os compiladores existentes imediatamente não conformes; todo mundo teria que fazer reescritas significativas. E mesmo em CPUs modernas e rápidas, ainda é possível obter ganhos reais de desempenho, ajustando a ordem da avaliação.
fonte
Primeiro você precisa entender que não é apenas x = x ++ que é indefinido. Ninguém se importa com x = x ++, já que não importa o que você definiria, não há motivo para isso. O que é indefinido é mais como "a = b ++, onde aeb são os mesmos" - ie
Existem várias maneiras diferentes de a função ser implementada, dependendo do que for mais eficiente para a arquitetura do processador (e para as instruções circundantes, caso essa seja uma função mais complexa que o exemplo). Por exemplo, dois óbvios:
ou
Observe que o primeiro listado acima, aquele que usa mais instruções e mais registros, é o que você precisaria para ser usado em todos os casos em que não se pode provar que aeb seja diferente.
fonte
b
antesa
.Legado
A suposição de que C poderia ser reinventada hoje não pode ser mantida. Existem tantas linhas de códigos C que foram produzidas e usadas diariamente, que mudar as regras do jogo no meio da jogada é errado.
Claro que você pode inventar um novo idioma, digamos C + = , com suas regras. Mas isso não será C.
fonte
Declarar que algo está definido não mudará os compiladores existentes para respeitar sua definição. Isso é especialmente verdadeiro no caso de uma suposição que pode ter sido explicada de forma explícita ou implícita em muitos lugares.
A principal questão para a suposição não é com
x = x++;
(compiladores podem facilmente verificar e devem avisar), é com*p1 = (*p2)++
e equivalente (p1[i] = p2[j]++;
quando p1 e p2 são parâmetros para uma função) em que o compilador não pode saber facilmente sep1 == p2
(em C99restrict
foi adicionado para espalhar a possibilidade de assumir p1! = p2 entre os pontos de sequência; portanto, considerou-se que as possibilidades de otimização eram importantes).fonte
p1[i]=p2[j]++
. Se o compilador não pode assumir nenhum alias, não há problema. Se não puder, deve seguir o livro - incrementep2[j]
primeiro, armazenep1[i]
depois. Exceto pelas oportunidades de otimização perdidas, que não parecem significativas, não vejo problema.x = x++;
não foi escrito, mast = x; x++; x = t;
oux=x; x++;
ou o que você quiser como semântico (mas e o diagnóstico?). Para um novo idioma, apenas abandone os efeitos colaterais.x++
como um ponto de sequência, como se fosse uma chamada de funçãoinc_and_return_old(&x)
, resolvesse o problema.Em alguns casos, esse tipo de código foi definido no novo padrão C ++ 11.
fonte
x = ++x
agora está bem definido (mas nãox = x++
)