Comportamento indefinido, não especificado e definido pela implementação

Qual é a diferença entre o comportamento indefinido, não especificado e definido pela implementação em C e C ++?

   

O comportamento indefinido é um dos aspectos da linguagem C e C ++ que pode ser surpreendente para programadores vindos de outras linguagens (outras linguagens tentam escondê-lo melhor). Basicamente, é possível escrever programas em C ++ que não se comportam de uma maneira previsível, mesmo que muitos compiladores de C ++ não reportem nenhum erro no programa!

Vamos ver um exemplo clássico:

#include  int main() { char* p = "hello!\n"; // yes I know, deprecated conversion p[0] = 'y'; p[5] = 'w'; std::cout < < p; } 

A variável p aponta para a string literal "hello!\n" , e as duas atribuições abaixo tentam modificar essa string literal. O que este programa faz? De acordo com a seção 2.14.5, parágrafo 11 da norma C ++, ele invoca um comportamento indefinido :

O efeito de tentar modificar um literal de string é indefinido.

Eu posso ouvir as pessoas gritando "Mas espere, eu posso compilar isso sem problema e obter a saída yellow " ou "O que você quer dizer com undefined, literais de string são armazenados em memory somente leitura, então a primeira tentativa de atribuição resulta em um dump principal" . Este é exatamente o problema com o comportamento indefinido. Basicamente, o padrão permite que qualquer coisa aconteça quando você invoca um comportamento indefinido (até mesmo demônios nasais). Se há um comportamento "correto" de acordo com o seu modelo mental da linguagem, esse modelo é simplesmente errado; O padrão C ++ tem o único voto, ponto final.

Outros exemplos de comportamento indefinido incluem acessar uma matriz além de seus limites, desreferenciando o ponteiro nulo , acessando objects após o término de sua vida útil ou escrevendo expressões supostamente inteligentes como i++ + ++i .

A seção 1.9 do padrão C ++ também menciona dois irmãos menos perigosos do comportamento indefinido , comportamento não especificado e comportamento definido pela implementação :

As descrições semânticas nesta Norma definem uma máquina abstrata não determinística parametrizada.

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.

Certos outros aspectos e operações da máquina abstrata são descritos nesta Norma como não especificados (por exemplo, ordem de avaliação de argumentos para uma function). Sempre que possível, esta Norma define um conjunto de comportamentos permitidos. Estes definem os aspectos não determinísticos da máquina abstrata.

Certas outras operações são descritas nesta Norma como indefinida (por exemplo, o efeito de desreferenciamento do ponteiro nulo). [ Nota : esta Norma não impõe requisitos sobre o comportamento de programas que contenham comportamento indefinido. - nota final

Especificamente, a seção 1.3.24 declara:

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 emissão de uma mensagem de diagnóstico), encerrar uma tradução ou execução (com a emissão de uma mensagem de diagnóstico).

O que você pode fazer para evitar entrar em comportamento indefinido? Basicamente, você tem que ler bons livros em C ++ por autores que sabem do que estão falando. Tutoriais de internet de parafuso. Parafuso bullschildt.

Bem, isso é basicamente um copy-paste direto do padrão

3.4.1 Comportamento não especificado de comportamento definido pela implementação, em que cada implementação documenta como a escolha é feita

2 EXEMPLO Um exemplo de comportamento definido pela implementação é a propagação do bit de alta ordem quando um inteiro com sinal é deslocado para a direita.

3.4.3 1 comportamento comportamental indefinido , mediante a utilização de uma construção de programa não-portável ou errônea ou de dados errados, para os quais esta Norma não impõe requisitos

2 NOTA Possível comportamento indefinido 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).

3 EXEMPLO Um exemplo de comportamento indefinido é o comportamento no estouro de inteiro.

3.4.4 uso de um comportamento não especificado de um valor não especificado, ou outro comportamento em que esta Norma Internacional forneça duas ou mais possibilidades e não imponha requisitos adicionais aos quais é escolhido em qualquer instância

2 EXEMPLO Um exemplo de comportamento não especificado é a ordem na qual os argumentos para uma function são avaliados.

Talvez a redação fácil possa ser mais fácil de entender do que a definição rigorosa dos padrões.

comportamento definido pela implementação
A linguagem diz que temos tipos de dados. Os fornecedores de compiladores especificam quais tamanhos eles devem usar e fornecem uma documentação do que eles fizeram.

comportamento indefinido
Você está fazendo algo errado. Por exemplo, você tem um valor muito grande em um int que não cabe no char . Como você coloca esse valor em char ? na verdade não tem jeito! Qualquer coisa poderia acontecer, mas a coisa mais sensata seria pegar o primeiro byte desse int e colocá-lo em char . É errado fazer isso para atribuir o primeiro byte, mas isso é o que acontece sob o capô.

comportamento não especificado
Qual function desses dois é executada primeiro?

 void fun(int n, int m); int fun1() { cout < < "fun1"; return 1; } int fun2() { cout << "fun2"; return 2; } ... fun(fun1(), fun2()); // which one is executed first? 

A linguagem não especifica a avaliação, da esquerda para a direita ou da direita para a esquerda! Portanto, um comportamento não especificado pode ou não resultar em um comportamento indefinido, mas certamente seu programa não deve produzir um comportamento não especificado.


@eSKay Eu acho que sua pergunta vale a pena editar a resposta para esclarecer mais 🙂

por fun(fun1(), fun2()); não é o comportamento "implementação definida"? O compilador tem que escolher um ou outro curso, afinal de contas?

A diferença entre implementação definida e não especificada, é que o compilador deve escolher um comportamento no primeiro caso, mas não é necessário no segundo caso. Por exemplo, uma implementação deve ter uma e apenas uma definição de sizeof(int) . Portanto, não é possível dizer que sizeof(int) é 4 para alguma parte do programa e 8 para outros. Ao contrário do comportamento não especificado, onde o compilador pode dizer OK, eu vou avaliar esses argumentos da esquerda para a direita e os argumentos da próxima function são avaliados da direita para a esquerda. Pode acontecer no mesmo programa, por isso é chamado de não especificado . Na verdade, o C ++ poderia ter sido facilitado se alguns dos comportamentos não especificados fossem especificados. Dê uma olhada aqui na resposta do Dr. Stroustrup para isso :

Alega-se que a diferença entre o que pode ser produzido dando ao compilador essa liberdade e exigindo "avaliação ordinária da esquerda para a direita" pode ser significativa. Não estou convencido, mas com inúmeros compiladores "lá fora" aproveitando a liberdade e algumas pessoas defendendo apaixonadamente essa liberdade, uma mudança seria difícil e poderia levar décadas para penetrar nos cantos distantes dos mundos C e C ++. Eu estou desapontado que nem todos os compiladores avisam contra código como ++ i + i ++. Da mesma forma, a ordem de avaliação dos argumentos não é especificada.

Na IMO, muitas "coisas" são deixadas indefinidas, não especificadas, definidas pela implementação etc. No entanto, é fácil dizer e até mesmo dar exemplos, mas é difícil de corrigir. Também deve ser notado que não é tão difícil evitar a maioria dos problemas e produzir código portátil.

Do documento oficial do Racional C

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

Comportamento não especificado fornece ao implementador alguma latitude na tradução de programas. Essa latitude não se estende a ponto de não conseguir traduzir o programa.

Comportamento indefinido fornece a licença do implementador para não detectar certos erros do programa que são difíceis de diagnosticar. Ele também identifica áreas de possível extensão de linguagem em conformidade: o implementador pode aumentar a linguagem fornecendo uma definição do comportamento oficialmente indefinido.

O comportamento definido pela implementação fornece ao implementador a liberdade de escolher a abordagem apropriada, mas exige 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 de codificação significativas com base na definição de implementação. Os implementadores devem ter em mente este critério ao decidir quão extensa uma definição de implementação deve ser. Como no comportamento não especificado, simplesmente não traduzir a fonte que contém o comportamento definido pela implementação não é uma resposta adequada.

Comportamento indefinido vs. Comportamento não especificado tem uma breve descrição dele.

Seu resumo final:

Resumindo, o comportamento não especificado é geralmente algo com o qual você não deve se preocupar, a menos que seu software precise ser portátil. Por outro lado, o comportamento indefinido é sempre indesejável e nunca deve ocorrer.

Historicamente, tanto Comportamento Definido por Implementação quanto Comportamento Indefinido representavam situações em que os autores do Padrão esperavam que as pessoas que escrevessem implementações de qualidade usassem de julgamento para decidir quais garantias comportamentais seriam úteis para programas no campo de aplicação pretendido em execução no alvos pretendidos. As necessidades do código high-end de processamento de números são bem diferentes daquelas do código de sistemas de baixo nível, e tanto o UB quanto o IDB dão flexibilidade aos escritores de compiladores para atender a essas diferentes necessidades. Nenhuma das categorias determina que as implementações se comportem de maneira útil para qualquer propósito específico, ou mesmo para qualquer finalidade. Implementações de qualidade que afirmam ser adequadas para uma finalidade específica, entretanto, devem se comportar de maneira condizente com tal propósito, quer a Norma exija ou não .

A única diferença entre Comportamento Definido por Implementação e Comportamento Indefinido é que o primeiro exige que as implementações definam e documentem um comportamento consistente mesmo em casos em que nada que a implementação possa fazer seja útil . A linha divisória entre eles não é se seria geralmente útil para implementações definir comportamentos (escritores de compiladores deveriam definir comportamentos úteis quando prático se o Padrão os requer ou não) mas se haveria implementações onde definir um comportamento seria simultaneamente caro e inútil . Um julgamento de que tais implementações possam existir não de forma alguma, forma ou formulário, implica qualquer julgamento sobre a utilidade de suportar um comportamento definido em outras plataformas.

Infelizmente, desde meados da década de 90, escritores de compiladores começaram a interpretar a falta de mandatos comportamentais como um julgamento de que garantias comportamentais não valem o custo, mesmo em campos de aplicação onde são vitais, e até mesmo em sistemas onde eles custam praticamente nada. Em vez de tratar a UB como um convite para exercer um julgamento razoável, os escritores de compiladores começaram a tratá-la como uma desculpa para não fazê-lo.

Por exemplo, dado o seguinte código:

 int scaled_velocity(int v, unsigned char pow) { if (v > 250) v = 250; if (v < -250) v = -250; return v << pow; } 

a implementação de um complemento de dois não teria que gastar nenhum esforço para tratar a expressão v < < pow como uma mudança do complemento de dois sem considerar se v era positivo ou negativo.

A filosofia preferida entre alguns dos escritores de compiladores de hoje, no entanto, sugeriria que v pode ser negativo apenas se o programa se envolver em Comportamento Indefinido, não há razão para que o programa retenha o intervalo negativo de v . Embora os valores negativos de deslocamento para a esquerda costumem ser suportados em cada compilador de significância, e uma grande quantidade de código existente depende desse comportamento, a filosofia moderna interpretaria o fato de que o padrão diz que os valores negativos de deslocamento à esquerda são UB como implicando que os escritores do compilador devem se sentir livres para ignorar isso.

Implementação definida

Os implementadores desejam, devem ser bem documentados, o padrão dá escolhas, mas é certo compilar

Não especificado –

Mesmo que definido pela implementação, mas não documentado

Indefinido-

Tudo pode acontecer, cuidar disso.

Padrão C ++ n3337 § 1.3.10 comportamento definido pela implementação

comportamento, para uma construção de programa bem formada e dados corretos, que depende da implementação e de que cada documento de implementação

Às vezes, o C ++ Standard não impõe um comportamento particular em algumas construções, mas diz que um comportamento específico e bem definido deve ser escolhido e descrito pela implementação específica (versão da biblioteca). Assim, o usuário ainda pode saber exatamente como o programa se comportará mesmo que o padrão não o descreva.


Padrão C ++ n3337 § 1.3.24 comportamento indefinido

comportamento para o qual esta Norma Internacional não impõe requisitos [Nota: O comportamento indefinido pode ser esperado quando esta Norma Internacional omite qualquer definição explícita de comportamento ou quando um programa usa uma construção errônea ou dados errados. 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 emissão de uma mensagem de diagnóstico), encerrar uma tradução ou execução (com a emissão de uma mensagem de diagnóstico). Muitas construções errôneas de programas não geram um comportamento indefinido; eles precisam ser diagnosticados. – nota final

Quando o programa encontra uma construção que não é definida de acordo com o C ++ Standard, é permitido fazer o que quiser (talvez enviar um email para mim ou talvez enviar um email para você ou talvez ignorar completamente o código).


Padrão C ++ n3337 § 1.3.25 comportamento não especificado

comportamento, para uma construção de programa bem formada e dados corretos, que depende da implementação [Nota: A implementação não é necessária para documentar qual comportamento ocorre. A gama de comportamentos possíveis é normalmente delineada por esta Norma Internacional. – nota final

O C ++ Standard não impõe um comportamento particular em algumas construções, mas diz que um determinado comportamento bem definido deve ser escolhido ( bot não é necessário descrito ) pela implementação particular (versão da biblioteca). Portanto, no caso em que nenhuma descrição foi fornecida, pode ser difícil para o usuário saber exatamente como o programa se comportará.

Existem muitas construções que devem se comportar de uma maneira útil e previsível em alguns casos, mas não podem ser feitas em todos os casos em todas as implementações. Frequentemente, o conjunto de casos para os quais uma construção deve ser utilizável dependerá da plataforma de destino e do campo de aplicação. Como a implementação de diferentes destinos e campos deve manipular diferentes conjuntos de casos, o Padrão visualiza a questão de quais casos tratar como um problema de Qualidade de Implementação. Além disso, como os autores do Padrão não precisavam proibir implementações “conformes” de serem de qualidade tão baixa que não pudessem ser úteis, elas geralmente não se preocupam em exigir explicitamente o comportamento dos casos que esperavam que todas as implementações não-lixo suportassem mesmo sem um mandato.

Por exemplo, o código:

 struct foo {int x;} = {0}; int main(void) { foo.x = 1; return foo.x-1; } 

usa um lvalue do tipo int [ie foo.x ] para acessar o valor armazenado de um object do tipo struct foo , mesmo que o N1570 6.5p7 não contenha nada que permita que um object do tipo struct foo seja acessado, exceto por meio de um lvalue do tipo struct foo ou um lvalue de um tipo de caractere, nem o Standard contém qualquer linguagem que exija expressões de access de membro de estrutura dos requisitos de 6.5p7.

Obviamente, qualquer compilador que não possa lidar com expressões simples de access ao membro struct deve ser visto como sendo de qualidade excepcionalmente baixa e provavelmente não é adequado para muito de qualquer coisa. Consequentemente, deve ser razoável esperar que qualquer um que deseje produzir uma implementação de qualidade apoiará tal construto, independentemente de o Padrão o exigir ou não. Desde que os escritores do compilador possam ser confiáveis, façam um esforço genuíno para produzir compiladores de qualidade que sejam adequados aos seus propósitos pretendidos, e sejam abertos sobre as finalidades para as quais seus compiladores são ou não adequados, não haveria razão para ter a tinta residual padrão. tentando afirmar coisas que deveriam ser óbvias. Muitas ações que devem ter comportamentos utilizáveis ​​e previsíveis são, na verdade, Undefined Behavior porque os autores dos editores de compiladores confiáveis ​​do Standard exercem julgamento razoável, em vez de usar o fato de que as ações invocam o comportamento indefinido como uma desculpa para lançar o julgamento pela janela. .