Por que f (i = -1, i = -1) é um comportamento indefinido?

267

Eu estava lendo sobre a ordem das violações de avaliação , e eles dão um exemplo que me intriga.

1) Se um efeito colateral em um objeto escalar não for seqüenciado em relação a outro efeito colateral no mesmo objeto escalar, o comportamento será indefinido.

// snip
f(i = -1, i = -1); // undefined behavior

Nesse contexto, ié um objeto escalar , o que aparentemente significa

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

Não vejo como a afirmação é ambígua nesse caso. Parece-me que, independentemente de o primeiro ou o segundo argumento ser avaliado primeiro, ele itermina como -1e os dois argumentos também -1.

Alguém pode esclarecer?


ATUALIZAR

Eu realmente aprecio toda a discussão. Até agora, eu gosto muito da resposta do harmic, uma vez que expõe as armadilhas e os meandros da definição dessa declaração, apesar de quão simples ela parece à primeira vista. O @ acheong87 aponta alguns problemas que surgem ao usar referências, mas acho que isso é ortogonal ao aspecto dos efeitos colaterais não decorrentes desta questão.


RESUMO

Como essa pergunta recebeu muita atenção, resumirei os principais pontos / respostas. Primeiro, permita-me uma pequena digressão para apontar que "por que" pode ter significados intimamente relacionados, porém sutilmente diferentes, a saber "por qual causa ", "por qual motivo " e "com que finalidade ". Agruparei as respostas pelas quais esses significados de "por que" eles abordaram.

por que causa

A principal resposta aqui vem de Paul Draper , com Martin J contribuindo com uma resposta semelhante, mas não tão extensa. A resposta de Paul Draper se resume a

É um comportamento indefinido porque não está definido qual é o comportamento.

Em geral, a resposta é muito boa em termos de explicação do que o padrão C ++ diz. Ele também aborda alguns casos relacionados de UB, como f(++i, ++i);e f(i=1, i=-1);. No primeiro dos casos relacionados, não está claro se o primeiro argumento deve ser i+1e o segundo i+2ou vice-versa; no segundo, não está claro se ideve ser 1 ou -1 após a chamada da função. Ambos os casos são UB porque se enquadram na seguinte regra:

Se um efeito colateral em um objeto escalar não for relacionado em relação a outro efeito colateral no mesmo objeto escalar, o comportamento será indefinido.

Portanto, f(i=-1, i=-1)também é UB, uma vez que se enquadra na mesma regra, apesar de a intenção do programador ser (IMHO) óbvia e inequívoca.

Paul Draper também deixa explícito em sua conclusão que

Poderia ter sido um comportamento definido? Sim. Foi definido? Não.

o que nos leva à questão de "por que razão / propósito foi f(i=-1, i=-1)deixado como comportamento indefinido?"

por que razão / propósito

Embora haja alguns descuidos (talvez descuidados) no padrão C ++, muitas omissões são bem fundamentadas e servem a um propósito específico. Embora eu esteja ciente de que o objetivo geralmente é "facilitar o trabalho do redator do compilador" ou "código mais rápido", eu estava interessado principalmente em saber se há um bom motivo para sair f(i=-1, i=-1) como UB.

harmic e supercat fornecer os principais respostas que fornecem uma razão para a UB. Harmic aponta que um compilador otimizador que pode dividir as operações de atribuição ostensivamente atômicas em várias instruções da máquina e que pode intercalar ainda mais essas instruções para obter a velocidade ideal. Isso pode levar a resultados muito surpreendentes: iacaba sendo -2 no cenário dele! Portanto, harmônico demonstra como atribuir o mesmo valor a uma variável mais de uma vez pode ter efeitos negativos se as operações não forem subsequentes.

O supercat fornece uma exposição relacionada das armadilhas de tentar f(i=-1, i=-1)fazer o que parece que deveria fazer. Ele ressalta que, em algumas arquiteturas, há restrições rígidas contra várias gravações simultâneas no mesmo endereço de memória. Um compilador poderia ter dificuldade em entender isso se estivéssemos lidando com algo menos trivial do que f(i=-1, i=-1).

O davidf também fornece um exemplo de instruções de intercalação muito semelhantes às do harmônico.

Embora cada um dos exemplos de harmônicos, supercat e davidf seja um pouco artificial, juntos eles ainda servem para fornecer uma razão tangível pela qual f(i=-1, i=-1)deve haver um comportamento indefinido.

Aceitei a resposta do harmic porque fazia o melhor trabalho para abordar todos os significados do porquê, mesmo que a resposta de Paul Draper tenha abordado melhor a parte "por que causa".

outras respostas

JohnB ressalta que, se considerarmos operadores de atribuição sobrecarregados (em vez de escalares simples), também podemos ter problemas.

Nicu Stiurca
fonte
1
Um objeto escalar é um objeto do tipo escalar. Consulte 3.9 / 9: "Tipos aritméticos (3.9.1), tipos de enumeração, tipos de ponteiros, ponteiros para tipos de membros (3.9.2) std::nullptr_te versões qualificadas para cv desses tipos (3.9.3) são coletivamente chamadas de tipos escalares . "
Rob Kennedy
1
Talvez haja um erro na página e eles realmente tenham significado f(i-1, i = -1)ou algo parecido.
Lister
Dê uma olhada nesta pergunta: stackoverflow.com/a/4177063/71074
Robert S. Barnes
@RobKennedy Thanks. "Tipos aritméticos" incluem bool?
Nicu Stiurca
1
SchighSchagh sua atualização deve estar na seção de respostas.
Grijesh Chauhan

Respostas:

343

Como as operações não são subsequentes, não há nada a dizer que as instruções que executam a atribuição não podem ser intercaladas. Pode ser o ideal, dependendo da arquitetura da CPU. A página referenciada afirma isso:

Se A não for sequenciado antes de B e B não for sequenciado antes de A, existem duas possibilidades:

  • as avaliações de A e B não são subsequentes: elas podem ser executadas em qualquer ordem e podem se sobrepor (em um único encadeamento de execução, o compilador pode intercalar as instruções da CPU que compreendem A e B)

  • as avaliações de A e B são seqüenciadas indeterminadamente: elas podem ser executadas em qualquer ordem, mas não podem se sobrepor: A estará completa antes de B ou B completa antes de A. A ordem pode ser o oposto na próxima vez que a mesma expressão é avaliado.

Isso por si só não parece causar um problema - supondo que a operação que está sendo executada esteja armazenando o valor -1 em um local de memória. Mas também não há nada a dizer que o compilador não pode otimizar isso em um conjunto separado de instruções que tenha o mesmo efeito, mas que poderá falhar se a operação for intercalada com outra operação no mesmo local da memória.

Por exemplo, imagine que era mais eficiente zerar a memória e depois diminuí-la, em comparação com carregar o valor -1 in. Então, este:

f(i=-1, i=-1)

pode se tornar:

clear i
clear i
decr i
decr i

Agora eu sou -2.

Provavelmente é um exemplo falso, mas é possível.

prejudicial
fonte
59
Um exemplo muito bom de como a expressão poderia realmente fazer algo inesperado enquanto estava em conformidade com as regras de seqüenciamento. Sim, um pouco artificial, mas também é o código que estou perguntando em primeiro lugar. :)
Nicu Stiurca
10
E mesmo que a atribuição seja feita como uma operação atômica, é possível conceber uma arquitetura superescalar em que ambas as atribuições sejam feitas simultaneamente, causando conflito de acesso à memória que resulta em falha. A linguagem foi projetada para que os escritores do compilador tenham o máximo de liberdade possível ao usar as vantagens da máquina de destino.
ACH
11
Eu realmente gosto de seu exemplo de como mesmo atribuir o mesmo valor para a mesma variável em ambos os parâmetros pode resultar em um resultado inesperado, porque as duas atribuições são unsequenced
Martin J.
1
+ 1e + 6 (ok, +1) para o ponto que o código compilado nem sempre é o que você esperaria. Os otimizadores são realmente bons em jogar esse tipo de curva para você quando você não segue as regras: P
Corey
3
No processador Arm, uma carga de 32 bits pode levar até 4 instruções: faz load 8bit immediate and shiftaté 4 vezes. Normalmente, o compilador fará endereçamento indireto para buscar um número em uma tabela para evitar isso. (-1 pode ser feito em 1 instrução, mas outro exemplo pode ser escolhido).
CTRL-ALT-DELOR
208

Primeiro, "objeto escalar" significa um tipo como a int, floatou um ponteiro (consulte O que é um objeto escalar em C ++? ).


Segundo, pode parecer mais óbvio que

f(++i, ++i);

teria um comportamento indefinido. Mas

f(i = -1, i = -1);

é menos óbvio.

Um exemplo um pouco diferente:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

Que tarefa aconteceu "por último" i = 1, ou i = -1? Não está definido no padrão. Realmente, esse meio ipoderia ser 5(veja a resposta do harmic para uma explicação completamente plausível de como isso deve ser o caso). Ou você programa pode falhar. Ou reformate seu disco rígido.

Mas agora você pergunta: "E o meu exemplo? Eu usei o mesmo valor ( -1) para as duas tarefas. O que poderia não estar claro sobre isso?"

Você está correto ... exceto na maneira como o comitê de padrões C ++ descreveu isso.

Se um efeito colateral em um objeto escalar não for relacionado em relação a outro efeito colateral no mesmo objeto escalar, o comportamento será indefinido.

Eles poderiam ter feito uma exceção especial para o seu caso especial, mas não o fizeram. (E por que deveriam? Que utilidade isso poderia ter?) Então, iainda poderia ser 5. Ou seu disco rígido pode estar vazio. Portanto, a resposta para sua pergunta é:

É um comportamento indefinido porque não está definido qual é o comportamento.

(Isso merece ênfase, porque muitos programadores acham que "indefinido" significa "aleatório" ou "imprevisível". Isso não acontece; significa não definido pelo padrão. O comportamento pode ser 100% consistente e ainda indefinido.)

Poderia ter sido um comportamento definido? Sim. Foi definido? Não. Portanto, é "indefinido".

Dito isto, "indefinido" não significa que um compilador irá formatar seu disco rígido ... significa que poderia e ainda seria um compilador compatível com os padrões. Realisticamente, tenho certeza de que g ++, Clang e MSVC farão o que você esperava. Eles simplesmente não "precisavam".


Uma pergunta diferente pode ser: Por que o comitê de padrões do C ++ optou por tornar esse efeito colateral sem conseqüência? . Essa resposta envolverá a história e as opiniões do comitê. Ou o que é bom em ter esse efeito colateral sem seqüência em C ++? , que permite qualquer justificativa, se foi ou não o raciocínio real do comitê de normas. Você pode fazer essas perguntas aqui ou em programmers.stackexchange.com.

Paul Draper
fonte
9
@ DVD, sim, na verdade eu sei que se você ativar o -Wsequence-pointg ++, ele avisará.
Paul Draper
47
"Tenho certeza de que g ++, Clang e MSVC farão o que você esperava" Eu não confiaria em um compilador moderno. Eles são maus. Por exemplo, eles podem reconhecer que esse é um comportamento indefinido e assumir que esse código é inacessível. Se não o fizerem hoje, poderão fazê-lo amanhã. Qualquer UB é uma bomba-relógio.
CodesInChaos
8
@BlacklightShining "sua resposta é ruim porque não é boa" não é um feedback muito útil, é?
Vincent van der Weele
13
@BobJarvis As compilações não têm absolutamente nenhuma obrigação de gerar código, mesmo remotamente correto, diante de um comportamento indefinido. É possível até assumir que esse código nunca é chamado e, portanto, substituir a coisa toda por um nop (Observe que os compiladores realmente fazem essas suposições em face do UB). Portanto, eu diria que a reação correta a tal relatório de erro só pode ser "fechada, funciona como pretendido"
Grizzly
7
@SchighSchagh Às vezes, uma reformulação dos termos (que apenas na superfície parece ser uma resposta tautológica) é o que as pessoas precisam. A maioria das pessoas que não conhece as especificações técnicas acha que isso undefined behaviorsignifica something random will happen, o que está longe de ser o caso na maioria das vezes.
Izkata
27

Um motivo prático para não fazer uma exceção das regras apenas porque os dois valores são os mesmos:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

Considere o caso em que isso foi permitido.

Agora, alguns meses depois, surge a necessidade de mudar

 #define VALUEB 2

Aparentemente inofensivo, não é? E, de repente, o prog.cpp não compilaria mais. No entanto, sentimos que a compilação não deve depender do valor de um literal.

Conclusão: não há exceção à regra, pois isso faria com que a compilação bem-sucedida dependesse do valor (e não do tipo) de uma constante.

EDITAR

O @HeartWare apontou que expressões constantes do formulário A DIV Bnão são permitidas em alguns idiomas, quando Bé 0, e causam falha na compilação. Portanto, a alteração de uma constante pode causar erros de compilação em outro local. O que é, IMHO, infeliz. Mas certamente é bom restringir essas coisas ao inevitável.

Ingo
fonte
Claro, mas o exemplo faz uso inteiro literais. Seu f(i = VALUEA, i = VALUEB);definitivamente tem o potencial de comportamento indefinido. Espero que você não esteja realmente codificando valores por trás dos identificadores.
Wolf
3
@Wold Mas o compilador não vê macros do pré-processador. E mesmo que não fosse assim, é difícil encontrar um exemplo em qualquer linguagem de programação, em que um código-fonte seja compilado até que alguém mude alguma constante de 1 para 2. Isso é simplesmente inaceitável e inexplicável, enquanto você vê explicações muito boas aqui por que esse código é quebrado mesmo com os mesmos valores.
Ingo
Sim, as compilações não veem macros. Mas, era essa a pergunta?
Wolf
1
Sua resposta está errada , leia a resposta do harmic e o comentário do OP.
Wolf
1
Poderia fazer SomeProcedure(A, B, B DIV (2-A)). De qualquer forma, se o idioma declarar que o CONST deve ser totalmente avaliado em tempo de compilação, é claro que minha afirmação não é válida para esse caso. Uma vez que, de alguma forma, confunde a distinção de compiletime e tempo de execução. Também notaria se escrevêssemos CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ?? Ou as funções não são permitidas?
Ingo
12

A confusão é que armazenar um valor constante em uma variável local não é uma instrução atômica em toda arquitetura em que o C foi projetado para ser executado. O processador em que o código é executado é mais importante que o compilador nesse caso. Por exemplo, no ARM, em que cada instrução não pode transportar uma constante completa de 32 bits, o armazenamento de um int em uma variável precisa de mais de uma instrução. Exemplo com este pseudo código em que você só pode armazenar 8 bits por vez e deve trabalhar em um registro de 32 bits, i é um int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

Você pode imaginar que, se o compilador deseja otimizar, ele pode intercalar a mesma sequência duas vezes e você não sabe qual valor será gravado em i; e vamos dizer que ele não é muito inteligente:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

No entanto, em meus testes, o gcc é gentil o suficiente para reconhecer que o mesmo valor é usado duas vezes e o gera uma vez e não faz nada estranho. Recebo -1, -1 Mas meu exemplo ainda é válido, pois é importante considerar que mesmo uma constante pode não ser tão óbvia quanto parece.

davidf
fonte
Suponho que no ARM o compilador carregue apenas a constante de uma tabela. O que você descreve parece mais com MIPS.
ach
1
@AndreyChernyakhovskiy Sim, mas em um caso em que não é simplesmente -1(que o compilador armazenou em algum lugar), mas sim 3^81 mod 2^32, mas constante, então o compilador pode fazer exatamente o que é feito aqui, e em alguma alavanca de otimização, eu intercale as seqüências de chamada para evitar esperar.
yo '
@tohecz, sim, eu já verifiquei. De fato, o compilador é inteligente demais para carregar todas as constantes de uma tabela. De qualquer forma, ele nunca usaria o mesmo registro para calcular as duas constantes. Isso certamente "indefiniria" o comportamento definido.
ACH
@AndreyChernyakhovskiy Mas você provavelmente não é "todo programador de compiladores C ++ do mundo". Lembre-se de que existem máquinas com três registros curtos disponíveis apenas para cálculos.
yo '
@tohecz, considere o exemplo f(i = A, j = B)where ie jsão dois objetos separados. Este exemplo não possui UB. Máquina com 3 registros curtos não é desculpa para o compilador misturar os dois valores de Ae Bno mesmo registro (como mostrado na resposta do @ davidf), porque isso quebraria a semântica do programa.
ACH
11

O comportamento é geralmente especificado como indefinido se houver alguma razão concebível para que um compilador que estava tentando ser "útil" possa fazer algo que cause um comportamento totalmente inesperado.

No caso em que uma variável é gravada várias vezes sem nada para garantir que as gravações ocorram em momentos distintos, alguns tipos de hardware podem permitir que várias operações de "armazenamento" sejam executadas simultaneamente em endereços diferentes usando uma memória de porta dupla. No entanto, algumas memórias de porta dupla proíbem expressamente o cenário em que duas lojas atingem o mesmo endereço simultaneamente, independentemente de os valores escritos corresponderem ou não. Se um compilador para essa máquina perceber duas tentativas não consecutivas de gravar a mesma variável, ele poderá se recusar a compilar ou garantir que as duas gravações não possam ser agendadas simultaneamente. Mas se um ou ambos os acessos forem via ponteiro ou referência, o compilador nem sempre poderá saber se as duas gravações podem atingir o mesmo local de armazenamento. Nesse caso, ele pode agendar as gravações simultaneamente, causando uma interceptação de hardware na tentativa de acesso.

Obviamente, o fato de alguém poder implementar um compilador C em tal plataforma não sugere que esse comportamento não deva ser definido em plataformas de hardware ao usar lojas de tipos pequenos o suficiente para serem processados ​​atomicamente. Tentar armazenar dois valores diferentes de maneira não sequencial pode causar estranheza se um compilador não estiver ciente disso; por exemplo, dado:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

se o compilador alinha a chamada para "moo" e pode dizer que não modifica "v", ele pode armazenar um 5 para v, em seguida, um 6 para * p, depois passar 5 para "zoo" e, em seguida, passe o conteúdo de v para "zoo". Se "zoo" não modificar "v", não haverá como as duas chamadas passarem valores diferentes, mas isso poderia facilmente acontecer de qualquer maneira. Por outro lado, nos casos em que ambas as lojas escreveriam o mesmo valor, essa estranheza não poderia ocorrer e, na maioria das plataformas, não haveria razão sensata para uma implementação fazer algo estranho. Infelizmente, alguns escritores de compiladores não precisam de desculpas para comportamentos tolos além de "porque o Padrão permite", portanto, mesmo esses casos não são seguros.

supercat
fonte
9

O fato de o resultado ser o mesmo na maioria das implementações nesse caso é incidental; a ordem da avaliação ainda está indefinida. Considere f(i = -1, i = -2): aqui, a ordem é importante. A única razão pela qual isso não importa no seu exemplo é o acidente de ambos os valores -1.

Dado que a expressão é especificada como uma com comportamento indefinido, um compilador compatível com códigos maliciosos pode exibir uma imagem inadequada ao avaliar f(i = -1, i = -1)e interromper a execução - e ainda ser considerado completamente correto. Felizmente, nenhum compilador de que conheço o faça.

Amadan
fonte
8

Parece-me que a única regra referente ao seqüenciamento da expressão do argumento da função está aqui:

3) Ao chamar uma função (se a função está ou não embutida e se a sintaxe explícita da chamada da função é usada), todo cálculo de valor e efeito colateral associado a qualquer expressão de argumento ou à expressão postfix que designa a função chamada é: sequenciado antes da execução de cada expressão ou instrução no corpo da função chamada.

Isso não define o seqüenciamento entre expressões de argumento, portanto, acabamos neste caso:

1) Se um efeito colateral em um objeto escalar não for relacionado em relação a outro efeito colateral no mesmo objeto escalar, o comportamento será indefinido.

Na prática, na maioria dos compiladores, o exemplo que você citou funcionará bem (em oposição a "apagar o disco rígido" e outras consequências teóricas e indefinidas do comportamento).
É, no entanto, um passivo, pois depende do comportamento específico do compilador, mesmo que os dois valores atribuídos sejam os mesmos. Além disso, obviamente, se você tentasse atribuir valores diferentes, os resultados seriam "verdadeiramente" indefinidos:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}
Martin J.
fonte
8

O C ++ 17 define regras de avaliação mais rígidas. Em particular, sequencia argumentos de função (embora em ordem não especificada).

N5659 §4.6:15
As avaliações A e B são sequenciadas indeterminadamente quando A é sequenciado antes de B ou B é sequenciado antes de A , mas não é especificado qual. [ Nota : As avaliações seqüenciadas indeterminadamente não podem se sobrepor, mas podem ser executadas primeiro. - nota final ]

N5659 § 8.2.2:5
A inicialização de um parâmetro, incluindo todo cálculo de valor associado e efeito colateral, é indeterminada em sequência em relação a qualquer outro parâmetro.

Ele permite alguns casos que seriam UB antes:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one
AlexD
fonte
2
Obrigado por adicionar esta atualização para o c ++ 17 , por isso não precisei. ;)
Yakk - Adam Nevraumont
Impressionante, muito obrigado por esta resposta. Pequeno acompanhamento: se fa assinatura fosse f(int a, int b), o C ++ 17 garante isso a == -1e b == -2se chamado como no segundo caso?
Nicu Stiurca 2/11
Sim. Se tivermos parâmetros ae b, i-then- aserão inicializados para -1, depois i-then- bserão inicializados para -2, ou o caminho a seguir. Nos dois casos, acabamos com a == -1e b == -2. Pelo menos é assim que eu leio " A inicialização de um parâmetro, incluindo todo cálculo de valor associado e efeito colateral, é sequenciada indeterminadamente em relação à de qualquer outro parâmetro ".
AlexD
Eu acho que tem sido o mesmo em C desde sempre.
fuz 13/07/19
5

O operador de atribuição pode estar sobrecarregado; nesse caso, o pedido pode importar:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true
JohnB
fonte
1
É verdade, mas a pergunta era sobre tipos escalares , que outros salientaram que significa essencialmente família int, família float e ponteiros.
Nicu Stiurca
O verdadeiro problema nesse caso é que o operador de atribuição é estável, portanto, mesmo a manipulação regular da variável é propensa a problemas como esse.
AJMansfield
2

Isso é apenas responder ao "não sei o que" objeto escalar "poderia significar além de algo como um int ou um float".

Eu interpretaria o "objeto escalar" como uma abreviação de "objeto do tipo escalar" ou apenas "variável do tipo escalar". Em seguida, pointer, enum(constante) são de tipo escalar.

Este é um artigo do MSDN sobre Tipos escalares .

Peng Zhang
fonte
Isso parece um pouco como uma "resposta apenas ao link". Você pode copiar os bits relevantes desse link para esta resposta (em uma citação em bloco)?
Cole Johnson
1
@ColeJohnson Esta não é uma resposta apenas do link. O link é apenas para mais explicações. Minha resposta é "ponteiro", "enum".
Peng Zhang
Não disse que sua resposta era apenas um link. Eu disse que "parece um [um]" . Sugiro que você leia por que não queremos respostas apenas para links na seção de ajuda. O motivo é que, se a Microsoft atualizar seus URLs no site, esse link será interrompido.
Cole Johnson
1

Na verdade, há uma razão para não depender do fato de o compilador verificar ise o mesmo valor foi atribuído duas vezes, para que seja possível substituí-lo por uma única atribuição. E se tivermos algumas expressões?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}
polkovnikov.ph
fonte
1
Não precisa provar o teorema de Fermat: basta atribuir 1a i. Ambos os argumentos atribuem 1e isso faz a coisa "certa" ou os argumentos atribuem valores diferentes e seu comportamento indefinido, portanto nossa escolha ainda é permitida.