O “Comportamento Indefinido” realmente permite que * qualquer coisa * aconteça?

EDIT: Esta questão não foi concebida como um fórum para discussão sobre o (des) mérito do comportamento indefinido, mas isso é uma espécie de o que se tornou. Em qualquer caso, este tópico sobre um compilador C hipotético sem comportamento indefinido pode ser de interesse adicional para aqueles que acham que este é um tópico importante.


O clássico exemplo apócrifo de “comportamento indefinido” é, naturalmente, “demônios nasais” – uma impossibilidade física, independentemente do que os padrões C e C ++ permitem.

Como as comunidades C e C ++ tendem a enfatizar a imprevisibilidade do comportamento indefinido e a idéia de que o compilador tem permissão para fazer com que o programa faça literalmente qualquer coisa quando um comportamento indefinido é encontrado, eu assumi que o padrão não impõe nenhuma restrição. sobre o comportamento de, bem, comportamento indefinido.

Mas a citação relevante no padrão C ++ parece ser :

[C++14: defns.undefined]: [..] O comportamento indefinido permissível varia de ignorar a situação completamente com resultados imprevisíveis, comportar-se durante a tradução ou execução do programa de uma maneira documentada característica do ambiente (com ou sem a emissão de uma mensagem de diagnóstico), para terminar uma tradução ou execução (com a emissão de uma mensagem de diagnóstico). [..]

Isso realmente especifica um pequeno conjunto de opções possíveis:

  • Ignorando a situação – Sim, o padrão diz que isso terá “resultados imprevisíveis”, mas isso não é o mesmo que o código de inserção do compilador (que, presumo, seria um pré-requisito para, você sabe, demônios nasais).
  • Comportamento de uma maneira documentada característica do ambiente – isso realmente parece relativamente benigno. (Eu certamente não ouvi falar de nenhum caso documentado de demônios nasais).
  • Terminar a tradução ou execução – com um diagnóstico, não menos. Será que todo o UB se comportaria tão bem?

Eu suponho que, na maioria dos casos, os compiladores optam por ignorar o comportamento indefinido; por exemplo, ao ler memory não inicializada, seria presumivelmente uma anti-otimização para inserir qualquer código para garantir um comportamento consistente. Eu suponho que os tipos estranhos de comportamento indefinido (como ” viagem no tempo “) cairiam na segunda categoria – mas isso requer que tais comportamentos sejam documentados e “característicos do ambiente” (então eu acho que os demônios nasais são produzidos apenas por computadores infernais?).

Eu estou entendendo mal a definição? Estes pretendem ser meros exemplos do que poderia constituir um comportamento indefinido, em vez de uma lista abrangente de opções? A afirmação de que “tudo pode acontecer” significava apenas um efeito colateral inesperado de ignorar a situação?

EDIT: dois pequenos pontos de esclarecimento:

  • Eu pensei que estava claro a partir da pergunta original, e eu acho que para a maioria das pessoas era, mas eu vou soletrar de qualquer maneira: eu percebo que “demônios nasais” é irônico.
  • Por favor, não escreva uma (outra) resposta explicando que o UB permite otimizações de compilador específicas da plataforma, a menos que você também explique como ele permite as otimizações que o comportamento definido pela implementação não permitiria.

    Sim, permite que qualquer coisa aconteça. A nota está apenas dando exemplos. A definição é bem clara:

    Comportamento não definido: comportamento para o qual esta Norma não impõe requisitos.

    Um dos propósitos históricos do Comportamento Indefinido foi permitir a possibilidade de que certas ações possam ter diferentes efeitos potencialmente úteis em diferentes plataformas. Por exemplo, nos primeiros dias de C, dado

     int i=INT_MAX; i++; printf("%d",i); 

    alguns compiladores poderiam garantir que o código imprimiria algum valor em particular (para uma máquina de complemento de dois seria tipicamente INT_MIN), enquanto outros garantiriam que o programa terminaria sem atingir o printf. Dependendo dos requisitos da aplicação, qualquer comportamento pode ser útil. Deixar o comportamento indefinido significava que um aplicativo em que a finalização anormal do programa era uma consequência aceitável do estouro, mas produzindo uma saída aparentemente válida, mas errada, poderia dispensar a verificação de estouro se fosse executado em uma plataforma que pudesse capturá-lo com segurança e um aplicativo onde a terminação anormal em caso de estouro não seria aceitável, mas produzir saída aritmeticamente incorreta seria, poderia renunciar à verificação de estouro se fosse executada em uma plataforma onde os transbordamentos não estivessem presos.

    Recentemente, no entanto, alguns autores de compiladores parecem ter participado de um concurso para ver quem consegue eliminar mais eficientemente qualquer código cuja existência não seja obrigatória pelo padrão. Dado, por exemplo …

     #include  int main(void) { int ch = getchar(); if (ch < 74) printf("Hey there!"); else printf("%d",ch*ch*ch*ch*ch); } 

    um compilador hipermoderno pode concluir que, se ch for 74 ou maior, o cálculo de ch*ch*ch*ch*ch produziria Undefined Behavior e, como conseqüência, o programa deve imprimir "Hey there!" incondicionalmente, independentemente de qual caractere foi typescript.

    Nitpicking : Você não citou um padrão.

    Estas são as fonts usadas para gerar rascunhos do padrão C ++. Essas fonts não devem ser consideradas como uma publicação ISO, nem documentos gerados a partir delas, a menos que adotadas oficialmente pelo grupo de trabalho C ++ (ISO / IEC JTC1 / SC22 / WG21).

    Interpretação : As notas não são normativas de acordo com as Diretrizes da ISO / IEC Parte 2.

    Notas e exemplos integrados no texto de um documento só devem ser usados ​​para fornecer informações adicionais destinadas a auxiliar o entendimento ou uso do documento. Não devem conter requisitos (“deve”; ver 3.3.1 e Tabela H.1) ou qualquer informação considerada indispensável para o uso do documento, por exemplo, instruções (imperativo; ver Tabela H.1), recomendações (“deve”; ver 3.3.2 e Tabela H.2) ou permissão (“pode”; veja a Tabela H.3). As notas podem ser escritas como uma declaração de fato.

    Ênfase minha. Só isso exclui “lista abrangente de opções”. Dar exemplos, no entanto, conta como “informações adicionais destinadas a auxiliar o entendimento … do documento”.

    Tenha em mente que o meme “demônio nasal” não deve ser tomado literalmente, da mesma forma que usar um balão para explicar como as obras de expansão do universo não contêm nenhuma verdade na realidade física. É para ilustrar que é imprudente discutir o que “comportamento indefinido” deve fazer quando é permitido fazer qualquer coisa. Sim, isso significa que não existe um elástico real no espaço exterior.

    A definição de comportamento indefinido, em todos os padrões C e C ++, é essencialmente que o padrão não impõe requisitos sobre o que acontece.

    Sim, isso significa que qualquer resultado é permitido. Mas não há resultados específicos que são necessários para acontecer, nem quaisquer resultados que são necessários para não acontecer. Não importa se você tem um compilador e uma biblioteca que produz consistentemente um comportamento particular em resposta a uma instância particular de comportamento indefinido – tal comportamento não é necessário, e pode mudar mesmo em uma versão futura do compilador – e o compilador ainda estará perfeitamente correto de acordo com cada versão dos padrões C e C ++.

    Se o seu sistema host tiver suporte de hardware na forma de conexão a sondas inseridas em suas narinas, é possível que uma ocorrência de comportamento indefinido cause efeitos nasais indesejados.

    Eu pensei em responder apenas um de seus pontos, já que as outras respostas respondem bem à pergunta geral, mas deixaram isso sem resposta.

    “Ignorando a situação – Sim, o padrão diz que isso terá” resultados imprevisíveis “, mas isso não é o mesmo que o código de inserção do compilador (que eu assumo seria um pré-requisito para, você sabe, demônios nasais). ”

    Uma situação em que demônios nasais poderiam muito razoavelmente esperar que ocorresse com um compilador sensato, sem o compilador inserir qualquer código, seria o seguinte:

     if(!spawn_of_satan) printf("Random debug value: %i\n", *x); // oops, null pointer deference nasal_angels(); else nasal_demons(); 

    Um compilador, se puder provar que aquele * x é um desreferencia de ponteiro nulo, tem o direito perfeito, como parte de alguma otimização, de dizer “OK, então eu vejo que eles desreferenciaram um ponteiro nulo neste ramo do if. Portanto, como parte desse ramo, tenho permissão para fazer qualquer coisa. Portanto, posso otimizar isso: ”

     if(!spawn_of_satan) nasal_demons(); else nasal_demons(); 

    “E a partir daí, posso otimizar isso:”

     nasal_demons(); 

    Você pode ver como esse tipo de coisa pode, nas circunstâncias certas, ser muito útil para um compilador otimizador e, ainda assim, causar um desastre. Eu vi alguns exemplos um tempo atrás dos casos em que, na verdade, é importante para a otimização otimizar esse tipo de caso. Eu poderia tentar desenterrá-los mais tarde, quando tiver mais tempo.

    EDIT: Um exemplo que só veio das profundezas da minha memory de um caso em que é útil para otimização é onde você freqüentemente verificar um ponteiro para ser NULL (talvez em funções auxiliares inlined), mesmo depois de já ter sido dereferenced e sem ter mudou isso. O compilador otimizador pode ver que você o desrefere e, assim, otimizar todas as verificações “is NULL”, já que se você o tiver desreferenciado e IS null, tudo poderá acontecer, incluindo simplesmente não executar o “is NULL” Verificações. Eu acredito que argumentos semelhantes se aplicam a outro comportamento indefinido.

    Primeiro, é importante notar que não é apenas o comportamento do programa do usuário que é indefinido, é o comportamento do compilador que é indefinido . Da mesma forma, o UB não é encontrado em tempo de execução, é uma propriedade do código-fonte.

    Para um compilador, “o comportamento é indefinido” significa “você não precisa levar em conta essa situação”, ou mesmo “você pode assumir que nenhum código-fonte produzirá esta situação”. Um compilador pode fazer qualquer coisa, intencionalmente ou não, quando apresentado com o UB, e ainda ser compatível com o padrão, então sim, se você concedeu access ao seu nariz …

    Então, nem sempre é possível saber se um programa tem UB ou não. Exemplo:

     int * ptr = calculateAddress(); int i = *ptr; 

    Saber se isso pode ser UB ou não exigiria conhecer todos os valores possíveis retornados por calculateAddress() , o que é impossível no caso geral (consulte ” Problema de parada “). Um compilador tem duas opções:

    • Suponha que ptr sempre tenha um endereço válido
    • insira verificações de tempo de execução para garantir um determinado comportamento

    A primeira opção produz programas rápidos, e coloca o ônus de evitar efeitos indesejados no programador, enquanto a segunda opção produz um código mais seguro, porém mais lento.

    Os padrões C e C ++ deixam essa opção em aberto, e a maioria dos compiladores escolhe o primeiro, enquanto o Java, por exemplo, determina o segundo.


    Por que o comportamento não é definido pela implementação, mas é indefinido?

    Meios definidos pela implementação ( N4296 , 1.9§2):

    Certos aspectos e operações da máquina abstrata são descritos nesta Norma como definida pela implementação (por exemplo, sizeof (int)). Estes constituem os parâmetros da máquina abstrata. Cada implementação deve include documentação descrevendo suas características e comportamento nesses aspectos. Essa documentação deve definir a instância da máquina abstrata que corresponde a essa implementação (referida como “instância correspondente” abaixo).

    Ênfase minha. Em outras palavras: Um compilador-escritor tem que documentar exatamente como o código da máquina se comporta, quando o código-fonte usa resources definidos pela implementação.

    Escrever em um ponteiro inválido não nulo random é uma das coisas mais imprevisíveis que você pode fazer em um programa, portanto, isso também exigiria verificações de tempo de execução com redução de desempenho.
    Antes que tivéssemos MMUs, você poderia destruir o hardware escrevendo para o endereço errado, que fica bem próximo dos demônios nasais 😉

    Uma das razões para deixar o comportamento indefinido é permitir que o compilador faça as suposições que desejar ao otimizar.

    Se houver alguma condição que deve ser mantida se uma otimização for aplicada e essa condição depender de um comportamento indefinido no código, o compilador poderá presumir que ela é atendida, pois um programa em conformidade não pode depender de um comportamento indefinido em qualquer caminho. Importante, o compilador não precisa ser consistente nessas suposições. (o que não é o caso para o comportamento definido pela implementação)

    Então suponha que seu código contenha um exemplo reconhecidamente planejado como o abaixo:

     int bar = 0; int foo = (undefined behavior of some kind); if (foo) { f(); bar = 1; } if (!foo) { g(); bar = 1; } assert(1 == bar); 

    O compilador é livre para assumir que! Foo é verdadeiro no primeiro bloco e foo é verdadeiro no segundo, e assim otimizar todo o pedaço de código de distância. Agora, logicamente foo ou foo devem ser verdadeiros, e assim, olhando para o código, você seria capaz de assumir que a barra deve ser igual a 1 depois de executar o código. Mas como o compilador é otimizado dessa maneira, o bar nunca é definido como 1. E agora essa afirmação se torna falsa e o programa termina, comportamento que não teria acontecido se foo não tivesse se baseado em comportamento indefinido.

    Agora, é possível que o compilador insira um código completamente novo se ele vir um comportamento indefinido? Se isso permitir, otimize mais, absolutamente. É provável que isso aconteça com frequência? Provavelmente não, mas você nunca pode garantir isso, portanto, operar com a suposição de que os demônios nasais são possíveis é a única abordagem segura.

    Comportamento indefinido é simplesmente o resultado de uma situação que os autores da especificação não previram.

    Tome a ideia de um semáforo. Vermelho significa parar, amarelo significa preparar para o vermelho e verde significa ir. Neste exemplo, pessoas dirigindo carros são a implementação da especificação.

    O que acontece se tanto o verde quanto o vermelho estiverem ligados? Você pára e depois vai? Você espera até que o vermelho apague e fique verde? Este é um caso que a especificação não descreveu e, como resultado, qualquer coisa que os drivers façam é um comportamento indefinido. Algumas pessoas farão uma coisa, outra alguma. Como não há garantia sobre o que acontecerá, você quer evitar essa situação. O mesmo se aplica ao código.

    Comportamentos indefinidos permitem que os compiladores gerem códigos mais rápidos em alguns casos. Considere duas arquiteturas de processador diferentes que ADICIONAM diferentemente: O processador A descarta inerentemente o bit de transporte após o estouro, enquanto o processador B gera um erro. (É claro que o Processador C inerentemente gera Demônios Nasais – é apenas a maneira mais fácil de descarregar essa energia extra em um nanobot movido a snot …)

    Se o padrão exigisse que um erro fosse gerado, então todo o código compilado para o processador A seria basicamente forçado a include instruções adicionais, para executar algum tipo de verificação de estouro e, em caso afirmativo, gerar um erro. Isso resultaria em código mais lento, mesmo que o desenvolvedor saiba que eles acabariam adicionando números pequenos.

    Comportamento indefinido sacrifica a portabilidade para velocidade. Ao permitir que ‘qualquer coisa’ aconteça, o compilador pode evitar escrever verificações de segurança para situações que nunca ocorrerão. (Ou, você sabe … eles podem.)

    Além disso, quando um programador sabe exatamente o que um comportamento indefinido causará em seu ambiente, eles estão livres para explorar esse conhecimento para obter desempenho adicional.

    Se você quiser garantir que seu código se comporte exatamente da mesma maneira em todas as plataformas, é necessário garantir que nenhum “comportamento indefinido” ocorra, no entanto, isso pode não ser o seu objective.

    Edit: (Em resposta a OPs editar) Implementação Comportamento definido exigiria a geração consistente de demônios nasais. O comportamento indefinido permite a geração esporádica de demônios nasais.

    É aí que a vantagem que o comportamento indefinido tem sobre o comportamento específico da implementação é exibida. Considere que um código extra pode ser necessário para evitar um comportamento inconsistente em um sistema específico. Nestes casos, o comportamento indefinido permite maior velocidade.