Tudo em C é comportamento indefinido
(blog.habets.se)- Comportamento indefinido (UB) não é uma otimização maliciosa do compilador, mas uma regra segundo a qual ele não precisa lidar com caminhos de execução impossíveis sob a premissa de que o código é válido
- Em código C/C++ não trivial, há UB espalhado por toda parte, não só em double-free ou acesso fora dos limites, mas também em detalhes sutis como alinhamento, casting, inicialização e incompatibilidade de tipos
- Acessar um
int*oustd::atomic<int>*desalinhado já é UB pelo padrão, embora na prática o resultado possa variar por plataforma entre SIGBUS, correção pelo kernel ou algo que parece funcionar normalmente - Códigos comuns como passar
charcom sinal paraisxdigit(), converterfloatparaintou usarNULLincorretamente com argumentos variáveis também saem facilmente do que o padrão permite - Não dá para jogar fora codebases existentes, mas é preciso corrigir UB em larga escala combinando detecção de UB com LLM e validação por especialistas, porque as falhas são sutis demais para delegar a juniores
O comportamento indefinido em C/C++ não é um problema de otimização
- Comportamento indefinido (UB) não significa que o compilador “explora” erros do desenvolvedor, mas sim que ele pode assumir que o programa é válido segundo o padrão
- Mesmo quando a intenção parece clara para humanos, essa intenção pode ser difícil de expressar entre estágios do compilador ou entre módulos
- O compilador não é obrigado a tratar casos especiais “impossíveis” na geração de código, e o resultado pode divergir da intenção ao longo do caminho de execução, incluindo o hardware
- Desativar otimizações não torna UB seguro, e não há garantia de que o mesmo comportamento se mantenha em compiladores ou arquiteturas atuais ou futuras
UB não existe só em código anormal
- double-free, use-after-free, acesso fora dos limites do objeto e acesso a memória não inicializada são exemplos conhecidos de UB, mas continuam se repetindo na indústria inteira
- Também há muito UB mais sutil e contraintuitivo, e código C/C++ aparentemente comum pode sair facilmente do que o padrão define
- O padrão C23 contém a palavra “undefined” 283 vezes, e o escopo é ainda maior se incluirmos casos não especificados que acabam ficando indefinidos
- Em código C/C++ não trivial, UB está por toda parte, e é difícil atribuir isso apenas a descuido individual de programadores
Acesso a objetos desalinhados
- Uma função que faz dereferência de
int*como abaixo entra em UB quando o ponteiro não está alinhado corretamenteint foo(const int* p) { return *p; } - Alinhamento (alignment) costuma significar um endereço múltiplo de
sizeof(int), mas os requisitos reais podem variar conforme a plataforma e a implementação - No Linux Alpha, em alguns casos o kernel conseguia interceptar a trap e emular em software o acesso pretendido, mas em outros o programa podia morrer com
SIGBUS - Em SPARC ocorre
SIGBUS, enquanto em x86/amd64 normalmente funciona sem problemas aparentes, ou até parece uma leitura atômica - Em ARM, RISC-V e arquiteturas futuras não dá para generalizar o resultado, e arquiteturas futuras podem até ter registradores especiais que não usem os bits baixos de
int* - Se o compilador passar a usar uma instrução de load diferente, um acesso antes corrigido pelo kernel pode deixar de ser corrigido
- O compilador não é obrigado a gerar assembly que funcione com ponteiros desalinhados, porque o próprio acesso já é UB
Tipos atômicos também entram em UB se o alinhamento estiver errado
- Mesmo chamando
store()ouload()em umstd::atomic<int>*como abaixo, o comportamento é UB se o objeto não estiver alinhado corretamentevoid set_it(std::atomic<int>* p) { p->store(123); } int get_it(std::atomic<int>* p) { return p->load(); } - Pela ótica do padrão, a pergunta “essa operação é atômica mesmo em um objeto desalinhado?” nem chega a fazer sentido
- No hardware real, a atomicidade pode virar um problema, mas pelo padrão isso já é UB antes mesmo dessa discussão
- Se o objeto que você achava estar lendo atomicamente atravessa uma página, a situação fica ainda mais complicada, mas a conclusão continua sendo UB, não “está tudo bem”
Só criar o ponteiro já pode ser um problema
- Um ponteiro desalinhado pode ser problemático mesmo antes da dereferência: fazer casting para um ponteiro de certo tipo já pode ser UB
bool parse_packet(const uint8_t* bytes) { const int* magic_intp = (const int*)bytes; // UB! int magic_raw = foo(magic_intp); // Probably crashes on SPARC. int magic = ntohl(magic_raw); // this is fine, at least. […] } - Aqui, o problema não é a chamada de
foo(), mas o cast(const int*)bytes - Pelo padrão, o compilador poderia até atribuir significado aos bits baixos de
int*, como metadados de garbage collection ou bits de tag de segurança
O problema de passar char para isxdigit()
- O código abaixo parece simples, mas pode ser UB em arquiteturas onde
charé signed quando o valor de entrada sai da faixa 0–127bool bar(char ch) { return isxdigit(ch); } isxdigit()é a função que verifica se um caractere é hexadecimal, e também pode receberEOF- Segundo C23 7.4p1,
EOFé umint, do qual se pode inferir que é um valor que não pode ser representado porunsigned char isxdigit()recebeint, nãochar, e embora a conversão decharparaintseja permitida, valores negativos designed charcausam problema- Segundo C23 6.2.5 parágrafo 20, o fato de
charser signed ou não é definido pela implementação - Uma implementação de
isxdigit()como a seguir pode acabar lendo memória desconhecida com um índice negativoint isxdigit(int c) { if (c == EOF) { return false; } return some_array[c]; } - Se essa memória estiver em uma área mapeada para I/O, isso pode causar não só valores aleatórios ou crash, mas até comportamento de hardware
- Isso é mais provável em sistemas embarcados do que em aplicações de desktop, mas há casos em espaço de usuário, como drivers de rede em user space, onde a proteção também pode ser insuficiente
O problema do cast de float para int
- Código como este, que converte um
floatem segundos para umintem milissegundos, é comum, mas contém UBint milliseconds(float seconds) { int tmp = (int)(seconds * 1000.0); /* WRONG */ return tmp + 1; /* WRONG separately (signed overflow is UB) */ } - C23 6.3.1.4 determina que, ao converter um valor finito de ponto flutuante real para um tipo inteiro, se a parte inteira não puder ser representada por esse tipo, o comportamento é indefinido
- Para valores não finitos, a falta de especificação também leva a UB
- Até comparar
floatcomINT_MAXestá longe de ser simples- Fazer cast de
floatparaintpode disparar exatamente o UB que se queria evitar - Fazer cast de
INT_MAXparafloatnão garante representação exata - Se
INT_MAXfor arredondado emfloatpara um valor que não cabe emint, a comparação deixa de ser representativa
- Fazer cast de
- Para tornar isso seguro, são necessários teste com
isfinite(), comparações com margem comoINT_MIN + 1000eINT_MAX - 1000, além de uma checagem extra após a conversão e antes da somaint milliseconds(float seconds) { const float ftmp = seconds * 1000.0f; if (!isfinite(ftmp)) { return 0; } if ((float)(INT_MIN + 1000) > ftmp) { return 0; } if ((float)(INT_MAX - 1000) < ftmp) { return 0; } const int tmp = (int)ftmp; if (INT_MAX == tmp) { return 0; } return tmp + 1; } - Você só queria converter
floatparaint, mas o código seguro fica muito maior
Objeto no endereço 0 e null pointer
- Em kernels de SO ou código embarcado, pode surgir a necessidade de colocar um objeto no endereço 0
- Na prática, dá para dizer que não existe um jeito viável de fazer isso em conformidade com o padrão C
- Em C 6.3.2.3, a constante inteira 0 e
nullptrque podem ser convertidos em ponteiro são “null pointer constant”, e aqui podem ser chamados deNULL - C não especifica que um ponteiro
NULLreal aponte para o endereço de máquina 0 - O padrão C trata da máquina abstrata de C, não do hardware, e só garante que
NULLe 0 se comparam como iguais - Essa igualdade pode existir porque o inteiro 0 é convertido para o valor nativo de
NULLdaquela plataforma, e esse valor poderia até ser0xffff - Dereferenciar um null pointer é UB independentemente de qual seja seu valor, e isso é um exemplo clássico em C 3.4.3
- Portanto, não se pode assumir que
memset(&ptr, 0, sizeof(ptr));cria um ponteiroNULL - A prática de zerar uma struct e assumir que seus ponteiros membro são
NULLé um problema real até para a maioria dos programadores experientes - Historicamente já existiram máquinas com ponteiros
NULLdiferentes de zero
O problema de assumir que há uma função no endereço 0
- Mesmo que, em máquinas modernas,
NULLaponte para o endereço 0 e realmente exista ali um objeto ou função, C 6.3.2.3 determina queNULLnão é igual a nenhum objeto nem função - Portanto, o código abaixo é UB
void (*func_ptr)() = NULL; func_ptr(); - Do ponto de vista de C, isso significa “não há função ali”, e talvez nem exista um jeito de expressar essa intenção dentro do compilador
- Não dá para simplesmente assumir que será emitido um comando de call para um endereço com todos os bits zerados
- Em x86 de 16 bits, nem sequer fica claro se “todos os zeros” significa
0000:0000ouCS:0000
Argumentos variáveis e incompatibilidade de tipos
- O último argumento de
execl()precisa ser um ponteiro, então passar diretamente a macroNULLou o inteiro 0 pode ser UBexecl("/bin/sh", "sh", "-c", "date", NULL); /* WRONG */ execl("/bin/sh", "sh", "-c", "date", 0); /* WRONG */ - A forma correta é fazer cast explícito para tipo ponteiro
execl("/bin/sh", "sh", "-c", "date", (char*)NULL); - A macro
NULLpode ser interpretada como inteiro 0, e em argumentos variáveis a informação de tipo necessária não é transmitida - Em
printf(), se o especificador de formato não corresponder ao tipo real do argumento, isso também é UBuint64_t blah = 123; printf("%ld\n", blah); /* WRONG */ - Para imprimir
uint64_t, deve-se usarPRIu64uint64_t blah = 123; printf("%"PRIu64"\n", blah); - Para imprimir
uid_t, uma opção pode ser fazer cast parauintmax_te usarPRIuMAX, mas nem sequer é garantido queuid_tseja unsigned - No pior caso, em vez de
-1, pode ser exibido algum valor sem sentido
Divisão por zero e problemas de segurança
- O fato de divisão por zero ser UB é amplamente conhecido, mas quando o denominador vem de entrada não confiável isso vira um problema de segurança
- O ponto importante é que UB pode surgir na fronteira de validação de entrada, não apenas como um erro trivial de runtime
Não é UB, mas promoção de inteiros também é perigosa
- As regras de promoção de inteiros são difíceis de aplicar no ritmo de uma leitura rápida e podem produzir resultados contraintuitivos
- No código abaixo,
overflowedvira 0, e não 1unsigned char a = 0xff; unsigned char b = 1; unsigned char zero = 0; bool overflowed = (a + b) == zero; // overflowed is set to zero, not one. - No código a seguir, embora todas as variáveis pareçam unsigned, o resultado não é
2147483648 (0x80000000), mas18446744071562067968 (ffffffff80000000)unsigned char a = 0x80; uint64_t b = a << 24; // Bonus UB(?) - Mesmo quando não há UB, as regras de inteiros em C/C++ não são intuitivas e facilitam a criação de defeitos
Detecção de UB com LLM
- LLMs modernos, quando recebem a tarefa de encontrar UB em código C arbitrário, quase sempre encontram algum problema e geralmente acertam
- Depois de encontrar UB em código pessoal, a mesma abordagem foi aplicada até ao código do OpenBSD, conhecido por ser maduro e escrito com rigor
- Vários problemas foram encontrados tendo
findcomo alvo inicial - Para o OpenBSD, foram enviados patches para escrita fora dos limites e para um bug lógico que não era UB
- Muitos outros casos restantes de UB não receberam patch
- Havia a experiência anterior de que o projeto OpenBSD não era muito receptivo a bug reports
- Havia a avaliação de que, na prática, aquilo talvez estivesse aceitável
- Para remover UB da codebase do OpenBSD, seria necessário um projeto maior do que simplesmente enviar patches individuais intermediados por uma LLM
Um caminho realista para lidar com codebases C/C++
- Não dá para descartar codebases C/C++ existentes, mas também não é opção deixá-las em um estado essencialmente quebrado
- É preciso corrigir UB em larga escala sem fazer commit de mudanças ruins geradas por IA e sem sobrecarregar revisores humanos
- Em 2026, escrever C ou C++ sem supervisão de UB por LLM pode passar a ser visto como uma violação da SOX e como algo irresponsável
- Se nem os desenvolvedores do OpenBSD encontraram todos esses problemas em mais de 30 anos, a chance em outros projetos é ainda menor
- Em projetos pessoais, é possível pedir a uma LLM que encontre UB, explique quando necessário, proponha correções e então deixar a validação final para humanos
- Ainda assim, validar o resultado exige especialistas, e especialistas normalmente já estão ocupados com outras coisas
- Isso parece trabalho de faxina, mas é sutil demais para ser delegado aos programadores juniores que tradicionalmente acabam recebendo esse tipo de tarefa
1 comentários
Comentários do Hacker News
Em C há muitos comportamentos indefinidos surpreendentes e estranhos, mas este texto não consegue mostrar bem isso e só arranha a superfície
Um exemplo ainda mais estranho é
volatile int x = 5; printf("%d in hex is 0x%x.\n", x, x);. Sexfor apenasint, tudo bem, mas se forvolatile, isso vira comportamento indefinido. Pelo padrão C, um acessovolatilejá é um efeito colateral só por ser uma leitura, efeitos colaterais não ordenados sobre o mesmo objeto escalar são comportamento indefinido, e a avaliação dos argumentos de função tem ordem não especificada entre siNormalmente, data race significa que threads diferentes acessam o mesmo objeto ao mesmo tempo e pelo menos um dos acessos é uma escrita, mas em C pode surgir uma situação parecida com data race até em uma única thread e sem escrita
undefinedaparece no padrão, nem todos os casos indefinidos que surgem por omissãoO ponto é que isso é inevitável. Pelo menos desde que C surgiu em 1972, nunca houve um ser humano que tenha evitado isso completamente
Se em 54 anos ninguém conseguiu, então “esforce-se mais” ou “não cometa erros” não são soluções. Uma falha explorável encontrada pelo Mythos no OpenBSD foi bastante elogiada pelos desenvolvedores do OpenBSD, mas mesmo ao rodar ferramentas no código mais simples apareceram vários comportamentos indefinidos
Por exemplo, em
find, ler a variável automática não inicializadastatusdepois dewaitpid(&status)e antes de verificar se houve erro emwaitpid()também é comportamento indefinido, embora seja difícil imaginar uma arquitetura ou compilador em que isso fosse explorávelComo escrevi no texto, não estou tentando listar todos os comportamentos indefinidos do mundo, e sim dizer que todo código C/C++ não trivial contém comportamento indefinido
volatileé um hack no sistema de tipos. Deveria ter havido uma solução mais baseada em princípios, e linguagens modernas não deveriam copiar isso como se “o C fez assim, então deve ser uma boa ideia”Os compiladores C iniciais sempre gravavam os valores na memória, então se um ponteiro apontasse para hardware de entrada/saída mapeado em memória, cada mudança em
xfazia a instrução da CPU realmente escrever na memória, e o código do driver funcionavaMas quando chegaram as otimizações, o compilador passou a ver que só
xestava sendo modificado e deixou o valor apenas em registrador, quebrando o driver. Ovolatilede C é um hack para dizer ao compilador “não faça essa otimização”, enquanto a solução correta, de fornecer intrínsecos de memória mapeada em E/S no nível de biblioteca, teria sido um trabalho muito maiorIntrínsecos são necessários porque conseguem expressar exatamente o que é possível e o que não é. Em alguns alvos, escritas de 1 byte, 2 bytes e 4 bytes são operações diferentes, e o hardware distingue isso. Um dispositivo pode esperar uma escrita RGBA de 4 bytes, e se você emitir quatro escritas de 1 byte ele pode se confundir ou não funcionar. Alguns alvos até suportam escrita em nível de bit. Com
volatilesozinho, não há como saber o que está acontecendo nem qual é o significado dissoDepois de compilar um programa C e desassemblá-lo, ele vira um programa em assembly sem comportamento indefinido, porque assembly não tem esse conceito
Comportamento indefinido é uma propriedade do programa-fonte, não do executável. Significa que a especificação da linguagem em que o fonte foi escrito não atribui significado àquele programa. Já o executável gerado pela compilação recebe significado da especificação da máquina
Race é uma propriedade do comportamento do programa. Portanto, dá para dizer que um programa C tem comportamento indefinido, mas não que o executável necessariamente tenha uma race real. Claro, como o compilador pode compilar arbitrariamente um programa com comportamento indefinido, ele até poderia introduzir uma race, mas se compilar sem criar novas threads, não haverá race
volatileé justamente que o valor pode ser alterado por outra coisa. Se for uma variável global, essa outra coisa pode ser outra thread, uma interrupção ou um manipulador de sinal. Se for um ponteiro lendo um endereço específico, pode ser um registrador de dispositivo de hardware cujo valor mudaO conceito de variável
volatileem si não é o problema. Se a linguagem quer dar suporte a rotinas de interrupção e entrada/saída mapeada em memória, ela precisa de um jeito de informar ao compilador que ler o mesmo registrador de hardware duas vezes não é o mesmo que ler a mesma posição de memória duas vezesO verdadeiro problema é que a interação entre os recursos e restrições da linguagem não foi bem resolvida. Se você declarou explicitamente “este valor pode mudar a qualquer momento”, então é tolice considerar certos usos como comportamento indefinido justamente por esse motivo. Para variáveis
volatile, deveria haver uma exceção na definição de “efeitos colaterais não ordenados”Muita gente se engana achando que C e C++ são “muito flexíveis porque deixam você fazer o que quiser”. Na prática, quase toda técnica que parece poderosa e elegante é um campo minado de comportamento indefinido
O comportamento indefinido de ponteiros desalinhados é pior ainda. Um ponteiro desalinhado é comportamento indefinido não só ao acessá-lo, mas pelo próprio ponteiro em si
Então converter implicitamente
void* vparaint* i, por exemplo comi=vem C ouf(v)quandofrecebeint*, também é comportamento indefinido se o ponteiro resultante não satisfizer o alinhamento exigido paraintÉ importante notar que isso é um problema no nível de C. Se um programa C tem comportamento indefinido, então formalmente ele é inválido e está errado. Não é um problema de hardware, nem tem relação necessária com travamento ou falha
A conversão de
void*paraint*normalmente não gera instrução nenhuma no hardware, e como tipos só existem em C, o hardware também não vai travar nessa conversão. Você pode pensar que, se é só um valor inteiro num registrador, está tudo bem, mas o ponto central não é se o ponteiro é realmente um inteiro no hardware, e sim que no instante em que você faz a conversão para um ponteiro desalinhado o programa C já está quebrado por definiçãoEu também quis transmitir que comportamento indefinido não existe no hardware e não tem relação necessária com travamentos ou falhas. Ao mesmo tempo, quis mostrar exemplos para quem diz “mas veja, funciona direitinho”, e na prática não é bem assim
#pragma pack(push, 1), isso quer dizer que não posso usar ponteiros para membros a menos que o membro esteja alinhado por acaso?Esse tipo de comportamento indefinido é aceitável, e quase ninguém vê como grande problema o fato de diferenças de hardware causarem bugs
Mas com o tempo interpretações agressivas transformaram C numa linguagem de design by contract implícito, e as restrições ficaram invisíveis. Isso cria um problema parecido com o de destrutores implícitos no RAII, que também não ficam visíveis
Em C, quando você desreferencia um ponteiro, o compilador adiciona uma restrição implícita de não nulo à assinatura da função. Se você passa um ponteiro possivelmente nulo para a função, em vez de um erro por falta de verificação ou asserção, o compilador propaga silenciosamente essa restrição de não nulo ao ponteiro. Se ele provar que a restrição é falsa, marca a função como inalcançável, e chamadas a funções inalcançáveis podem tornar a função chamadora inalcançável também
As 5 etapas de aprender sobre comportamento indefinido em C
Negação: “Eu sei como overflow com sinal funciona na minha máquina”
Raiva: “Esse compilador é um lixo! Por que não faz o que eu mandei?”
Barganha: “Vou enviar esta proposta ao wg14 para consertar C...”
Depressão: “Dá para confiar em algum código C?”
Aceitação: “É só não usar comportamento indefinido”
Acesso desalinhado dá para resolver usando structs empacotadas. O compilador magicamente gera o código correto. Na verdade, ele sempre soube fazer isso direito, só não fazia
Regras rígidas de aliasing podem ser contornadas usando conversão por union. Compiladores importantes documentam isso como funcionando mesmo que o padrão não diga. Ou então basta desligar com
-fno-strict-aliasing. Aí você pode reinterpretar memória como quiser; ainda haverá cantos perigosos, mas pelo menos eles não virão do compiladorOverflow dá para definir com
-fwrapv. E se trocar+,-e*por__builtin_*_overflow, você ainda ganha verificação explícita de erro de graça. A interface funcional é boa e ainda gera código eficienteA verdadeira aceitação se aproxima mais de “pessoas normais não ligam para o padrão C”. O padrão é ruim; o que importa é o compilador. E compiladores têm muitos recursos extremamente úteis para contornar a maior parte desses problemas. As pessoas não usam porque querem escrever C “portável”, “padrão”, e superar essa mentalidade é a verdadeira aceitação
Seguindo essa lógica, fiz um interpretador Lisp em C de ambiente hospedado e ele até passou no UBSan. Achei que explodiria no começo, mas não aconteceu, e se eu consegui, qualquer um consegue
Enquanto seres humanos escreverem código, isso não pode ser o estado final. Nenhum ser humano consegue evitar completamente comportamento indefinido em C/C++
Os exemplos parecem menos comportamentos indefinidos reais e mais casos que podem se tornar comportamento indefinido dependendo da entrada ou da situação
Se ampliar assim, toda chamada de função também é comportamento indefinido, porque pode estourar o espaço de pilha. Na prática, em qualquer linguagem daria para dizer algo parecido nesse sentido
C já tem arestas reais suficientemente notáveis; esse tipo de sensacionalismo pode distrair a atenção, especialmente dos iniciantes, e acabar fazendo mais mal do que bem
STORAGE_ERRORhttp://archive.adaic.com/standards/83lrm/html/lrm-11-01.html
Ela também ocorre em “execução de uma chamada de subprograma quando não há espaço de armazenamento suficiente”
Primeiro, é possível definir o que acontece quando o espaço de pilha se esgota. Além disso, nem todo programa precisa de uma pilha de tamanho arbitrário, e alguns programas precisam apenas de um tamanho constante calculável de antemão. Há implementações de linguagem que nem usam pilha
A linguagem também pode oferecer ferramentas para verificar o espaço restante da pilha e dar garantias com base nisso. Ou pode permitir instalar um tratador para ser executado quando o espaço da pilha acabar
A forma correta de pensar é que, no momento em que surge comportamento indefinido, você deixa de estar sob a proteção do padrão da linguagem. Pode continuar funcionando por um tempo, talvez para sempre. Mas na prática passa a depender, sem perceber, dos caprichos da toolchain, de troca ou upgrade de compilador, de arquitetura, de runtime, de versão de libc
No fim, você constrói sobre areia movediça, e esse é o perigo do comportamento indefinido
O problema do comportamento indefinido não é que pode travar em alguma arquitetura
O verdadeiro problema é que o compilador espera que esse código nunca aconteça. Mesmo assim, se você escrever código com comportamento indefinido, o compilador, especialmente o otimizador, pode traduzi-lo para qualquer forma conveniente ao caminho “normal”. E esse “qualquer forma” às vezes pode ser algo muito inesperado, como apagar blocos enormes de código
Se ainda houver otimização de cauda, o bug pode nem aparecer na build de debug, mas só se revelar quando você aumentar o nível de otimização
Pior é quando o programa continua silenciosamente com valores lixo, formata seu disco rígido ou entrega ao atacante as chaves do reino
Quem pede para simplesmente definir isso ou transformá-lo em comportamento não especificado perde o ponto central de que o compilador poder eliminar grandes partes do programa é justamente o essencial
Se você escreve código que se torna comportamento indefinido para certas entradas, então sua intenção é que, para essas entradas, o programa não tenha comportamento algum. Você quer que o compilador elimine esse caminho por otimização ou faça algum outro tratamento que ajude o comportamento nos casos definidos
Colocar uma string de log alcançável apenas via comportamento indefinido e ver que ela não fica no binário é bastante satisfatório
Eu já escrevi uma análise que assumia executar no fim do pipeline de transformações, e essa suposição era necessária para a correção. Como depois disso não haveria mais otimizações, eu achava que estava seguro, mas agora já não tenho tanta certeza
Usei C por 20 anos, mas nunca vi se falar tanto de comportamento indefinido quanto nos últimos 6 meses no Hacker News
Em conversas reais isso quase nunca aparecia. Você escreve o código e, se não funcionar, depura, corrige ou contorna. Não sei por que o tema do comportamento indefinido em C continua aparecendo tanto na capa
Sempre existe uma minoria de formados em ciência da computação que acha que desenvolver ou usar uma nova linguagem de programação é a coisa mais interessante do mundo, e parte deles continua pensando assim
É natural que essas pessoas se interessem pelo lado de design de linguagem, e o comportamento indefinido em C pertence a esse campo. Só que muito disso originalmente era uma tentativa de acomodar arquiteturas de CPU antigas sem perda de desempenho, então chamar isso de “escolha de design” às vezes é tão estranho quanto dizer que rodas são redondas por design
Houve alguns “escândalos” bastante conhecidos por volta do GCC 3.2, quando o compilador começou a explorar comportamento indefinido de forma muito mais agressiva nas otimizações, e por isso muita gente ficou no GCC 2.95 por bastante tempo. O GCC 3.2 saiu em 2002
Como todas as empresas vivem enfatizando segurança e exposição, ou seja, virar notícia, a narrativa contra o “não seguro” cresceu demais
O novo mundo parece gente da cidade que nunca viu natureza bruta e se assusta ao ver um cortador de grama. Tem lâminas girando? Que absurdo!
Se o alvo real for um pequeno sistema embarcado no topo de uma torre de comunicação no meio do nada, “funciona na minha máquina” não serve de nada. Claro que a maioria não trabalha com isso, e provavelmente a maioria dos desenvolvedores aqui faz web, mas ainda assim é uma discussão interessante, talvez até mais interessante quando você não lida com isso diretamente
Compiladores podem ter bugs em que algo deveria funcionar segundo a especificação e não funciona, há muitas extensões sem equivalente no padrão, e até comportamentos que são indefinidos no padrão mas recebem um significado útil específico de implementação
Concordo em geral com a introdução, mas os exemplos são ruins e o texto todo parece uma embalagem para empurrar codificação com LLM
Dá a impressão de alguém que quer escrever qualquer código como quiser e ainda esperar que ele se comporte igual em todo ambiente. Se você fizer uma linguagem assim, perde-se a vantagem de poder escrever de forma adaptada à plataforma quando isso interessa
O código C++ deste texto em parte já não era idiomático há mais de 10 anos, e hoje seria visto como code smell
A linguagem evoluiu bastante e já é bem diferente da que era quando foi criada. Assim que apareceram vários ponteiros crus e acesso manual a ponteiros, ficou claro que parte do texto precisava ser lida com algum filtro
Outro problema óbvio é a perspectiva de juntar C e C++ como se fossem quase a mesma linguagem. Hoje em dia elas já estão bem distantes na prática
std::atomic, nãoatomic_intEntão é assim que se entende comportamento indefinido em C?
O programa P tem um conjunto A de entradas que não provoca comportamento indefinido, e um conjunto complementar B que provoca
Um compilador correto compila P para um executável P'. Para toda entrada em A, P' deve se comportar da mesma forma que P
Mas para qualquer entrada em B, o comportamento de P' não tem nenhuma exigência
Exemplo concreto de comportamento indefinido causado por ponteiro desalinhado: https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on...