Seja em C ou C ++, acho que esse programa ilegal, cujo comportamento de acordo com o padrão C ou C ++ é indefinido, é interessante:
#include <stdio.h>
int foo() {
int a;
const int b = a;
a = 555;
return b;
}
void bar() {
int x = 123;
int y = 456;
}
int main() {
bar();
const int n1 = foo();
const int n2 = foo();
const int n3 = foo();
printf("%d %d %d\n", n1, n2, n3);
return 0;
}
Saída na minha máquina (após compilação sem otimização):
123 555 555
Eu acho que esse programa ilegal é interessante porque ilustra a mecânica de empilhamento, porque o motivo pelo qual se usa C ou C ++ (em vez de, digamos, Java) é programar próximo ao hardware, próximo à mecânica de empilhamento e similares.
No entanto, no StackOverflow, quando o código de um interlocutor lê inadvertidamente a partir de armazenamento não inicializado, as respostas mais fortemente votadas invariavelmente citam o padrão C ou C ++ (especialmente C ++) para o efeito de que o comportamento é indefinido. Isso é verdade, é claro, no que diz respeito ao padrão - o comportamento é realmente indefinido - mas é curioso que respostas alternativas tentem, do ponto de vista do hardware ou da mecânica da pilha, investigar por que um comportamento indefinido específico (como o saída acima) pode ter ocorrido, é raro e tende a ser ignorado.
Até me lembro de uma resposta que sugeria que um comportamento indefinido poderia incluir a reformatação do meu disco rígido. Não me preocupei muito com isso antes de executar o programa acima.
Minha pergunta é a seguinte: por que é mais importante ensinar aos leitores apenas que o comportamento é indefinido em C ou C ++, do que entender o comportamento indefinido? Quero dizer, se o leitor entendeu o comportamento indefinido, ele não teria mais chances de evitá-lo?
Minha formação acontece em engenharia elétrica, e trabalho como engenheiro de construção civil, e a última vez em que trabalhei como programador em si foi em 1994, por isso estou curioso para entender a perspectiva dos usuários de maneiras mais convencionais e mais convencionais. históricos recentes de desenvolvimento de software.
fonte
Respostas:
A análise de valor do Frama-C, um analisador estático cujo objetivo é encontrar todos os comportamentos indefinidos em um programa em C, considera a atribuição
const int b = a;
como aceitável. Essa é uma decisão deliberada de design para permitirmemcpy()
(normalmente implementado como um loop sobreunsigned char
elementos de uma matriz virtual, e que o padrão C indiscutivelmente permite reimplementar como tal) copiar umastruct
(que pode ter membros preenchidos e não inicializados) para outro.A "exceção" é apenas para
lvalue = lvalue;
atribuições sem uma conversão intermediária, ou seja, para uma atribuição que equivale a uma cópia de uma fatia de memória de um local de memória para outro.Eu (como um dos autores da análise de valor de Frama-C) discuti isso com Xavier Leroy em um momento em que ele próprio se perguntava sobre a definição a escolher no compilador C verificado CompCert, para que ele pudesse ter acabado usando a mesma definição. Na minha opinião, é mais limpo do que o que o padrão C tenta fazer com valores indeterminados que podem ser representações de interceptações e o tipo
unsigned char
que é garantido não ter representações de interceptações, mas CompCert e Frama-C assumem metas relativamente não exóticas, e talvez o comitê de padronização estivesse tentando acomodar plataformas nas quais a leitura de um não inicializadoint
pode realmente abortar o programa.Retornando
b
, ou passarn1
,n2
oun3
paraprintf
, no final, pelo menos, pode ser considerado um comportamento indefinido, porque copiar uma fatia não inicializado da memória não tornando-inicializado. Com uma versão antiga do Frama-C:E em uma versão antiga do CompCert, após pequenas modificações para tornar o programa aceitável:
fonte
Comportamento indefinido significa, em última análise, que o comportamento é não determinístico. Os programadores que não sabem que estão escrevendo código não determinístico são
apenasprogramadores ignorantes. Este site tem como objetivo tornar os programadores melhores (e menos ignorantes).Escrever um programa correto em face do comportamento não determinístico não é impossível. No entanto, é um ambiente de programação especializado e requer um tipo diferente de disciplina de programação.
Mesmo no seu exemplo, se o programa receber um sinal gerado externamente, os valores na "pilha" podem mudar de forma que você não obtenha os valores esperados. Além disso, se a máquina tiver valores de interceptação, a leitura de valores aleatórios pode muito bem causar algo estranho.
fonte
Porque o comportamento específico pode não ser repetível, mesmo de execução para execução sem reconstrução.
Perseguir exatamente o que aconteceu pode ser um exercício acadêmico útil para entender melhor as peculiaridades de sua plataforma específica, mas, de uma perspectiva de codificação , a única lição relevante é "não faça isso". Uma expressão como
a++ * a++
é um erro de codificação, ponto final. Isso é realmente tudo o que alguém precisa saber.fonte
"Comportamento indefinido" é uma abreviação de "Esse comportamento não é determinístico; provavelmente não só se comportará de maneira diferente em diferentes compiladores ou plataformas de hardware, mas também poderá se comportar de maneira diferente em versões diferentes do mesmo compilador".
A maioria dos programadores consideraria isso uma característica indesejável, especialmente porque C e C ++ são linguagens baseadas em padrões ; ou seja, você os utiliza, em parte, porque a especificação do idioma garante certas formas de comportamento do idioma, se você estiver usando um compilador compatível com os padrões.
Como na maioria das coisas em programação, você deve ponderar as vantagens e desvantagens. Se o benefício de alguma operação que é UB exceder a dificuldade de fazer com que ela se comporte de maneira estável e independente de plataforma, use, por todos os meios, o comportamento indefinido. A maioria dos programadores acha que não vale a pena, na maioria das vezes.
O remédio para qualquer comportamento indefinido é examinar o comportamento que você realmente obtém, considerando uma plataforma e compilador específicos. Esse tipo de exame não é o que um programador especialista provavelmente irá explorar para você em um ambiente de perguntas e respostas.
fonte
(-1)<<1
que C89 definido como -2 em plataformas que usam não-acolchoada complemento de dois ...Se a documentação de um compilador em particular disser o que fará quando o código fizer algo que é considerado "Comportamento Indefinido" pelo padrão, o código que se baseia nesse comportamento funcionará corretamente quando compilado com esse compilador , mas poderá se comportar de maneira arbitrária quando compilado usando outro compilador cuja documentação não especifica o comportamento.
Se a documentação de um compilador não especificar como ele irá lidar com algum "comportamento indefinido" específico, o fato de o comportamento de um programa parecer obedecer a certas regras não diz nada sobre como os programas semelhantes se comportarão. Qualquer variedade de fatores pode fazer com que um compilador emita código que lida com situações inesperadas de maneira diferente - às vezes de maneira aparentemente bizarra.
Considere, por exemplo, em uma máquina em que
int
um número inteiro de 32 bits:Se
size1
esize2
ambos eram iguais a 46341 (seu produto é 2147488281), pode-se esperar que a função retorne 3, mas um compilador poderia legitimamente pular o primeiro teste completamente; ou o produto seria pequeno o suficiente para que a condição fosse falsa ou a multiplicação futura estouraria e aliviaria o compilador de qualquer requisito para fazer ou ter feito qualquer coisa. Embora esse comportamento possa parecer bizarro, alguns autores de compiladores parecem se orgulhar das habilidades dos compiladores de eliminar esses testes "desnecessários". Algumas pessoas podem esperar que um estouro na segunda multiplicação, na pior das hipóteses, faça com que todos os bits desse produto em particular sejam arbitrariamente corrompidos; de fato, no entanto,fonte
int
for um número inteiro de 32 bits, os valores do tipouint16_t
serão promovidos paraint
antes de qualquer cálculo que os envolva. Uma regra que geralmente seria adequada se as implementações tratassem apenas a aritmética assinada como diferente da não assinada nos casos em que teriam comportamentos definidos diferentes.unsigned
e com uma gama de valores que se encaixam totalmente dentro deleint
, são promovidos a assinadosint
.