Em que ponto do loop o estouro de inteiro torna-se um comportamento indefinido?


86

Este é um exemplo para ilustrar minha pergunta, que envolve um código muito mais complicado que não posso postar aqui.

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

Este programa contém comportamento indefinido na minha plataforma porque airá estourar no terceiro loop.

Isso faz com que todo o programa tenha um comportamento indefinido ou apenas depois que o estouro realmente acontecer ? O compilador poderia potencialmente descobrir que a irá estourar para que possa declarar todo o loop indefinido e não se incomodar em executar o printfs, mesmo que todos eles ocorram antes do estouro?

(Etiquetados como C e C ++, embora sejam diferentes, porque eu estaria interessado em respostas para ambas as linguagens, caso fossem diferentes).



12
Você pode gostar de My Little Optimizer: Undefined Behavior is Magic da CppCon este ano. É tudo sobre o que os compiladores de otimizações podem realizar com base em comportamento indefinido.
TartanLlama,



Respostas:


108

Se você estiver interessado em uma resposta puramente teórica, o padrão C ++ permite um comportamento indefinido para "viagem no tempo":

[intro.execution]/5: Uma implementação conforme executando um programa bem formado deve produzir o mesmo comportamento observável que uma das execuções possíveis da instância correspondente da máquina abstrata com o mesmo programa e a mesma entrada. No entanto, se qualquer execução contiver uma operação indefinida, esta Norma Internacional não impõe nenhum requisito sobre a implementação que executa esse programa com essa entrada (nem mesmo em relação às operações anteriores à primeira operação indefinida)

Dessa forma, se o seu programa contém um comportamento indefinido, o comportamento de todo o programa é indefinido.


4
@KeithThompson: Mas então, a sneeze()função em si é indefinida em qualquer coisa da classe Demon(da qual a variedade nasal é uma subclasse), tornando a coisa toda circular de qualquer maneira.
Sebastian Lenartowicz

1
Mas printf pode não retornar, então as duas primeiras rodadas são definidas porque até que sejam concluídas não está claro se haverá UB. Consulte stackoverflow.com/questions/23153445/…
usr

1
É por isso que um compilador é tecnicamente dentro de seus direitos para emitir "nop" para o kernel Linux (porque o código de inicialização depende de um comportamento indefinido): blog.regehr.org/archives/761
Crashworks

3
@Crashworks E é por isso que o Linux é escrito e compilado como um C. não portável (ou seja, um superconjunto de C que requer um compilador específico com opções específicas, como -fno-strict-aliasing)
user253751

3
@usr Espero que esteja definido se printfnão retornar, mas se printffor retornar, então o comportamento indefinido pode causar problemas antes de printfser chamado. Conseqüentemente, viagem no tempo. printf("Hello\n");e a próxima linha é compilada comoundoPrintf(); launchNuclearMissiles();
user253751

31

Primeiro, deixe-me corrigir o título desta pergunta:

Comportamento indefinido não é (especificamente) do reino da execução.

O comportamento indefinido afeta todas as etapas: compilar, vincular, carregar e executar.

Alguns exemplos para cimentar isso, tenha em mente que nenhuma seção é exaustiva:

  • o compilador pode presumir que partes do código que contêm comportamento indefinido nunca são executadas e, portanto, presumir que os caminhos de execução que levariam a eles são código morto. Veja o que todo programador C deve saber sobre comportamento indefinido de ninguém menos que Chris Lattner.
  • o vinculador pode assumir que, na presença de múltiplas definições de um símbolo fraco (reconhecido pelo nome), todas as definições são idênticas graças à Regra de Uma Definição
  • o carregador (no caso de você usar bibliotecas dinâmicas) pode assumir o mesmo, escolhendo assim o primeiro símbolo que encontrar; isso é geralmente (ab) usado para interceptar chamadas usando LD_PRELOADtruques em Unixes
  • a execução pode falhar (SIGSEV) se você usar ponteiros pendentes

Isso é o que é tão assustador sobre o comportamento indefinido: é quase impossível prever, com antecedência, que comportamento exato ocorrerá, e essa previsão deve ser revisada a cada atualização do conjunto de ferramentas, sistema operacional subjacente, ...


Recomendo assistir a este vídeo de Michael Spencer (Desenvolvedor LLVM): CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .


3
Isso é o que me preocupa. No meu código real, é complexo, mas eu poderia ter um caso em que será sempre estouro. E eu realmente não me importo com isso, mas temo que o código "correto" também seja afetado por isso. Obviamente, preciso consertar, mas consertar requer compreensão :)
jcoder

8
@jcoder: Há um escape importante aqui. O compilador não tem permissão para adivinhar os dados de entrada. Contanto que haja pelo menos uma entrada para a qual o Comportamento Indefinido não ocorra, o compilador deve garantir que essa entrada específica ainda produza a saída correta. Toda a conversa assustadora sobre otimizações perigosas se aplica apenas ao UB inevitável . Praticamente falando, se você tivesse usado argccomo contagem de loop, o caso argc=1não produziria UB e o compilador seria forçado a lidar com isso.
MSalters

@jcoder: neste caso, este não é um código morto. O compilador, entretanto, pode ser inteligente o suficiente para deduzir que inão pode ser incrementado mais do que Nvezes e, portanto, que seu valor é limitado.
Matthieu M.

4
@jcoder: se f(good);faz alguma coisa X e f(bad);invoca um comportamento indefinido, então um programa que apenas invoca f(good);garante fazer X, mas f(good); f(bad);não é garantido que fará X.

4
@Hurkyl mais interessante, se o seu código for if(foo) f(good); else f(bad);, um compilador inteligente descartará a comparação e produzirá um incondicional foo(good).
John Dvorak

28

Um agressivamente optimizar compilador C ou C ++ segmentação um pouco 16 intvai saber que o comportamento em adição 1000000000a um inttipo é indefinido .

É permitido por qualquer um dos padrões fazer qualquer coisa que desejar, que pode incluir a exclusão de todo o programa, e sair int main(){}.

Mas e quanto a ints maiores ? Não conheço um compilador que faça isso ainda (e não sou um especialista em design de compiladores C e C ++ de forma alguma), mas imagino que, em algum momento, um compilador voltado para 32 bits intou superior descobrirá que o loop é infinito ( inão muda) e então aacabará por transbordar. Então, mais uma vez, ele pode otimizar a saída para int main(){}. O que estou tentando enfatizar aqui é que, conforme as otimizações do compilador se tornam progressivamente mais agressivas, mais e mais construções de comportamento indefinido estão se manifestando de maneiras inesperadas.

O fato de seu loop ser infinito não é em si indefinido, pois você está gravando na saída padrão no corpo do loop.


3
É permitido pelo padrão fazer qualquer coisa que quiser antes mesmo que o comportamento indefinido se manifeste? Onde isso é declarado?
Jimifiki,

4
porque 16 bits? Acho que OP está procurando um estouro assinado de 32 bits.
4386427

8
@jimifiki No padrão. C ++ 14 (N4140) 1.3.24 "Comportamento definido = comportamento para o qual esta Norma Internacional não impõe requisitos." Além de uma longa nota que elabora. Mas a questão é que não é o comportamento de uma "instrução" que é indefinida, é o comportamento do programa. Isso significa que, enquanto o UB for acionado por uma regra no padrão (ou pela ausência de uma regra), o padrão deixa de se aplicar ao programa como um todo. Portanto, qualquer parte do programa pode se comportar como quiser.
Angew não está mais orgulhoso de SO

5
A primeira afirmação está errada. Se intfor de 16 bits, a adição ocorrerá em long(porque o operando literal tem tipo long) onde está bem definido e, em seguida, será convertido por uma conversão definida pela implementação de volta para int.
R .. GitHub PARAR DE AJUDAR O ICE

2
@usr o comportamento de printfé definido pelo padrão para sempre retornar
MM

11

Tecnicamente, sob o padrão C ++, se um programa contém um comportamento indefinido, o comportamento de todo o programa, mesmo em tempo de compilação (antes mesmo de o programa ser executado), é indefinido.

Na prática, porque o compilador pode assumir (como parte de uma otimização) que o estouro não ocorrerá, pelo menos o comportamento do programa na terceira iteração do loop (assumindo uma máquina de 32 bits) será indefinido, embora é provável que você obtenha resultados corretos antes da terceira iteração. No entanto, uma vez que o comportamento de todo o programa é tecnicamente indefinido, não há nada que impeça o programa de gerar uma saída completamente incorreta (incluindo nenhuma saída), travando em tempo de execução em qualquer ponto durante a execução ou mesmo falhando totalmente na compilação (já que o comportamento indefinido se estende até tempo de compilação).

O comportamento indefinido fornece ao compilador mais espaço para otimizar porque elimina certas suposições sobre o que o código deve fazer. Ao fazer isso, os programas que dependem de suposições que envolvem comportamento indefinido não têm a garantia de funcionar conforme o esperado. Dessa forma, você não deve confiar em nenhum comportamento específico que seja considerado indefinido pelo padrão C ++.


E se a parte UB estiver dentro de um if(false) {}escopo? Isso envenena todo o programa, devido ao compilador assumir que todos os ramos contêm porções bem definidas de lógica e, portanto, operar em suposições erradas?
mlvljr

1
O padrão não impõe nenhum requisito sobre o comportamento indefinido, portanto, em teoria , sim, ele envenena todo o programa. No entanto, na prática , qualquer compilador de otimização provavelmente apenas removerá o código morto, portanto, provavelmente não teria nenhum efeito na execução. Você ainda não deve confiar neste comportamento, no entanto.
bwDraco

Bom saber, obrigado :)
mlvljr

9

Para entender por que o comportamento indefinido pode 'viajar no tempo', como @TartanLlama colocou adequadamente , vamos dar uma olhada na regra 'como se':

1.9 Execução do programa

1 As descrições semânticas neste Padrão Internacional definem uma máquina abstrata não determinística parametrizada. Este Padrão Internacional não impõe requisitos à estrutura de implementações em conformidade. Em particular, eles não precisam copiar ou emular a estrutura da máquina abstrata. Em vez disso, implementações em conformidade são necessárias para emular (apenas) o comportamento observável da máquina abstrata, conforme explicado abaixo.

Com isso, podemos ver o programa como uma 'caixa preta' com uma entrada e uma saída. A entrada pode ser entrada do usuário, arquivos e muitas outras coisas. A saída é o 'comportamento observável' mencionado na norma.

O padrão apenas define um mapeamento entre a entrada e a saída, nada mais. Ele faz isso descrevendo um 'exemplo de caixa preta', mas diz explicitamente que qualquer outra caixa preta com o mesmo mapeamento é igualmente válida. Isso significa que o conteúdo da caixa preta é irrelevante.

Com isso em mente, não faria sentido dizer que um comportamento indefinido ocorre em um determinado momento. Na implementação de amostra da caixa preta, poderíamos dizer onde e quando isso acontece, mas a caixa preta real poderia ser algo completamente diferente, então não podemos mais dizer onde e quando isso acontece. Teoricamente, um compilador poderia, por exemplo, decidir enumerar todas as entradas possíveis e pré-computar as saídas resultantes. Então, o comportamento indefinido teria acontecido durante a compilação.

O comportamento indefinido é a inexistência de um mapeamento entre a entrada e a saída. Um programa pode ter comportamento indefinido para alguma entrada, mas comportamento definido para outra. Então, o mapeamento entre entrada e saída é simplesmente incompleto; há entrada para a qual não existe mapeamento para saída.
O programa em questão tem comportamento indefinido para qualquer entrada, portanto, o mapeamento está vazio.


6

Supondo que intseja de 32 bits, o comportamento indefinido ocorre na terceira iteração. Portanto, se, por exemplo, o loop só fosse alcançável condicionalmente, ou pudesse ser encerrado condicionalmente antes da terceira iteração, não haveria comportamento indefinido a menos que a terceira iteração fosse realmente alcançada. No entanto, no caso de comportamento indefinido, toda a saída do programa é indefinida, incluindo a saída que está "no passado" em relação à invocação de comportamento indefinido. Por exemplo, no seu caso, isso significa que não há garantia de ver 3 mensagens de "Olá" na saída.


6

A resposta da TartanLlama está correta. O comportamento indefinido pode acontecer a qualquer momento, mesmo durante o tempo de compilação. Isso pode parecer absurdo, mas é um recurso importante para permitir que os compiladores façam o que precisam. Nem sempre é fácil ser um compilador. Você tem que fazer exatamente o que a especificação diz, todas as vezes. No entanto, às vezes pode ser monstruosamente difícil provar que um determinado comportamento está ocorrendo. Se você se lembra do problema da parada, é bastante trivial desenvolver um software para o qual você não pode provar se ele completa ou entra em um loop infinito quando alimentado com uma entrada específica.

Poderíamos fazer os compiladores serem pessimistas e compilar constantemente com medo de que a próxima instrução possa ser um desses problemas de travamento, mas isso não é razoável. Em vez disso, damos uma chance ao compilador: nesses tópicos de "comportamento indefinido", eles ficam livres de qualquer responsabilidade. O comportamento indefinido consiste em todos os comportamentos que são tão sutilmente nefastos que temos dificuldade em separá-los dos problemas de parada realmente desagradáveis ​​e nefastos e outros enfeites.

Há um exemplo que adoro postar, embora admita que perdi a fonte, então tenho que parafrasear. Era de uma versão específica do MySQL. No MySQL, eles tinham um buffer circular que era preenchido com dados fornecidos pelo usuário. Eles, é claro, queriam ter certeza de que os dados não estourassem o buffer, então tiveram que verificar:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

Parece sensato o suficiente. No entanto, e se numberOfNewChars for realmente grande e estourar? Em seguida, ele envolve e se torna um ponteiro menor que endOfBufferPtr, de modo que a lógica de estouro nunca seria chamada. Então, eles adicionaram uma segunda verificação, antes dessa:

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

Parece que você cuidou do erro de estouro de buffer, certo? No entanto, um bug foi enviado informando que este buffer estourou em uma versão particular do Debian! Uma investigação cuidadosa mostrou que esta versão do Debian foi a primeira a usar uma versão particularmente avançada do gcc. Nesta versão do gcc, o compilador reconheceu que currentPtr + numberOfNewChars nunca pode ser um ponteiro menor que currentPtr porque estouro de ponteiros é um comportamento indefinido! Isso foi suficiente para o gcc otimizar toda a verificação e, de repente, você não estava protegido contra estouros de buffer , embora tenha escrito o código para verificar isso!

Este era um comportamento específico. Tudo estava legal (embora pelo que eu ouvi, o gcc reverteu essa mudança na próxima versão). Não é o que eu consideraria um comportamento intuitivo, mas se você forçar um pouco a imaginação, é fácil ver como uma ligeira variante dessa situação pode se tornar um problema de travamento para o compilador. Por causa disso, os redatores das especificações o tornaram "Comportamento indefinido" e afirmaram que o compilador poderia fazer absolutamente qualquer coisa que quisesse.


Não considero compiladores especialmente surpreendentes que às vezes se comportam como se a aritmética assinada fosse executada em tipos cujo intervalo se estende além de "int", especialmente considerando que mesmo ao fazer a geração de código direta em x86, há momentos em que fazer isso é mais eficiente do que truncar intermediário resultados. O que é mais surpreendente é quando o estouro afeta outros cálculos, o que pode acontecer no gcc mesmo se o código armazena o produto de dois valores uint16_t em um uint32_t - uma operação que não deve ter nenhuma razão plausível para agir de forma surpreendente em uma compilação não higienizadora.
supercat

Claro, a verificação correta seria if(numberOfNewChars > endOfBufferPtr - currentPtr), desde que numberOfNewChars nunca possa ser negativo e currentPtr sempre aponte para algum lugar dentro do buffer que você nem mesmo precisa da verificação ridícula de "envolvente". (Não acho que o código que você forneceu tenha qualquer esperança de funcionar em um buffer circular - você omitiu o que for necessário para isso na paráfrase, então estou ignorando esse caso também)
Random832

@ Random832 Eu deixei uma tonelada de fora. Tentei citar o contexto mais amplo, mas, como perdi minha fonte, descobri que parafrasear o contexto me trouxe mais problemas, então deixei de fora. Eu realmente preciso encontrar aquele maldito relatório de bug para que possa citá-lo corretamente. É realmente um exemplo poderoso de como você pode pensar que escreveu código de uma maneira e que compila de maneira completamente diferente.
Cort Ammon,

Este é o meu maior problema com comportamento indefinido. Às vezes, torna-se impossível escrever o código correto e, quando o compilador o detecta, por padrão não informa que o comportamento indefinido foi acionado. Neste caso, o usuário simplesmente deseja fazer aritmética - ponteiro ou não - e todo o seu trabalho árduo para escrever código seguro foi desfeito. Deve haver pelo menos uma maneira de anotar uma seção de código para dizer - sem otimizações extravagantes aqui. C / C ++ é usado em muitas áreas críticas para permitir que esta situação perigosa continue a favor da otimização
John McGrath

4

Além das respostas teóricas, uma observação prática seria que por muito tempo os compiladores aplicaram várias transformações em loops para reduzir a quantidade de trabalho realizado dentro deles. Por exemplo, dado:

for (int i=0; i<n; i++)
  foo[i] = i*scale;

um compilador pode transformar isso em:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

Salvando assim uma multiplicação com cada iteração do loop. Uma forma adicional de otimização, que os compiladores adaptaram com vários graus de agressividade, transformariam isso em:

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

Mesmo em máquinas com wraparound silencioso em transbordamento, isso poderia funcionar mal se houvesse algum número menor que n que, quando multiplicado pela escala, resultaria em 0. Também poderia se transformar em um loop infinito se a escala fosse lida da memória mais de uma vez e algo mudou seu valor inesperadamente (em qualquer caso onde "escala" pudesse mudar no meio do loop sem invocar o UB, um compilador não teria permissão para realizar a otimização).

Enquanto a maioria dessas otimizações não teria nenhum problema nos casos em que dois tipos curtos sem sinal são multiplicados para produzir um valor que está entre INT_MAX + 1 e UINT_MAX, o gcc tem alguns casos em que tal multiplicação dentro de um loop pode fazer com que o loop saia antecipadamente . Não notei esses comportamentos decorrentes de instruções de comparação no código gerado, mas é observável nos casos em que o compilador usa o estouro para inferir que um loop pode ser executado no máximo 4 ou menos vezes; por padrão, ele não gera avisos nos casos em que algumas entradas causariam UB e outras não, mesmo que suas inferências façam com que o limite superior do loop seja ignorado.


4

O comportamento indefinido é, por definição, uma área cinzenta. Você simplesmente não pode prever o que ele fará ou não - isso é o que significa "comportamento indefinido" .

Desde tempos imemoriais, os programadores sempre tentaram salvar resquícios de definição de uma situação indefinida. Eles têm um código que realmente desejam usar, mas que se revelou indefinido, então tentam argumentar: "Eu sei que é indefinido, mas com certeza irá, na pior das hipóteses, fazer isto ou aquilo; nunca fará aquilo . " E às vezes esses argumentos estão mais ou menos certos - mas freqüentemente, eles estão errados. E à medida que os compiladores ficam cada vez mais espertos (ou, algumas pessoas podem dizer, cada vez mais furtivos), os limites da questão continuam mudando.

Então, realmente, se você quiser escrever um código que funcione garantido, e que continuará funcionando por muito tempo, só há uma escolha: evite o comportamento indefinido a todo custo. Na verdade, se você se envolver com ele, ele voltará para assombrá-lo.


e, no entanto, é o seguinte ... compiladores podem usar comportamento indefinido para otimizar, mas GERALMENTE NÃO CONTAM PARA VOCÊ. Portanto, se temos essa ferramenta incrível que você deve evitar fazer X a todo custo, por que o compilador não pode lhe dar um aviso para que você possa corrigi-lo?
Jason S,

1

Uma coisa que seu exemplo não considera é a otimização. aé definido no loop, mas nunca usado, e um otimizador poderia resolver isso. Como tal, é legítimo para o otimizador descartara completamente e, nesse caso, todo comportamento indefinido desaparece como a vítima de um boojum.

No entanto, é claro que isso em si é indefinido, porque a otimização é indefinida. :)


1
Não há razão para considerar a otimização ao determinar se o comportamento é indefinido.
Keith Thompson

2
O fato de o programa se comportar como se poderia supor não significa que o comportamento indefinido "desaparece". O comportamento ainda está indefinido e você está simplesmente contando com a sorte. O próprio fato de que o comportamento do programa pode mudar com base nas opções do compilador é um forte indicador de que o comportamento é indefinido.
Jordan Melo,

@JordanMelo Como muitas das respostas anteriores discutiram sobre otimização (e o OP perguntou especificamente sobre isso), mencionei um recurso de otimização que nenhuma resposta anterior havia abordado. Também indiquei que, embora a otimização pudesse removê-lo, a confiança na otimização para funcionar de qualquer maneira específica é novamente indefinida. Certamente não estou recomendando! :)
Graham,

@KeithThompson Claro, mas o OP perguntou especificamente sobre a otimização e seu efeito no comportamento indefinido que ele veria em sua plataforma. Esse comportamento específico pode desaparecer, dependendo da otimização. Como eu disse em minha resposta, porém, a indefinição não.
Graham,

0

Uma vez que esta questão tem duas tags C e C ++, tentarei abordar ambos. C e C ++ têm abordagens diferentes aqui.

Em C, a implementação deve ser capaz de provar que o comportamento indefinido será invocado para tratar todo o programa como se ele tivesse um comportamento indefinido. No exemplo dos OPs, pareceria trivial para o compilador provar isso e, portanto, é como se todo o programa fosse indefinido.

Podemos ver isso no Relatório de defeito 109, que em seu ponto crucial pergunta:

Se, no entanto, o padrão C reconhece a existência separada de "valores indefinidos" (cuja mera criação não envolve totalmente "comportamento indefinido"), então uma pessoa fazendo o teste do compilador poderia escrever um caso de teste como o seguinte, e ele / ela também poderia esperar (ou possivelmente exigir) que uma implementação em conformidade deve, no mínimo, compilar esse código (e possivelmente também permitir sua execução) sem "falha".

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Portanto, a questão fundamental é esta: O código acima deve ser "traduzido com sucesso" (seja lá o que isso signifique)? (Veja a nota de rodapé anexada à subcláusula 5.1.1.3.)

e a resposta foi:

A Norma C usa o termo "valor indeterminado" e não "valor indefinido". O uso de um objeto de valor indeterminado resulta em um comportamento indefinido. A nota de rodapé da subcláusula 5.1.1.3 indica que uma implementação é livre para produzir qualquer número de diagnósticos, desde que um programa válido ainda seja traduzido corretamente. Se uma expressão cuja avaliação resultaria em comportamento indefinido aparece em um contexto onde uma expressão constante é necessária, o programa que o contém não está estritamente em conformidade. Além disso, se cada execução possível de um determinado programa resultasse em um comportamento indefinido, o programa fornecido não está em conformidade estrita. Uma implementação em conformidade não deve deixar de traduzir um programa em conformidade estrita simplesmente porque alguma execução possível desse programa resultaria em um comportamento indefinido. Como foo pode nunca ser chamado, o exemplo dado deve ser traduzido com sucesso por uma implementação conforme.

Em C ++, a abordagem parece mais relaxada e sugere que um programa tem um comportamento indefinido, independentemente de a implementação poder provar isso estaticamente ou não.

Temos [intro.abstrac] p5 que diz:

Uma implementação conforme executando um programa bem formado deve produzir o mesmo comportamento observável que uma das execuções possíveis da instância correspondente da máquina abstrata com o mesmo programa e a mesma entrada. No entanto, se qualquer uma dessas execuções contiver uma operação indefinida, este documento não impõe nenhum requisito à implementação que executa esse programa com aquela entrada (nem mesmo em relação às operações anteriores à primeira operação indefinida).


O fato de a execução de uma função invocar o UB só pode afetar a maneira como um programa se comporta quando recebe alguma entrada específica se pelo menos uma execução possível do programa, quando dada essa entrada, invocar o UB. O fato de que invocar uma função invocaria o UB não impede que um programa tenha um comportamento definido quando é alimentado com uma entrada que não permitiria que a função fosse invocada.
supercat

@supercat Eu acredito que é o que minha resposta nos diz para C, pelo menos.
Shafik Yaghmour

Acho que o mesmo se aplica ao texto citado em relação ao C ++, uma vez que a frase "Qualquer execução" se refere às maneiras como o programa poderia ser executado com uma determinada entrada. Se uma determinada entrada não pudesse resultar na execução de uma função, não vejo nada no texto citado que diga que qualquer coisa em tal função resultaria em UB.
supercat

-2

A principal resposta é um equívoco errado (mas comum):

O comportamento indefinido é uma propriedade de tempo de execução *. Ele NÃO PODE "viagem no tempo"!

Certas operações são definidas (pelo padrão) para ter efeitos colaterais e não podem ser otimizadas. As operações que fazem E / S ou que acessam volatilevariáveis ​​se enquadram nesta categoria.

No entanto , há uma advertência: UB pode ser qualquer comportamento, incluindo comportamento que desfaça operações anteriores. Isso pode ter consequências semelhantes, em alguns casos, à otimização do código anterior.

Na verdade, isso é consistente com a citação na resposta principal (ênfase minha):

Uma implementação conforme executando um programa bem formado deve produzir o mesmo comportamento observável que uma das execuções possíveis da instância correspondente da máquina abstrata com o mesmo programa e a mesma entrada.
No entanto, se qualquer uma dessas execuções contiver uma operação indefinida, esta Norma Internacional não impõe nenhum requisito à implementação que executa esse programa com aquela entrada (nem mesmo em relação às operações anteriores à primeira operação indefinida).

Sim, esta citação faz dizer "nem mesmo no que diz respeito a operações anteriores à primeira operação indefinido" , mas aviso que este é especificamente sobre o código que está sendo executado , não apenas compilado.
Afinal, o comportamento indefinido que não é realmente alcançado não faz nada, e para que a linha que contém o UB seja realmente alcançada, o código que o precede deve ser executado primeiro!

Então, sim, uma vez que o UB é executado , quaisquer efeitos das operações anteriores se tornam indefinidos. Mas até que isso aconteça, a execução do programa está bem definida.

Observe, no entanto, que todas as execuções do programa que resultam nisso podem ser otimizadas para programas equivalentes , incluindo qualquer um que execute operações anteriores, mas depois desfaça seus efeitos. Consequentemente, o código anterior pode ser otimizado sempre que isso for equivalente a seus efeitos serem desfeitos ; caso contrário, não pode. Veja abaixo um exemplo.

* Nota: Isso não é inconsistente com o UB ocorrendo em tempo de compilação . Se o compilador pode realmente provar que o código UB vai sempre ser executadas para todas as entradas, então UB pode estender-se a tempo de compilação. No entanto, isso requer saber que todo o código anterior eventualmente retorna , o que é um requisito forte. Novamente, veja abaixo um exemplo / explicação.


Para tornar isso concreto, observe que o código a seguir deve ser impresso fooe aguardar sua entrada, independentemente de qualquer comportamento indefinido que o segue:

printf("foo");
getchar();
*(char*)1 = 1;

No entanto, também observe que não há garantia de que foopermanecerá na tela após a ocorrência do UB, ou que o caractere digitado não estará mais no buffer de entrada; ambas as operações podem ser "desfeitas", o que tem um efeito semelhante à "viagem no tempo" do UB.

Se a getchar()linha não estivesse lá, seria legal que as linhas fossem otimizadas se e somente se isso fosse indistinguível da saída fooe, em seguida, "desfazendo".

Se os dois seriam indistinguíveis ou não, dependeria inteiramente da implementação (ou seja, do seu compilador e da biblioteca padrão). Por exemplo, você pode printf bloquear seu thread aqui enquanto espera que outro programa leia a saída? Ou vai voltar imediatamente?

  • Se ele pode bloquear aqui, então outro programa pode se recusar a ler sua saída completa e pode nunca retornar e, conseqüentemente, UB pode nunca realmente ocorrer.

  • Se ele pode retornar imediatamente aqui, então sabemos que ele deve retornar e, portanto, otimizá-lo é totalmente indistinguível de executá-lo e desfazer seus efeitos.

Obviamente, como o compilador sabe qual comportamento é permitido para sua versão específica do printf, ele pode otimizar de acordo e, conseqüentemente, printfpode ser otimizado em alguns casos e não em outros. Mas, novamente, a justificativa é que isso seria indistinguível das operações anteriores de desfazer do UB, não que o código anterior esteja "envenenado" por causa do UB.


1
Você está interpretando mal o padrão. Diz que o comportamento ao executar o programa é indefinido. Período. Esta resposta está 100% errada. O padrão é muito claro - a execução de um programa com entrada que produz UB em qualquer ponto do fluxo ingênuo de execução é indefinida.
David Schwartz,

@DavidSchwartz: Se você seguir sua interpretação até as conclusões lógicas, deve perceber que não faz sentido lógico. A entrada não é algo totalmente determinado quando o programa é iniciado. A entrada para o programa (mesmo sua mera presença ) em qualquer linha pode depender de todos os efeitos colaterais do programa até aquela linha. Portanto, o programa não pode evitar a produção de efeitos colaterais que vêm antes da linha UB, porque isso requer interação com seu ambiente e, portanto, afeta se a linha UB será alcançada ou não em primeiro lugar.
user541686,

3
Isso não importa. Realmente. Novamente, você só não tem imaginação. Por exemplo, se o compilador pode dizer que nenhum código compatível poderia dizer a diferença, ele poderia mover o código que é UB de forma que a parte que é UB execute antes das saídas que você ingenuamente espera estar "precedendo".
David Schwartz,

2
@Mehrdad: Talvez um meio melhor de dizer as coisas seria dizer que o UB não pode viajar no tempo, passando do último ponto onde algo poderia ter acontecido no mundo real que teria definido o comportamento. Se uma implementação pudesse determinar, examinando os buffers de entrada, que nenhuma das próximas 1000 chamadas para getchar () poderia bloquear, e também pudesse determinar que UB ocorreria após a milésima chamada, não seria necessário realizar nenhum dos as chamadas. Se, no entanto, uma implementação especificar que a execução não passará um getchar () até que toda a saída anterior tenha ...
supercat

2
... foi entregue a um terminal de 300 baud, e que qualquer controle-C que ocorra antes disso fará com que getchar () eleve um sinal, mesmo se houver outros caracteres no buffer precedendo-o, então tal implementação não poderia mova qualquer UB além da última saída anterior a getchar (). O que é complicado é saber em que caso se espera que um compilador passe pelo programador quaisquer garantias comportamentais que uma implementação de biblioteca possa oferecer além daquelas exigidas pelo Padrão.
supercat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.