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 i
termina como -1
e 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+1
e o segundo i+2
ou vice-versa; no segundo, não está claro se i
deve 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: i
acaba 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.
fonte
std::nullptr_t
e versões qualificadas para cv desses tipos (3.9.3) são coletivamente chamadas de tipos escalares . "f(i-1, i = -1)
ou algo parecido.Respostas:
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:
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:
pode se tornar:
Agora eu sou -2.
Provavelmente é um exemplo falso, mas é possível.
fonte
load 8bit immediate and shift
até 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).Primeiro, "objeto escalar" significa um tipo como a
int
,float
ou um ponteiro (consulte O que é um objeto escalar em C ++? ).Segundo, pode parecer mais óbvio que
teria um comportamento indefinido. Mas
é menos óbvio.
Um exemplo um pouco diferente:
Que tarefa aconteceu "por último"
i = 1
, oui = -1
? Não está definido no padrão. Realmente, esse meioi
poderia ser5
(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.
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,
i
ainda poderia ser5
. 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.
fonte
-Wsequence-point
g ++, ele avisará.undefined behavior
significasomething random will happen
, o que está longe de ser o caso na maioria das vezes.Um motivo prático para não fazer uma exceção das regras apenas porque os dois valores são os mesmos:
Considere o caso em que isso foi permitido.
Agora, alguns meses depois, surge a necessidade de mudar
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 B
não são permitidas em alguns idiomas, quandoB
é 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.fonte
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.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êssemosCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y;
?? Ou as funções não são permitidas?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:
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:
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.
fonte
-1
(que o compilador armazenou em algum lugar), mas sim3^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.f(i = A, j = B)
wherei
ej
sã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 deA
eB
no mesmo registro (como mostrado na resposta do @ davidf), porque isso quebraria a semântica do programa.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:
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.
fonte
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.fonte
Parece-me que a única regra referente ao seqüenciamento da expressão do argumento da função está aqui:
Isso não define o seqüenciamento entre expressões de argumento, portanto, acabamos neste caso:
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:
fonte
O C ++ 17 define regras de avaliação mais rígidas. Em particular, sequencia argumentos de função (embora em ordem não especificada).
Ele permite alguns casos que seriam UB antes:
fonte
f
a assinatura fossef(int a, int b)
, o C ++ 17 garante issoa == -1
eb == -2
se chamado como no segundo caso?a
eb
,i
-then-a
serão inicializados para -1, depoisi
-then-b
serão inicializados para -2, ou o caminho a seguir. Nos dois casos, acabamos coma == -1
eb == -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 ".O operador de atribuição pode estar sobrecarregado; nesse caso, o pedido pode importar:
fonte
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 .
fonte
Na verdade, há uma razão para não depender do fato de o compilador verificar
i
se 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?fonte
1
ai
. Ambos os argumentos atribuem1
e isso faz a coisa "certa" ou os argumentos atribuem valores diferentes e seu comportamento indefinido, portanto nossa escolha ainda é permitida.