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

530

O que é um comportamento indefinido em C e C ++? E o comportamento não especificado e o comportamento definido pela implementação? Qual a diferença entre eles?

Zolomon
fonte
1
Eu tinha certeza de que já fizemos isso antes, mas não consigo encontrá-lo. Veja também: stackoverflow.com/questions/2301372/…
dmckee --- gatinho ex-moderador
1
Aqui está uma discussão interessante (a seção "Anexo L e comportamento indefinido").
Owen

Respostas:

406

O comportamento indefinido é um daqueles aspectos da linguagem C e C ++ que pode surpreender os programadores vindos de outras linguagens (outras linguagens tentam escondê-lo melhor). Basicamente, é possível escrever programas em C ++ que não se comportam de maneira previsível, mesmo que muitos compiladores em C ++ não relatem erros no programa!

Vejamos um exemplo clássico:

#include <iostream>

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

A variável paponta 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 do padrão C ++, ele invoca um comportamento indefinido :

O efeito de tentar modificar um literal de cadeia de caracteres é indefinido.

Eu posso ouvir as pessoas gritando "Mas espere, eu posso compilar isso sem problemas e obter a saída yellow" ou "O que você quer dizer com literais de seqüência indefinidos são armazenados na memória somente leitura, para que a primeira tentativa de atribuição resulte em um dump principal". Esse é exatamente o problema do comportamento indefinido. Basicamente, o padrão permite que qualquer coisa aconteça depois que você invoca um comportamento indefinido (mesmo demônios nasais). Se existe um comportamento "correto" de acordo com o seu modelo mental da linguagem, esse modelo está 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, desreferenciar o ponteiro nulo , acessar objetos após o término de sua vida útil ou escrever expressões supostamente inteligentes como i++ + ++i.

A Seção 1.9 do padrão C ++ também menciona os 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 definidos pela implementação (por exemplo sizeof(int)). Estes constituem os parâmetros da máquina abstrata. Cada implementação deve incluir 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 dos argumentos de uma função). Onde possível, esta Norma define um conjunto de comportamentos permitidos. Eles definem os aspectos não determinísticos da máquina abstrata.

Certas outras operações são descritas nesta Norma como indefinidas (por exemplo, o efeito de desreferenciar o ponteiro nulo). [ Nota : esta Norma Internacional não impõe requisitos ao comportamento de programas que contêm comportamento indefinido. - nota final ]

Especificamente, a seção 1.3.24 declara:

O comportamento indefinido permitido varia de ignorar completamente a situação com resultados imprevisíveis , comportar-se durante a tradução ou a execução de um programa de maneira documentada, característica do ambiente (com ou sem a emissão de uma mensagem de diagnóstico), até o término de uma tradução ou execução (com a emissão de uma mensagem de diagnóstico).

O que você pode fazer para evitar um comportamento indefinido? Basicamente, você precisa ler bons livros em C ++ de autores que sabem do que estão falando. Parafusos tutoriais da Internet. Bullschildt do parafuso.

fredoverflow
fonte
6
É um fato estranho que resultou da mesclagem de que esta resposta cobre apenas C ++, mas as tags dessa pergunta incluem C. C tem uma noção diferente de "comportamento indefinido": ainda exigirá que a implementação forneça mensagens de diagnóstico, mesmo se o comportamento também for indicado. ser indefinido para determinadas violações de regras (violações de restrição).
Johannes Schaub - litb
8
@Benoit É um comportamento indefinido porque o padrão diz que é um comportamento indefinido, ponto final. Em alguns sistemas, na verdade literais de string são armazenados no segmento de texto somente leitura, e o programa falhará se você tentar modificar um literal de string. Em outros sistemas, o literal da string parecerá realmente alterado. O padrão não determina o que deve acontecer. É isso que comportamento indefinido significa.
Fredoverflow
5
@FredOverflow, Por que um bom compilador nos permite compilar código que oferece um comportamento indefinido? Exatamente o que de bom pode compilar esse tipo de código? Por que nem todos os bons compiladores nos deram um enorme sinal de alerta vermelho quando tentamos compilar código que oferece comportamento indefinido?
Pacerier 27/09/13
14
@Pacerier Há certas coisas que não podem ser verificadas no momento da compilação. Por exemplo, nem sempre é possível garantir que um ponteiro nulo nunca seja desreferenciado, mas isso é indefinido.
precisa saber é o seguinte
4
@Celeritas, o comportamento indefinido pode ser não determinístico. Por exemplo, é impossível saber antecipadamente qual será o conteúdo da memória não inicializada, por exemplo. int f(){int a; return a;}: o valor de apode mudar entre as chamadas de função.
Mark
97

Bem, isso é basicamente um copiar-colar direto do padrão

3.4.1 1 comportamento definido pela implementação comportamento não especificado 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 ordem superior quando um número inteiro assinado é deslocado para a direita.

3.4.3 1 comportamento comportamental indefinido , mediante o uso de uma construção de programa não transportável ou incorreta ou de dados errados, para os quais esta Norma Internacional não impõe requisitos

2 NOTA O possível comportamento indefinido varia de ignorar a situação completamente com resultados imprevisíveis, se comportar durante a tradução ou a execução do programa de maneira documentada característica do ambiente (com ou sem a emissão de uma mensagem de diagnóstico), até o término de 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 em excesso de número inteiro.

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

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

Formiga
fonte
3
Qual é a diferença entre o comportamento definido pela implementação e o não especificado?
Zolomon
26
@Zolomon: Assim como diz: basicamente a mesma coisa, exceto que, no caso de implementação definida, a implementação é requerida para documentar (para garantir) o que exatamente vai acontecer, enquanto no caso de não especificada, a implementação não é obrigada a documentar ou garantir qualquer coisa.
AnT
1
@Zolomon: Isso se reflete na diferença entre 3.4.1 e 2.4.4.
S7 /
8
@Eleritas: compiladores hiper-modernos podem fazer melhor que isso. Dado que int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }um compilador pode determinar que, como todos os meios de invocar a função que não inicia os mísseis invocam o Comportamento indefinido, ele pode tornar a chamada launch_missiles()incondicional.
Supercat
2
@ northerner Como a citação afirma, o comportamento não especificado geralmente é restrito a um conjunto limitado de comportamentos possíveis. Em alguns casos, você pode até chegar à conclusão de que todas essas possibilidades são aceitáveis ​​no contexto fornecido, caso em que o comportamento não especificado não é um problema. O comportamento indefinido é totalmente irrestrito (eb "o programa pode decidir formatar seu disco rígido"). O comportamento indefinido é sempre um problema.
AnT
60

Talvez uma 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 do compilador 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 intque não se encaixa char. Como você coloca esse valor char? na verdade não tem como! Tudo poderia acontecer, mas a coisa mais sensata seria pegar o primeiro byte desse int e inseri-lo char. É errado fazer isso para atribuir o primeiro byte, mas é o que acontece sob o capô.

comportamento não especificado
Qual função 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?

O idioma 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 Acho que vale a pena editar sua resposta para esclarecer mais :)

pois fun(fun1(), fun2());o comportamento "implementação não está definido"? O compilador tem que escolher um ou outro curso, afinal?

A diferença entre definido pela implementação e não especificado, é que o compilador deve escolher um comportamento no primeiro caso, mas não no segundo caso. Por exemplo, uma implementação deve ter uma e apenas uma definição de sizeof(int). Portanto, não se pode dizer que sizeof(int)é 4 para uma parte do programa e 8 para outras. Diferentemente do comportamento não especificado, onde o compilador pode dizer OK, vou avaliar esses argumentos da esquerda para a direita e os argumentos da próxima função são avaliados da direita para a esquerda. Isso pode acontecer no mesmo programa, por isso é chamado de não especificado . De fato, o C ++ poderia ter sido facilitado se alguns 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 "por aí" aproveitando a liberdade e algumas pessoas defendendo apaixonadamente essa liberdade, uma mudança seria difícil e levaria décadas para penetrar nos cantos distantes dos mundos C e C ++. Estou decepcionado que nem todos os compiladores alertam contra códigos como ++ i + i ++. Da mesma forma, a ordem de avaliação dos argumentos não é especificada.

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

AraK
fonte
1
pois fun(fun1(), fun2());não é o comportamento "implementation defined"? O compilador tem que escolher um ou outro curso, afinal?
Lazer
1
@ Arak: obrigado pela explicação. Eu entendo isso agora. Btw, "I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"eu entendo isso canacontecer. Realmente, com os compiladores que usamos hoje em dia?
Lazer
1
@eSKay Você precisa perguntar a um guru sobre isso que sujou as mãos de muitos compiladores :) O AFAIK VC avalia sempre os argumentos da direita para a esquerda.
AraK
4
@Lazer: Isso definitivamente pode acontecer. Cenário simples: foo (bar, boz ()) e foo (boz (), bar), em que bar é um int e boz () é uma função que retorna int. Suponha uma CPU em que os parâmetros sejam passados ​​nos registradores R0-R1. Os resultados da função são retornados em R0; funções podem lixeira R1. Avaliar "bar" antes de "boz ()" exigiria salvar uma cópia da barra em outro lugar antes de chamar boz () e, em seguida, carregar a cópia salva. Avaliar "bar" depois de "boz ()" evitará um armazenamento de memória e será buscado novamente, e é uma otimização que muitos compiladores fariam, independentemente de sua ordem na lista de argumentos.
supercat
6
Eu não sei sobre C ++, mas o padrão C diz que a conversão de um int em um char é uma implementação definida ou até bem definida (dependendo dos valores reais e da assinatura dos tipos). Veja C99 §6.3.1.3 (inalterado em C11).
Nikolai Ruhe
27

Do documento oficial de justificativa C

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, além de 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 fornece ao implementador alguma latitude na tradução de programas. Essa latitude não se estende a falhar 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. Ele 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 exige que essa opção 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 o quão extensa deve ser 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.

Johannes Schaub - litb
fonte
3
Gravadores de compiladores hiper-modernos também consideram "comportamento indefinido" como dar licença aos gravadores de compilador para assumir que os programas nunca receberão entradas que causariam um comportamento indefinido e alterar arbitrariamente todos os aspectos de como os programas se comportam quando recebem tais entradas.
Supercat
2
Outro ponto que acabei de notar: o C89 não usou o termo "extensão" para descrever os recursos garantidos em algumas implementações, mas não em outras. Os autores do C89 reconheceram que a maioria das implementações atuais trataria aritmética assinada e aritmética não assinada de maneira idêntica, exceto quando os resultados foram usados ​​de certas maneiras, e esse tratamento foi aplicado mesmo em caso de estouro assinado; eles não listaram isso como uma extensão comum no Anexo J2, no entanto, o que sugere para mim que a viam como um estado natural de coisas, e não como uma extensão.
Supercat
10

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

Seu resumo final:

Em resumo, o comportamento não especificado é geralmente algo com que 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.

Anders Abel
fonte
1
Existem dois tipos de compiladores: aqueles que, a menos que estejam explicitamente documentados de outra forma, interpretam a maioria das formas de Comportamento Indefinido do Padrão como recorrendo a comportamentos característicos documentados pelo ambiente subjacente e aqueles que, por padrão, apenas expõem de maneira útil comportamentos que o Padrão caracteriza como Definido pela implementação. Ao usar compiladores do primeiro tipo, muitas coisas do primeiro tipo podem ser feitas com eficiência e segurança usando o UB. Os compiladores para o segundo tipo serão adequados apenas para essas tarefas se fornecerem opções para garantir o comportamento nesses casos.
21817
8

Historicamente, o Comportamento Definido pela Implementação e o Comportamento Indefinido representavam situações nas quais os autores da Norma esperavam que as pessoas que escrevessem implementações de qualidade usassem julgamento para decidir quais garantias comportamentais, se houver, seriam úteis para programas no campo de aplicação pretendido em execução no alvos pretendidos. As necessidades do código de processamento de números de ponta são bastante diferentes das do código de sistemas de baixo nível, e o UB e o BID oferecem aos escritores de compiladores flexibilidade para atender a essas diferentes necessidades. Nenhuma categoria determina que as implementações se comportem de uma maneira que seja útil para uma finalidade específica ou mesmo para qualquer finalidade. As implementações de qualidade que afirmam ser adequadas a um propósito específico, no entanto, devem se comportar de maneira adequada a esse objetivose a Norma exige ou não .

A única diferença entre o comportamento definido pela implementação e o comportamento indefinido é que o primeiro exige que as implementações definam e documentem um comportamento consistente, mesmo nos casos em que nada que a implementação possa fazer seria útil . A linha divisória entre eles não é se geralmente seria útil para implementações definir comportamentos (os escritores do compilador devem definir comportamentos úteis quando praticáveis, se o Padrão exige ou não), mas se pode haver implementações em que definir um comportamento seria simultaneamente caro. e inútil . Um julgamento de que tais implementações possam existir não implica, de forma alguma, nenhum julgamento sobre a utilidade de suportar um comportamento definido em outras plataformas.

Infelizmente, desde meados da década de 90, os escritores de compiladores começaram a interpretar a falta de mandatos comportamentais como um julgamento de que as garantias comportamentais não valem o custo, mesmo nos campos de aplicativos onde são vitais e até nos sistemas em que não custam praticamente nada. Em vez de tratar o UB como um convite para exercer um julgamento razoável, os escritores do compilador começaram a tratá-lo 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 precisaria fazer nenhum esforço para tratar a expressão v << powcomo uma mudança de complemento de dois sem considerar se vera positivo ou negativo.

A filosofia preferida entre alguns dos escritores de compiladores de hoje, no entanto, sugeriria que, porque vsó pode ser negativo se o programa se envolver em Comportamento indefinido, não há razão para que o programa limite o intervalo negativo v. Embora o deslocamento para a esquerda de valores negativos costumava ser suportado em todos os compiladores significativos, e uma grande quantidade de código existente se baseie nesse comportamento, a filosofia moderna interpretaria o fato de o Padrão afirmar que os valores negativos para o deslocamento à esquerda são UB como implicando que os escritores do compilador devem ficar à vontade para ignorá-lo.

supercat
fonte
Mas lidar com um comportamento indefinido de uma maneira agradável não é de graça. Todo o motivo pelo qual os compiladores modernos exibem um comportamento tão bizarro em alguns casos do UB é que eles estão otimizando incansavelmente e, para fazer o melhor trabalho, precisam assumir que o UB nunca ocorre.
Tom Swirly
Mas o fato de <<ser UB em números negativos é uma pequena armadilha desagradável e fico feliz em ser lembrado disso!
Tom Swirly
1
@TomSwirly: Infelizmente, os criadores de compiladores não se importam que oferecer garantias comportamentais frouxas além daquelas exigidas pelo Padrão muitas vezes permita um aumento de velocidade maciço em comparação com exigir que esse código evite a todo custo qualquer coisa não definida pelo Padrão. Se um programador não se importa se i+j>kgera 1 ou 0 nos casos em que a adição transborda, desde que não tenha outros efeitos colaterais , um compilador poderá fazer algumas otimizações massivas que não seriam possíveis se o programador escrevesse o código como (int)((unsigned)i+j) > k.
Supercat
1
@TomSwirly: Para eles, se o compilador X puder executar um programa em conformidade estrita para executar alguma tarefa T e produzir um executável 5% mais eficiente do que o compilador Y produziria com o mesmo programa, isso significa que X é melhor, mesmo que Y poderia gerar código que executasse a mesma tarefa três vezes mais eficientemente, considerando um programa que explora comportamentos que Y garante, mas X não.
Supercat 25/05
6

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 que cada implementação documenta

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


Padrão C ++ n3337 § 1.3.24 comportamento indefinido

comportamento para o qual este Padrão Internacional não impõe requisitos [Nota: Um comportamento indefinido pode ser esperado quando este Padrão Internacional omite qualquer definição explícita de comportamento ou quando um programa usa uma construção ou dados errados. O comportamento indefinido permitido varia de ignorar a situação completamente com resultados imprevisíveis, comportar-se durante a tradução ou a execução de um programa de maneira documentada, característica do ambiente (com ou sem a emissão de uma mensagem de diagnóstico), até o término de uma tradução ou execução (com a emissão de uma mensagem de diagnóstico). Muitas construções de programas erradas não geram comportamento indefinido; eles precisam ser diagnosticados. - nota final]

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


N3337 padrão C ++ § 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 é geralmente delineada por este Padrão Internacional. - nota final]

O C ++ Standard não impõe um comportamento específico em algumas construções, mas diz que um comportamento específico e bem definido deve ser escolhido ( não é necessário descrever o bot ) por uma implementação específica (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á.

4pie0
fonte
6

Implementação definida

Os implementadores desejam, devem ser bem documentados, o padrão oferece opções, mas certamente compila

Não especificado -

O mesmo que definido pela implementação, mas não documentado

Indefinido-

Qualquer coisa pode acontecer, cuide disso.

Suraj K Thomas
fonte
2
Eu acho importante notar que o significado prático de "indefinido" mudou nos últimos anos. Antigamente uint32_t s;, avaliar 1u<<squando sé 33 pode render 0 ou talvez 2, mas não fazer outra coisa maluca. No entanto, a avaliação de compiladores mais recentes 1u<<spode fazer com que um compilador determine que, como sdeve ter sido menor que 32, qualquer código antes ou depois dessa expressão que só seria relevante se stivesse 32 ou mais pode ser omitido.
Supercat