Filosofia por trás do comportamento indefinido

59

As especificações C \ C ++ deixam de fora um grande número de comportamentos em aberto para os compiladores implementarem de sua própria maneira. Há várias perguntas que sempre são feitas aqui sobre o mesmo e temos excelentes postagens sobre isso:

Minha pergunta não é sobre o que é um comportamento indefinido ou é realmente ruim. Conheço os perigos e a maioria das citações de comportamento indefinidas relevantes do padrão. Portanto, evite postar respostas sobre quão ruim é. Esta pergunta é sobre a filosofia por trás de deixar de fora tantos comportamentos em aberto para a implementação do compilador.

Eu li um excelente post no blog que afirma que o desempenho é a principal razão. Fiquei me perguntando se o desempenho é o único critério para permitir isso, ou existem outros fatores que influenciam a decisão de deixar as coisas em aberto para a implementação do compilador?

Se você tiver exemplos para citar sobre como um comportamento indefinido específico oferece espaço suficiente para otimização do compilador, liste-os. Se você souber de outros fatores que não sejam o desempenho, retorne sua resposta com detalhes suficientes.

Se você não entender a pergunta ou não tiver evidências / fontes suficientes para apoiar sua resposta, não poste respostas especulativas gerais.

Alok Save
fonte
7
quem já ouviu falar de um computador determinístico?
sova
11
como indica a excelente resposta de litb programmers.stackexchange.com/a/99741/192238 , o título e o corpo desta pergunta parecem um pouco incompatíveis: "comportamentos abertos para compiladores implementarem de sua própria maneira" são geralmente referidos como definidos pela implementação . com certeza, o UB real pode ser definido pelo autor da implementação, mas, na maioria das vezes, eles não se incomodam (e otimizam tudo, etc.) #
underscore_d

Respostas:

49

Primeiro, observarei que, embora apenas mencione "C" aqui, o mesmo se aplica igualmente ao C ++.

O comentário mencionando Godel estava parcialmente (mas apenas parcialmente) em questão.

Quando você começar a ele, um comportamento indefinido nas normas C é em grande parte apenas apontando a fronteira entre o que as tentativas padrão para definir, e que isso não acontece.

Os teoremas de Godel (existem dois) dizem basicamente que é impossível definir um sistema matemático que possa ser provado (por suas próprias regras) completo e consistente. Você pode fazer suas regras para que sejam completas (o caso com o qual ele lidou foram as regras "normais" para números naturais), ou então você pode tornar possível provar sua consistência, mas não pode ter as duas.

No caso de algo como C, isso não se aplica diretamente - na maioria das vezes, a "provabilidade" da integridade ou consistência do sistema não é uma alta prioridade para a maioria dos designers de idiomas. Ao mesmo tempo, sim, eles provavelmente foram influenciados (pelo menos até certo ponto) por saber que é comprovadamente impossível definir um sistema "perfeito" - que seja comprovadamente completo e consistente. Saber que tal coisa é impossível pode ter tornado um pouco mais fácil dar um passo para trás, respirar um pouco e decidir sobre os limites do que eles tentariam definir.

Correndo o risco de (mais uma vez) ser acusado de arrogância, eu caracterizaria o padrão C como sendo governado (em parte) por duas idéias básicas:

  1. O idioma deve suportar a maior variedade de hardware possível (idealmente, todo o hardware "sadio" até um limite inferior razoável).
  2. O idioma deve oferecer suporte à escrita da maior variedade de software possível para o ambiente especificado.

O primeiro significa que, se alguém definir uma nova CPU, deve ser possível fornecer uma implementação boa, sólida e utilizável de C para isso, desde que o design fique pelo menos razoavelmente próximo de algumas diretrizes simples - basicamente, se segue algo na ordem geral do modelo de Von Neumann e fornece pelo menos uma quantidade mínima razoável de memória, que deve ser suficiente para permitir uma implementação em C. Para uma implementação "hospedada" (que é executada em um sistema operacional), é necessário oferecer suporte a alguma noção que corresponda razoavelmente aos arquivos e ter um conjunto de caracteres com um determinado conjunto mínimo de caracteres (91 são necessários).

O segundo significa que deve ser possível escrever um código que manipule o hardware diretamente, para que você possa escrever coisas como gerenciadores de inicialização, sistemas operacionais, software incorporado que é executado sem nenhum SO, etc. Existem alguns limites nesse sentido, quase todos os sistema operacional prático, carregador de inicialização, etc., provavelmente conterá pelo menos um pouco de código escrito em linguagem assembly. Da mesma forma, mesmo um pequeno sistema incorporado provavelmente incluirá pelo menos algum tipo de rotina de biblioteca pré-escrita para dar acesso aos dispositivos no sistema host. Embora seja difícil definir um limite preciso, a intenção é que a dependência desse código seja mantida no mínimo.

O comportamento indefinido no idioma é amplamente motivado pela intenção do idioma de oferecer suporte a esses recursos. Por exemplo, o idioma permite converter um número inteiro arbitrário em um ponteiro e acessar o que quer que esteja nesse endereço. O padrão não tenta dizer o que acontecerá quando você o fizer (por exemplo, mesmo a leitura de alguns endereços pode ter efeitos visíveis externamente). Ao mesmo tempo, não faz nenhuma tentativa de impedi-lo de fazer essas coisas, porque você precisa de alguns tipos de software que deveria poder escrever em C.

Há algum comportamento indefinido direcionado por outros elementos de design. Por exemplo, uma outra intenção de C é oferecer suporte à compilação separada. Isso significa (por exemplo) que se pretende "vincular" partes usando um vinculador que segue aproximadamente o que a maioria de nós vê como o modelo usual de um vinculador. Em particular, deve ser possível combinar módulos compilados separadamente em um programa completo sem o conhecimento da semântica da linguagem.

Há outro tipo de comportamento indefinido (que é muito mais comum em C ++ que C), que está presente simplesmente devido aos limites da tecnologia do compilador - coisas que basicamente sabemos que são erros e que provavelmente o compilador deve diagnosticar como erros, mas, considerando os limites atuais da tecnologia do compilador, é duvidoso que eles possam ser diagnosticados sob todas as circunstâncias. Muitos desses fatores são motivados por outros requisitos, como compilação separada, portanto é uma questão de equilibrar requisitos conflitantes; nesse caso, o comitê geralmente optou por oferecer suporte a recursos maiores, mesmo que isso signifique falta de diagnóstico de possíveis problemas, em vez de limitar os recursos para garantir que todos os problemas possíveis sejam diagnosticados.

Essas diferenças de intenção conduzem a maioria das diferenças entre C e algo como Java ou sistemas baseados em CLI da Microsoft. Estes últimos são explicitamente limitados a trabalhar com um conjunto de hardware muito mais limitado ou exigir que o software imite o hardware mais específico que eles almejam. Eles também pretendem especificamente impedir qualquer manipulação direta de hardware, exigindo que você use algo como JNI ou P / Invoke (e código escrito em algo como C) para fazer essa tentativa.

Voltando aos teoremas de Godel por um momento, podemos traçar um paralelo: Java e CLI optaram pela alternativa "internamente consistente", enquanto C optou pela alternativa "completa". Claro, isso é uma analogia muito grosseira - Eu duvido que alguém está tentando uma prova formal de qualquer consistência interna ou completude em ambos os casos. No entanto, a noção geral se encaixa bastante de perto com as escolhas que fizeram.

Jerry Coffin
fonte
25
Eu acho que os Teoremas de Godel são um arenque vermelho. Eles lidam com a prova de um sistema a partir de seus próprios axiomas, o que não é o caso aqui: C não precisa ser especificado em C. É bem possível ter uma linguagem completamente especificada (considere uma máquina de Turing).
poolieby
9
Desculpe, mas temo que você tenha entendido completamente mal os Teoremas de Godel. Eles lidam com a impossibilidade de provar todas as afirmações verdadeiras em um sistema consistente de lógica; em termos de computação, o teorema da incompletude é análogo a dizer que existem problemas que não podem ser resolvidos por nenhum programa - problemas são análogos a declarações verdadeiras, programas a provas e o modelo de computação ao sistema lógico. Não tem nenhuma conexão com o comportamento indefinido. Veja uma explicação da analogia aqui: scottaaronson.com/blog/?p=710 .
Alex10 Brink
5
Devo observar que uma máquina Von Neumann não é necessária para uma implementação em C. É perfeitamente possível (e nem mesmo muito difícil) para desenvolver uma implementação C para uma arquitetura de Harvard (e eu não ficaria surpreso de ver um monte de tais implementações em sistemas embarcados)
bdonlan
11
Infelizmente, a filosofia moderna do compilador C leva o UB a um nível totalmente novo. Mesmo nos casos em que um programa foi preparado para lidar com quase todas as conseqüências "naturais" plausíveis de uma forma específica de Comportamento Indefinido, e aquelas com as quais não era possível lidar pelo menos seriam reconhecíveis (por exemplo, excesso de números inteiros presos), a nova filosofia favorece ignorando qualquer código que não pudesse ser executado a menos que o UB ocorresse, transformando o código que teria se comportado corretamente em qualquer implementação em código que é "mais eficiente", mas simplesmente errado.
Supercat
20

A lógica C explica

Os termos comportamento não especificado, comportamento indefinido e comportamento definido pela implementação são usados ​​para categorizar o resultado da gravação de programas cujas propriedades o Padrão não descreve, ou não pode, descrever completamente. O objetivo de adotar essa categorização é permitir que uma certa variedade de implementações permita que a qualidade da implementação seja uma força ativa no mercado, bem como permitir certas extensões populares , sem remover o cachê de conformidade com a Norma. O Apêndice F da Norma cataloga os comportamentos que se enquadram em uma dessas três categorias.

O comportamento não especificado dá ao implementador alguma latitude na tradução de programas. Essa latitude não se estende até a falha na conversão do programa.

O comportamento indefinido dá ao implementador licença para não detectar certos erros de programa difíceis de diagnosticar. Também identifica áreas de possível extensão de idioma em conformidade: o implementador pode aumentar o idioma fornecendo uma definição do comportamento oficialmente indefinido.

O comportamento definido pela implementação dá ao implementador a liberdade de escolher a abordagem apropriada, mas requer que essa escolha seja explicada ao usuário. Os comportamentos designados como definidos pela implementação geralmente são aqueles em que um usuário pode tomar decisões significativas de codificação com base na definição da implementação. Os implementadores devem ter em mente esse critério ao decidir a extensão de uma definição de implementação. Como no comportamento não especificado, simplesmente falhar na conversão da fonte que contém o comportamento definido pela implementação não é uma resposta adequada.

Importante também é o benefício para os programas, não apenas o benefício para implementações. Um programa que depende de comportamento indefinido ainda pode estar em conformidade , se for aceito por uma implementação em conformidade. A existência de comportamento indefinido permite que um programa use recursos não portáteis explicitamente marcados como tal ("comportamento indefinido"), sem se tornar não conforme. A justificativa observa:

O código C pode não ser portátil. Embora se esforçasse para dar aos programadores a oportunidade de escrever programas verdadeiramente portáteis, o Comitê não queria forçar os programadores a escrever de maneira portável, para impedir o uso de C como um `` assembler de alto nível '': a capacidade de escrever específicos para máquinas o código é um dos pontos fortes de C. É esse princípio que motiva amplamente a distinção entre programa estritamente conforme e programa conforme (§1.7).

E em 1.7 nota

A definição tripla de conformidade é usada para ampliar a população de programas em conformidade e distinguir entre programas em conformidade usando uma única implementação e programas em conformidade portáteis.

Um programa estritamente conforme é outro termo para um programa maximamente portátil. O objetivo é dar ao programador a chance de criar programas C poderosos que também são altamente portáteis, sem degradar programas C perfeitamente úteis que, por acaso, não são portáteis. Assim, o advérbio estritamente.

Portanto, este pequeno programa sujo que funciona perfeitamente no GCC ainda está em conformidade !

Johannes Schaub - litb
fonte
15

A questão da velocidade é especialmente um problema quando comparado ao C. Se o C ++ fizesse algumas coisas que poderiam ser sensatas, como inicializar grandes matrizes de tipos primitivos, perderia uma tonelada de benchmarks no código C. Portanto, o C ++ inicializa seus próprios tipos de dados, mas deixa os tipos C do jeito que eram.

Outro comportamento indefinido apenas reflete a realidade. Um exemplo é a troca de bits com uma contagem maior que o tipo. Isso realmente difere entre as gerações de hardware da mesma família. Se você tem um aplicativo de 16 bits, o mesmo binário exato fornecerá resultados diferentes em um 80286 e um 80386. Portanto, o padrão da linguagem diz que não sabemos!

Algumas coisas são mantidas como eram, como a ordem de avaliação das subexpressões não especificada. Originalmente, acreditava-se que isso ajudasse os escritores do compilador a otimizar melhor. Atualmente, os compiladores são bons o suficiente para descobrir isso de qualquer maneira, mas o custo de encontrar todos os locais nos compiladores existentes que tiram vantagem da liberdade é muito alto.

Bo Persson
fonte
+1 no segundo parágrafo, que mostra algo que seria estranho especificar como comportamento definido pela implementação.
David Thornley
3
A mudança de bits é apenas um exemplo de aceitação do comportamento indefinido do compilador e do uso dos recursos de hardware. Seria trivial especificar um resultado C para uma mudança de bit quando a contagem for maior que o tipo, mas dispendiosa de implementar em algum hardware.
mattnz
7

Como exemplo, os acessos a ponteiros quase precisam ser indefinidos e não necessariamente apenas por razões de desempenho. Por exemplo, em alguns sistemas, carregar registros específicos com um ponteiro gerará uma exceção de hardware. No acesso SPARC a um objeto de memória alinhado incorretamente, ocorrerá um erro de barramento, mas no x86 seria "apenas" lento. É difícil especificar o comportamento nesses casos, já que o hardware subjacente determina o que acontecerá, e o C ++ é portátil para muitos tipos de hardware.

É claro que também oferece ao compilador a liberdade de usar conhecimentos específicos da arquitetura. Para um exemplo de comportamento não especificado, o deslocamento à direita dos valores assinados pode ser lógico ou aritmético, dependendo do hardware subjacente, para permitir o uso de qualquer operação de turno disponível e não forçar a emulação de software.

Acredito também que isso facilita bastante o trabalho do compilador-escritor, mas não me lembro do exemplo agora. Vou adicioná-lo se me lembrar da situação.

Mark B
fonte
3
O idioma C poderia ter sido especificado de tal forma que ele sempre tivesse que usar leituras de byte a byte em sistemas com restrições de alinhamento, e de fornecer traps de exceção com comportamento bem definido para acessos de endereço inválidos. Mas é claro que tudo isso teria sido incrivelmente caro (em tamanho, complexidade e desempenho do código) e não ofereceria nenhum benefício ao código correto e correto.
R ..
6

Simples: velocidade e portabilidade. Se o C ++ garantisse que você recebeu uma exceção ao cancelar a referência de um ponteiro inválido, ele não seria portátil para o hardware incorporado. Se o C ++ garantisse algumas outras coisas, como sempre as primitivas sempre inicializadas, seria mais lento e, na época da origem do C ++, mais lento seria uma coisa muito, muito ruim.

DeadMG
fonte
11
Hã? O que as exceções têm a ver com o hardware incorporado?
Mason Wheeler
2
Exceções podem bloquear o sistema de maneiras muito ruins para os sistemas embarcados que precisam responder rapidamente. Há situações em que uma leitura falsa é muito menos prejudicial que um sistema lento.
World Engineer
11
@Mason: Porque o hardware precisa capturar o acesso inválido. É fácil para o Windows lançar uma violação de acesso e mais difícil para o hardware incorporado sem sistema operacional fazer nada além de morrer.
DeadMG
3
Lembre-se também de que nem toda CPU possui uma MMU para se proteger contra acessos inválidos no hardware, para começar. Se você começar a exigir que seu idioma verifique todos os acessos a ponteiros, será necessário emular uma MMU em CPUs sem uma - e, assim, TODOS os acessos à memória se tornam extremamente caros.
fofo
4

C foi inventado em uma máquina com bytes de 9 bits e sem unidade de ponto flutuante - suponha que ele exigisse que bytes fossem 9 bits, palavras 18 bits e que os flutuadores fossem implementados usando a aritmética anterior à IEEE754?

Martin Beckett
fonte
5
Eu suspeito que você esteja pensando em Unix - C foi originalmente usado no PDP-11, que na verdade eram padrões atuais bastante convencionais. Eu acho que a idéia básica permanece, no entanto.
Jerry Coffin
@ Jerry - sim, você está certo - eu estou ficando velho!
Martin Beckett
Sim - acontece com o melhor de nós, eu tenho medo.
Jerry Coffin
4

Eu não acho que a primeira justificativa para o UB foi deixar espaço para o compilador otimizar, mas apenas a possibilidade de usar a implementação óbvia para os destinos em um momento em que as arquiteturas tinham mais variedade do que agora (lembre-se se C foi projetado em um PDP-11, que tem uma arquitetura um tanto familiar, a primeira porta foi para a Honeywell 635, que é muito menos familiar - endereçável por palavras, usando palavras de 36 bits, 6 ou 9 bits, endereços de 18 bits ... bem, pelo menos, usou 2's complemento). Mas se a otimização pesada não era um destino, a implementação óbvia não inclui a adição de verificações em tempo de execução para estouros, a contagem de turnos sobre o tamanho do registro, que é um alias nas expressões que modificam vários valores.

Outra coisa levada em consideração foi a facilidade de implementação. O compilador de CA, na época, era várias passagens usando vários processos, porque ter um processo gerenciando tudo não teria sido possível (o programa teria sido muito grande). Solicitar uma verificação de coerência pesada estava fora do caminho - especialmente quando envolvia várias UC. (Outro programa que não os compiladores C, o lint, foi usado para isso).

AProgrammer
fonte
Eu me pergunto o que levou a filosofia de mudança do UB de "Permitir que programadores usem comportamentos expostos por sua plataforma" a "Encontrar desculpas para permitir que os compiladores implementem um comportamento totalmente maluco"? Também me pergunto o quanto essas otimizações acabam melhorando o tamanho do código depois que o código é modificado para funcionar no novo compilador? Eu não ficaria surpreso se, em muitos casos, o único efeito de adicionar essas "otimizações" ao compilador forçar os programadores a escrever código maior e mais lento, para evitar que o compilador o interrompa.
Supercat
É um desvio no ponto de vista. As pessoas ficaram menos conscientes da máquina na qual o programa é executado, ficaram mais preocupadas com a portabilidade, evitando evitar o comportamento indefinido, não especificado e definido pela implementação. Houve pressão nos otimizadores para obter os melhores resultados no benchmark, e isso significa fazer uso de toda indulgência deixada pelas especificações dos idiomas. Há também o fato de que os advogados de idiomas da Internet - Usenet de cada vez, hoje em dia - também tendem a fornecer uma visão tendenciosa da lógica e do comportamento subjacentes dos escritores de compiladores.
AProgramador15 de
11
O que acho curioso são as afirmações que vi como "C pressupõe que os programadores nunca se envolverão em comportamentos indefinidos" - um fato que historicamente não é verdadeiro. Uma declaração correta teria sido "C assumiu que os programadores não acionariam um comportamento indefinido pelo padrão, a menos que estivesse preparado para lidar com as conseqüências naturais da plataforma desse comportamento. Dado que o C foi projetado como uma linguagem de programação de sistemas, grande parte de seu objetivo era permitir que os programadores fizessem coisas específicas do sistema não definidas pelo padrão de linguagem; a idéia de que nunca o fariam é absurda #
687
É bom que os programadores realizem esforços extras para garantir a portabilidade nos casos em que diferentes plataformas inerentemente fazem coisas diferentes , mas os criadores de compiladores desperdiçam o tempo de todos quando eliminam comportamentos que os programadores historicamente poderiam razoavelmente esperar serem comuns a todos os futuros compiladores. Dados inteiros ie n, de modo que n < INT_BITSe i*(1<<n)não estourariam, eu consideraria i<<=n;mais claro que i=(unsigned)i << n;; em muitas plataformas, seria mais rápido e menor que i*=(1<<N);. O que é ganho quando os compiladores o proíbem?
supercat
Embora eu ache que seria bom para o padrão permitir traps para muitas coisas que ele chama de UB (por exemplo, excesso de números inteiros), e há boas razões para não exigir que os traps façam algo previsível, eu pensaria que, sob todos os pontos de vista imagináveis, o padrão seria melhorado se exigisse que a maioria das formas de UB ou produzisse valor indeterminado ou documentasse o fato de que eles se reservam o direito de fazer outra coisa, sem ser absolutamente necessário documentar o que essa outra coisa pode ser. Compiladores que criaram tudo "UB" seriam legais, mas provavelmente desfavorecidos ... #
317
3

Um dos primeiros casos clássicos foi a adição de número inteiro assinado. Em alguns dos processadores em uso, isso causaria uma falha e, em outros, continuaria com um valor (provavelmente o valor modular apropriado). A especificação de ambos os casos significaria que os programas para máquinas com o estilo aritmético desfavorável teriam que ter código extra, incluindo uma ramificação condicional, para algo tão semelhante à adição de número inteiro.

David Thornley
fonte
A adição de números inteiros é um caso interessante; além da possibilidade de comportamento de interceptação que, em alguns casos, seria útil, mas poderia, em outros casos, causar execução aleatória de código, há situações em que seria razoável para um compilador fazer inferências com base no fato de que o excesso de número inteiro não está especificado para quebrar. Por exemplo, um compilador com int16 bits e turnos estendidos por sinal são caros podem calcular (uchar1*uchar2) >> 4usando um turno estendido sem sinal. Infelizmente, alguns compiladores estendem inferências não apenas aos resultados, mas também aos operandos.
Supercat
2

Eu diria que era menos sobre filosofia do que sobre realidade - C sempre foi uma linguagem de plataforma cruzada, e o padrão deve refletir isso e o fato de que, no momento em que um padrão for lançado, haverá um grande número de implementações em muitos hardwares diferentes. Um padrão que proíbe o comportamento necessário seria desconsiderado ou produziria um organismo de padrões concorrente.

jmoreno
fonte
Originalmente, muitos comportamentos eram deixados indefinidos para permitir a possibilidade de que sistemas diferentes fizessem coisas diferentes, incluindo acionar uma armadilha de hardware com um manipulador que pode ou não ser configurável (e pode, se não configurado, causar comportamento arbitrariamente imprevisível). Exigir que um deslocamento à esquerda de um valor negativo não seja bloqueado, por exemplo, interromperia qualquer código que foi projetado para um sistema em que ocorreu e se baseou nesse comportamento. Em resumo, eles foram deixados indefinidos para não impedir os implementadores de fornecerem comportamentos que consideravam úteis .
supercat
Infelizmente, porém, isso foi distorcido de tal forma que mesmo o código que sabe que está sendo executado em um processador que faria algo útil em um caso específico não pode tirar proveito desse comportamento, porque os compiladores podem usar o fato de que o padrão C não não especifica o comportamento (embora a plataforma o aplique) para aplicar reescritas do mundo bizarro no código.
supercat
1

Alguns comportamentos não podem ser definidos por qualquer meio razoável. Quero dizer, acessar um ponteiro excluído. A única maneira de detectá-lo seria banir o valor do ponteiro após a exclusão (memorizar seu valor em algum lugar e não permitir que nenhuma função de alocação o retornasse mais). Não apenas essa memorização seria um exagero, mas por um longo período de programa causaria a falta dos valores permitidos dos ponteiros.

Tadeusz Kopec
fonte
ou você pode alocar todos os ponteiros como weak_ptre anular todas as referências a um ponteiro que obtém deleted ... oh espere, estamos nos aproximando da coleta de lixo: /
Matthieu M.
boost::weak_ptrA implementação de é um modelo muito bom para começar com esse padrão de uso. Em vez de rastrear e anular weak_ptrsexternamente, a weak_ptrapenas contribui para a shared_ptrcontagem fraca do contador, e a contagem fraca é basicamente uma contagem de refc para o ponteiro em si. Assim, você pode anular o shared_ptrsem precisar excluí-lo imediatamente. Não é perfeito (você ainda pode ter muitos weak_ptrs expirados mantendo o subjacente shared_countsem uma boa razão), mas pelo menos é rápido e eficiente.
fofo
0

Vou dar um exemplo em que não há praticamente nenhuma escolha sensata que não seja um comportamento indefinido. Em princípio, qualquer ponteiro pode apontar para a memória que contém qualquer variável, com a pequena exceção de variáveis ​​locais que o compilador pode saber que nunca teve seu endereço obtido. No entanto, para obter um desempenho aceitável em uma CPU moderna, um compilador deve copiar valores variáveis ​​em registradores. Operar completamente com memória insuficiente não é de partida.

Isso basicamente oferece duas opções:

1) Limpe tudo dos registros antes de qualquer acesso através de um ponteiro, caso o ponteiro aponte para a memória dessa variável em particular. Em seguida, carregue tudo o que for necessário no registro, caso os valores sejam alterados pelo ponteiro.

2) Tenha um conjunto de regras para quando um ponteiro tem permissão para usar um pseudônimo em uma variável e quando o compilador tem permissão para assumir que um ponteiro não tem um pseudônimo para uma variável.

C opta pela opção 2, porque 1 seria péssimo para o desempenho. Mas então, o que acontece se um ponteiro derramar uma variável de uma maneira que as regras C proíbem? Como o efeito depende se o compilador realmente armazenou a variável em um registro, não há como o padrão C garantir definitivamente resultados específicos.

David Schwartz
fonte
Haveria uma diferença semântica entre dizer "Um compilador pode se comportar como se X fosse verdadeiro" e dizer "Qualquer programa em que X não seja verdadeiro se envolverá em comportamento indefinido", embora, infelizmente, os padrões para não deixar clara a distinção. Em muitas situações, incluindo o seu exemplo de alias, a declaração anterior permitiria muitas otimizações do compilador que seriam impossíveis de outra forma; o último permite mais algumas "otimizações", mas muitas dessas otimizações são coisas que os programadores não gostariam.
Supercat
Por exemplo, se algum código define um foopara 42 e, em seguida, chama um método que usa um ponteiro ilegitimamente modificado para definir foopara 44, posso ver o benefício de dizer que até a próxima gravação "legítima" foo, as tentativas de lê-lo podem legitimamente produz 42 ou 44, e uma expressão como foo+foopoderia até render 86, mas vejo muito menos benefício em permitir que o compilador faça inferências estendidas e até retroativas, mudando o comportamento indefinido cujos comportamentos "naturais" plausíveis seriam benignos em uma licença para gerar código sem sentido.
supercat
0

Historicamente, o comportamento indefinido tinha dois objetivos principais:

  1. Para evitar exigir que os autores do compilador gerem código para manipular condições que nunca deveriam ocorrer.

  2. Para permitir a possibilidade de que, na ausência de código, lidar explicitamente com essas condições, as implementações podem ter vários tipos de comportamentos "naturais" que, em alguns casos, seriam úteis.

Como um exemplo simples, em algumas plataformas de hardware, a tentativa de adicionar dois números inteiros positivos assinados cuja soma é muito grande para caber em um número inteiro sinalizado produzirá um número inteiro negativo negativo em particular. Em outras implementações, ele acionará uma armadilha do processador. Para o padrão C exigir que qualquer comportamento exija que os compiladores de plataformas cujo comportamento natural seja diferente do padrão precisem gerar código extra para gerar o comportamento correto - código que pode ser mais caro que o código para a adição real. Pior, isso significaria que os programadores que desejassem o comportamento "natural" teriam que adicionar ainda mais código extra para alcançá-lo (e esse código adicional seria novamente mais caro do que a adição).

Infelizmente, alguns autores de compiladores adotaram a filosofia de que os compiladores deveriam se esforçar para encontrar condições que evocassem o Comportamento indefinido e, presumindo que tais situações nunca ocorram, tiram inferências estendidas disso. Assim, em um sistema com 32 bits int, dado código como:

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

o padrão C permitiria ao compilador dizer que se q for 46341 ou maior, a expressão q * q produzirá um resultado muito grande para caber em um int, causando consequentemente comportamento indefinido e, como resultado, o compilador teria o direito de assumir que não pode acontecer e, portanto, não seria necessário incrementar *pse isso acontecer. Se o código de chamada usar *pcomo um indicador de que ele deve descartar os resultados da computação, o efeito da otimização pode ser pegar um código que teria produzido resultados sensatos em sistemas que executam quase qualquer maneira imaginável com estouro de número inteiro (o trapping pode ser feio, mas pelo menos sensato), e transformou-o em código que pode se comportar sem sentido.

supercat
fonte
-6

Eficiência é a desculpa usual, mas qualquer que seja a desculpa, o comportamento indefinido é uma péssima idéia para a portabilidade. Com efeito, comportamentos indefinidos se tornam suposições não verificadas e não declaradas.

ddyer
fonte
7
O OP especificou o seguinte: "Minha pergunta não é sobre o que é um comportamento indefinido, ou é realmente ruim. Conheço os perigos e a maioria das citações relevantes de comportamento indefinido do padrão; portanto, evite postar respostas sobre o quão ruim é. . " Parece que você não leu a pergunta.
Etienne de Martel