Esta publicação do Stack Overflow lista uma lista bastante abrangente de situações em que a especificação da linguagem C / C ++ declara ser 'comportamento indefinido'. No entanto, quero entender por que outras linguagens modernas, como C # ou Java, não têm o conceito de 'comportamento indefinido'. Isso significa que o designer do compilador pode controlar todos os cenários possíveis (C # e Java) ou não (C e C ++)?
50
nullptr
) não alguém se preocupou em definir o comportamento escrevendo e / ou adotando uma especificação proposta ". : cRespostas:
O comportamento indefinido é uma daquelas coisas que foram reconhecidas como uma péssima idéia apenas em retrospecto.
Os primeiros compiladores foram grandes conquistas e elogiaram com júbilo as melhorias em relação à alternativa - programação em linguagem de máquina ou linguagem de montagem. Os problemas com isso eram bem conhecidos e as linguagens de alto nível foram inventadas especificamente para resolver esses problemas conhecidos. (O entusiasmo na época era tão grande que as HLLs eram às vezes aclamadas como "o fim da programação" - como se de agora em diante tivéssemos que escrever apenas trivialmente o que queríamos e o compilador faria todo o trabalho real.)
Não foi até mais tarde que percebemos os problemas mais recentes que vieram com a abordagem mais recente. Estar distante da máquina real em que o código é executado significa que há mais possibilidade de as coisas silenciosamente não fazerem o que esperávamos que elas fizessem. Por exemplo, alocar uma variável normalmente deixaria o valor inicial indefinido; isso não foi considerado um problema, porque você não alocaria uma variável se não quisesse manter um valor nela, certo? Certamente não era demais esperar que programadores profissionais não esquecessem de atribuir o valor inicial, não é?
Aconteceu que, com as bases de código maiores e as estruturas mais complicadas que se tornaram possíveis com sistemas de programação mais poderosos, sim, muitos programadores de fato cometiam tais omissões de tempos em tempos, e o comportamento indefinido resultante se tornava um grande problema. Ainda hoje, a maioria dos vazamentos de segurança de pequeno a horrível é o resultado de um comportamento indefinido de uma forma ou de outra. (O motivo é que, geralmente, o comportamento indefinido é realmente muito definido pelas coisas do próximo nível inferior na computação, e os atacantes que entendem esse nível podem usar essa sala de manobra para fazer com que um programa não faça apenas coisas não intencionais, mas exatamente as coisas eles pretendem.)
Desde que reconhecemos isso, houve um esforço geral para banir o comportamento indefinido de linguagens de alto nível, e o Java foi particularmente completo sobre isso (o que foi relativamente fácil, pois ele foi projetado para rodar em sua própria máquina virtual especificamente projetada). Idiomas antigos como C não podem ser facilmente adaptados dessa maneira sem perder a compatibilidade com a enorme quantidade de código existente.
Edit: Como apontado, a eficiência é outro motivo. Comportamento indefinido significa que os escritores do compilador têm muita margem de manobra para explorar a arquitetura de destino, de modo que cada implementação consiga a implementação mais rápida possível de cada recurso. Isso foi mais importante nas máquinas com pouca potência de ontem do que com hoje, quando o salário do programador costuma ser o gargalo para o desenvolvimento de software.
fonte
int32_t add(int32_t x, int32_t y)
) em C ++. Os argumentos usuais em torno desse são relacionados à eficiência, mas frequentemente intercalados com alguns argumentos de portabilidade (como em "Escreva uma vez, execute ... na plataforma em que você o escreveu ... e em nenhum outro lugar ;-)"). Grosso modo, um argumento poderia, portanto, ser: Algumas coisas são indefinido, porque você não sabe se você estiver em um microcontrolador de 16 bits ou um servidor de 64 bits (um fraco, mas ainda um argumento)Basicamente, porque os designers de Java e linguagens semelhantes não queriam um comportamento indefinido em sua linguagem. Isso foi uma troca - permitir que comportamentos indefinidos tenham o potencial de melhorar o desempenho, mas os projetistas de idiomas priorizaram mais a segurança e a previsibilidade.
Por exemplo, se você alocar uma matriz em C, os dados são indefinidos. Em Java, todos os bytes devem ser inicializados para 0 (ou algum outro valor especificado). Isso significa que o tempo de execução deve passar sobre a matriz (uma operação O (n)), enquanto C pode executar a alocação em um instante. Portanto, C sempre será mais rápido para essas operações.
Se o código que utiliza a matriz irá preenchê-lo de qualquer maneira antes de ler, isso é basicamente um esforço desperdiçado para Java. Mas no caso em que o código é lido primeiro, você obtém resultados previsíveis em Java, mas resultados imprevisíveis em C.
fonte
valgrind
, o qual mostraria exatamente onde o valor não inicializado foi usado. Você não pode usar ovalgrind
código java porque o tempo de execução faz a inicialização, tornandovalgrind
inúteis as verificações de s.O comportamento indefinido permite uma otimização significativa, dando latitude ao compilador para fazer algo estranho ou inesperado (ou mesmo normal) em certos limites ou outras condições.
Consulte http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
fonte
a + b
ser compilado para aadd b a
instrução nativa em todas as situações, em vez de exigir que um compilador simule alguma outra forma de aritmética inteira assinada.HashSet
é maravilhoso.<<
pode ser o caso difícil.x << y
avalia com algum valor válido do tipo,int32_t
mas não vamos dizer qual". Isso permite que os implementadores usem a solução rápida, mas não age como uma falsa pré-condição, permitindo otimizações no estilo de viagem no tempo, porque o não determinismo é limitado à saída dessa operação - a especificação garante que memória, variáveis voláteis etc. não sejam visivelmente afetadas pela avaliação da expressão. ...Nos primeiros dias de C, havia muito caos. Compiladores diferentes trataram o idioma de maneira diferente. Quando houvesse interesse em escrever uma especificação para a linguagem, essa especificação precisaria ser razoavelmente compatível com o C em que os programadores estavam confiando com seus compiladores. Mas alguns desses detalhes não são portáteis e não fazem sentido em geral, por exemplo, assumindo uma disposição ou disposição de dados específica. O padrão C, portanto, reserva muitos detalhes como comportamento indefinido ou especificado pela implementação, o que deixa muita flexibilidade para os escritores do compilador. O C ++ se baseia no C e também apresenta um comportamento indefinido.
O Java tentou ser uma linguagem muito mais segura e muito mais simples que o C ++. Java define a semântica da linguagem em termos de uma máquina virtual completa. Isso deixa pouco espaço para comportamento indefinido, por outro lado, exige requisitos que podem ser difíceis para uma implementação Java (por exemplo, que as designações de referência devem ser atômicas ou como os números inteiros funcionam). Onde o Java suporta operações potencialmente inseguras, elas geralmente são verificadas pela máquina virtual em tempo de execução (por exemplo, algumas transmissões).
fonte
this
null?" Verifica um tempo atrás, alegando que othis
sernullptr
é UB e, portanto, nunca pode realmente acontecer.)As linguagens JVM e .NET são fáceis:
Existem bons pontos para as escolhas:
Onde as hachuras de escape são fornecidas, elas convidam o comportamento indefinido completo a voltar. Mas pelo menos elas geralmente são usadas apenas em alguns trechos muito curtos, que são, portanto, mais fáceis de verificar manualmente.
fonte
unsafe
palavra-chave ou atributos emSystem.Runtime.InteropServices
). Mantendo essas coisas para os poucos programadores que sabem como depurar coisas não gerenciadas e novamente o mínimo possível, mantemos os problemas em baixa. Faz mais de dez anos desde o último martelo inseguro relacionado ao desempenho, mas às vezes você precisa fazê-lo porque não há literalmente outra solução.Java e C # são caracterizados por um fornecedor dominante, pelo menos no início de seu desenvolvimento. (Sun e Microsoft, respectivamente). C e C ++ são diferentes; eles tiveram várias implementações concorrentes desde o início. C também rodou em plataformas de hardware exóticas. Como resultado, houve variação entre as implementações. Os comitês da ISO que padronizaram C e C ++ poderiam concordar com um grande denominador comum, mas nos limites em que as implementações diferem, os padrões deixam espaço para a implementação.
Isso ocorre também porque a escolha de um comportamento pode ser cara em arquiteturas de hardware tendenciosas em relação a outra opção - endianness é a escolha óbvia.
fonte
A verdadeira razão se resume a uma diferença fundamental na intenção entre C e C ++, por um lado, e Java e C # (por apenas alguns exemplos), por outro. Por razões históricas, grande parte da discussão aqui fala sobre C e não sobre C ++, mas (como você provavelmente já sabe) C ++ é um descendente bastante direto de C, então o que diz sobre C se aplica igualmente a C ++.
Embora eles sejam amplamente esquecidos (e sua existência às vezes até negada), as primeiras versões do UNIX foram escritas em linguagem assembly. Grande parte (se não apenas) do objetivo original de C era a porta UNIX da linguagem assembly para uma linguagem de nível superior. Parte da intenção era escrever o máximo possível do sistema operacional em um idioma de nível superior - ou examiná-lo de outra direção, para minimizar a quantidade que precisava ser escrita em linguagem assembly.
Para conseguir isso, C precisava fornecer quase o mesmo nível de acesso ao hardware que a linguagem assembly. O PDP-11 (por exemplo) mapeou os registros de E / S para endereços específicos. Por exemplo, você leu um local de memória para verificar se uma tecla foi pressionada no console do sistema. Um bit foi definido nesse local quando havia dados aguardando para serem lidos. Você leu um byte de outro local especificado para recuperar o código ASCII da tecla que foi pressionada.
Da mesma forma, se você quiser imprimir alguns dados, verifique outro local especificado e, quando o dispositivo de saída estiver pronto, escreva seus dados em outro local especificado.
Para oferecer suporte à gravação de drivers para esses dispositivos, C permitiu especificar um local arbitrário usando algum tipo de número inteiro, convertê-lo em um ponteiro e ler ou gravar esse local na memória.
Obviamente, isso tem um problema muito sério: nem todas as máquinas na Terra têm sua memória distribuída de forma idêntica a um PDP-11 do início dos anos 70. Portanto, quando você pega esse número inteiro, converte-o em um ponteiro e, em seguida, lê ou escreve através desse ponteiro, ninguém pode fornecer nenhuma garantia razoável sobre o que você obterá. Apenas para um exemplo óbvio, a leitura e a gravação podem mapear para separar os registros no hardware; portanto, você (ao contrário da memória normal) se escreve algo e tenta lê-lo novamente, o que lê pode não corresponder ao que escreveu.
Eu posso ver algumas possibilidades que restam:
Destes, 1 parece suficientemente absurdo que dificilmente vale mais discussão. 2 é basicamente jogar fora a intenção básica da linguagem. Isso deixa a terceira opção como essencialmente a única que eles poderiam razoavelmente considerar.
Outro ponto que aparece com bastante frequência é o tamanho dos tipos inteiros. C assume a "posição" que
int
deve ter o tamanho natural sugerido pela arquitetura. Portanto, se estou programando um VAX deint
32 bits, provavelmente deve ter 32 bits, mas se estou programando um Univac de 36 bits,int
provavelmente deve ter 36 bits (e assim por diante). Provavelmente não é razoável (e talvez nem seja possível) gravar um sistema operacional para um computador de 36 bits usando apenas tipos que garantem múltiplos de 8 bits. Talvez eu esteja apenas sendo superficial, mas parece-me que se eu estivesse escrevendo um sistema operacional para uma máquina de 36 bits, provavelmente desejaria usar uma linguagem que suporte um tipo de 36 bits.Do ponto de vista da linguagem, isso leva a um comportamento ainda mais indefinido. Se eu pegar o maior valor que caberá em 32 bits, o que acontecerá quando eu adicionar 1? No hardware típico de 32 bits, ele será rolado (ou possivelmente causará algum tipo de falha no hardware). Por outro lado, se estiver rodando em hardware de 36 bits, apenas adicionará um. Se o idioma oferecer suporte à escrita de sistemas operacionais, você não poderá garantir nenhum dos dois comportamentos - basta permitir que o tamanho dos tipos e o comportamento do estouro variem de um para outro.
Java e C # podem ignorar tudo isso. Eles não pretendem oferecer suporte a sistemas operacionais de gravação. Com eles, você tem algumas opções. Uma é fazer com que o hardware suporte o que eles exigem - já que eles exigem tipos de 8, 16, 32 e 64 bits, apenas construa um hardware que suporte esses tamanhos. A outra possibilidade óbvia é que o idioma seja executado apenas em cima de outro software que forneça o ambiente desejado, independentemente do hardware subjacente.
Na maioria dos casos, isso não é realmente uma opção de escolha. Em vez disso, muitas implementações fazem um pouco de ambos. Você normalmente executa o Java em uma JVM em execução no sistema operacional. Na maioria das vezes, o sistema operacional é escrito em C e a JVM em C ++. Se a JVM estiver sendo executada em uma CPU ARM, é bem provável que a CPU inclua as extensões Jazelle da ARM, para adaptar o hardware mais de perto às necessidades de Java, portanto, menos precisa ser feito em software e o código Java é executado mais rapidamente (ou menos de qualquer maneira).
Sumário
C e C ++ têm comportamento indefinido, porque ninguém definiu uma alternativa aceitável que lhes permita fazer o que pretendem fazer. C # e Java adotam uma abordagem diferente, mas essa abordagem se encaixa mal (se é que existe) com os objetivos de C e C ++. Em particular, nenhum deles parece fornecer uma maneira razoável de escrever software de sistema (como um sistema operacional) na maioria dos hardwares escolhidos arbitrariamente. Ambos geralmente dependem das instalações fornecidas pelo software do sistema existente (geralmente escrito em C ou C ++) para realizar seus trabalhos.
fonte
Os autores do Padrão C esperavam que seus leitores reconhecessem algo que consideravam óbvio e aludido na justificativa publicada, mas não disseram abertamente: o Comitê não deveria precisar solicitar que os redatores de compiladores atendessem às necessidades de seus clientes, já que os clientes devem conhecer melhor que o Comitê quais são suas necessidades. Se é óbvio que se espera que os compiladores para certos tipos de plataformas processem uma construção de uma certa maneira, ninguém deve se importar se o Padrão diz que essa construção invoca o Comportamento Indefinido. O fracasso da Norma em exigir que os compiladores em conformidade processem um pedaço de código útil de forma alguma implica que os programadores estejam dispostos a comprar compiladores que não o fazem.
Essa abordagem para o design de idiomas funciona muito bem em um mundo onde os escritores de compiladores precisam vender seus produtos a clientes pagantes. Ele se desfaz completamente em um mundo onde os escritores de compiladores são isolados dos efeitos do mercado. É duvidoso que existam condições adequadas de mercado para orientar um idioma da maneira como haviam dirigido o que se tornou popular na década de 1990, e ainda mais duvidoso que qualquer designer de linguagem sã desejasse confiar nessas condições de mercado.
fonte
C ++ e c têm padrões descritivos (as versões ISO, pelo menos).
Que existem apenas para explicar como os idiomas funcionam e para fornecer uma única referência sobre o que é o idioma. Normalmente, fornecedores de compiladores e escritores de bibliotecas lideram o caminho e algumas sugestões são incluídas no principal padrão ISO.
Java e C # (ou Visual C #, que suponho que você queira dizer) têm padrões prescritivos . Eles informam o que há no idioma definitivamente antes do tempo, como ele funciona e o que é considerado comportamento permitido.
Mais importante que isso, o Java realmente tem uma "implementação de referência" no Open-JDK. (Acho que Roslyn conta como a implementação de referência do Visual C #, mas não conseguiu encontrar uma fonte para isso.)
No caso de Java, se houver alguma ambiguidade no padrão, e o Open-JDK faz isso de uma certa maneira. A maneira como o Open-JDK faz isso é o padrão.
fonte
O comportamento indefinido permite ao compilador gerar código muito eficiente em uma variedade de arquiteturas. A resposta de Erik menciona otimização, mas vai além disso.
Por exemplo, transbordamentos assinados são um comportamento indefinido em C. Na prática, esperava-se que o compilador gerasse um código de operação simples de adição assinada para a CPU executar, e o comportamento seria o que essa CPU específica fizesse.
Isso permitiu que C tivesse um desempenho muito bom e produzisse um código muito compacto na maioria das arquiteturas. Se o padrão tivesse especificado que números inteiros assinados tinham que transbordar de uma certa maneira, as CPUs que se comportassem de maneira diferente precisariam de muito mais código gerado para uma simples adição assinada.
Essa é a razão para grande parte do comportamento indefinido em C e por que coisas como o tamanho de
int
variam entre sistemas.Int
depende da arquitetura e geralmente é selecionado para ser o tipo de dados mais rápido e eficiente que é maior que achar
.Quando C era novo, essas considerações eram importantes. Os computadores eram menos potentes, geralmente com velocidade e memória limitadas de processamento. C foi usado onde o desempenho realmente importava, e os desenvolvedores deveriam entender como os computadores funcionavam bem o suficiente para saber quais seriam esses comportamentos indefinidos em seus sistemas específicos.
Linguagens posteriores, como Java e C #, preferiram eliminar o comportamento indefinido do que o desempenho bruto.
fonte
Em certo sentido, o Java também o possui. Suponha que você forneceu um comparador incorreto para Arrays.sort. Pode lançar exceção do que detecta. Caso contrário, ele classificará uma matriz de alguma maneira que não é garantida como particular.
Da mesma forma, se você modificar variáveis de vários threads, os resultados também serão imprevisíveis.
O C ++ foi além para tornar mais indefinidas situações (ou melhor, o java decidiu definir mais operações) e ter um nome para ela.
fonte
a
seria um comportamento indefinido se você pudesse obter 51 ou 73, mas se você pode obter apenas 53 ou 71, está bem definido.