Digamos que eu vá compilar algum código-fonte C ++ mal escrito que invoca um comportamento indefinido e, portanto (como eles dizem) "tudo pode acontecer".
Do ponto de vista do que a especificação da linguagem C ++ considera aceitável em um compilador "compatível", "qualquer coisa" neste cenário inclui a falha do compilador (ou roubo de minhas senhas, ou de outro modo com comportamento incorreto ou erro durante a compilação), ou é escopo do comportamento indefinido limitado especificamente ao que pode acontecer quando o executável resultante é executado?
c++
language-lawyer
undefined-behavior
Jeremy Friesner
fonte
fonte
Respostas:
A definição normativa de comportamento indefinido é a seguinte:
Embora a nota em si não seja normativa, ela descreve uma variedade de implementações de comportamentos que são conhecidas por exibir. Portanto, travar o compilador (que é a tradução encerrando abruptamente), é legítimo de acordo com essa nota. Mas realmente, como diz o texto normativo, o padrão não impõe nenhum limite para a execução ou tradução. Se uma implementação roubar suas senhas, não é uma violação de nenhum contrato estabelecido no padrão.
fonte
A maioria dos tipos de UB com os quais geralmente nos preocupamos, como NULL-deref ou dividir por zero, são UB em tempo de execução . Compilar uma função que causaria UB em tempo de execução, se executada, não deve causar a falha do compilador. A menos que possa provar que a função (e aquele caminho através da função) definitivamente será executada pelo programa.
(2ª reflexão: talvez eu não tenha considerado a avaliação necessária do template / constexpr em tempo de compilação. Possivelmente UB durante isso pode causar estranheza arbitrária durante a tradução, mesmo se a função resultante nunca for chamada.)
O comportamento durante a tradução da citação ISO C ++ na resposta de @ StoryTeller é semelhante à linguagem usada no padrão ISO C. C não inclui modelos ou avaliação
constexpr
obrigatória em tempo de compilação.Mas curiosidade : ISO C diz em uma nota que se a tradução for encerrada, deve ser com uma mensagem de diagnóstico. Ou "comportando-se durante a tradução ... de maneira documentada". Não acho que "ignorar a situação completamente" possa ser lido como incluindo a interrupção da tradução.
Resposta antiga, escrita antes de eu aprender sobre o tempo de tradução UB. É verdade para o runtime-UB, entretanto, e, portanto, ainda potencialmente útil.
Não existe UB que aconteça em tempo de compilação. Ele pode ser visível para o compilador ao longo de um determinado caminho de execução, mas em termos de C ++ não aconteceu até que a execução alcance esse caminho de execução por meio de uma função.
Defeitos em um programa que tornam impossível até mesmo compilar não são UB, são erros de sintaxe. Esse programa "não é bem formado" na terminologia C ++ (se eu entendi corretamente o padrão). Um programa pode ser bem formado, mas conter UB. Diferença entre comportamento indefinido e mal formado, nenhuma mensagem de diagnóstico necessária
A menos que eu esteja entendendo mal alguma coisa, ISO C ++ requer que este programa seja compilado e executado corretamente, porque a execução nunca atinge a divisão por zero. (Na prática ( Godbolt ), bons compiladores apenas fazem executáveis funcionais. Gcc / clang avisa sobre
x / 0
isso, mas não, mesmo durante a otimização. Mas de qualquer forma, estamos tentando dizer o quão baixo ISO C ++ permite que a qualidade de implementação seja. Portanto, verificar o gcc / clang dificilmente é um teste útil além de confirmar que escrevi o programa corretamente.)int cause_UB() { int x=0; return 1 / x; // UB if ever reached. // Note I'm avoiding x/0 in case that counts as translation time UB. // UB still obvious when optimizing across statements, though. } int main(){ if (0) cause_UB(); }
Um caso de uso para isso pode envolver o pré-processador C, ou
constexpr
variáveis e ramificações nessas variáveis, o que leva a um absurdo em alguns caminhos que nunca são alcançados para essas escolhas de constantes.Caminhos de execução que causam UB visível no tempo de compilação podem ser assumidos como nunca tomados, por exemplo, um compilador para x86 poderia emitir um
ud2
(causar exceção de instrução ilegal) como a definição paracause_UB()
. Ou dentro de uma função, se um lado de umif()
levar a UB comprovável , o ramal pode ser removido.Mas o compilador ainda precisa compilar todo o resto de uma maneira sã e correta. Todos os caminhos que não encontram (ou não podem ser encontrados) UB ainda devem ser compilados para asm que executa como se a máquina abstrata C ++ o estivesse executando.
Você poderia argumentar que o UB visível no tempo de compilação incondicional
main
é uma exceção a essa regra. Ou, de outra forma, comprovável em tempo de compilação que a execução começandomain
em de fato atinge UB garantido.Eu ainda argumentaria que os comportamentos legais do compilador incluem a produção de uma granada que explode se for executada. Ou, mais plausivelmente, uma definição de
main
que consiste em uma única instrução ilegal. Eu diria que, se você nunca executou o programa, ainda não houve nenhum UB. O compilador em si não pode explodir, IMO.Funções contendo UB possíveis ou prováveis dentro dos ramos
O UB ao longo de qualquer caminho de execução volta no tempo para "contaminar" todo o código anterior. Mas, na prática, os compiladores só podem tirar proveito dessa regra quando podem realmente provar que os caminhos de execução levam a UB visível no tempo de compilação. por exemplo
int minefield(int x) { if (x == 3) { *(char*)nullptr = x/0; } return x * 5; }
O compilador deve fazer um conjunto que funcione para todos os
x
outros que não 3, até os pontos em quex * 5
causa estouro de sinal UB em INT_MIN e INT_MAX. Se esta função nunca for chamada comx==3
, o programa obviamente não contém UB e deve funcionar como escrito.Poderíamos também ter escrito
if(x == 3) __builtin_unreachable();
em GNU C para dizer ao compilador quex
definitivamente não é 3.Na prática, há código de "campo minado" em todos os lugares em programas normais. por exemplo, qualquer divisão por um inteiro promete ao compilador que é diferente de zero. Qualquer deref de ponteiro promete ao compilador que não é NULL.
fonte
O que significa "legal" aqui? Qualquer coisa que não contradiga o padrão C ou C ++ é legal, de acordo com esses padrões. Se você executar uma declaração
i = i++;
e, como resultado, os dinossauros dominarem o mundo, isso não contradiz os padrões. No entanto, contradiz as leis da física, por isso não vai acontecer :-)Se o comportamento indefinido travar seu compilador, isso não viola o padrão C ou C ++. No entanto, isso significa que a qualidade do compilador pode (e provavelmente deve) ser melhorada.
Nas versões anteriores do padrão C, havia declarações que eram erros ou não dependentes de comportamento indefinido:
char* p = 1 / 0;
É permitido atribuir uma constante 0 a um char *. Permitir uma constante diferente de zero não é. Uma vez que o valor de 1/0 é um comportamento indefinido, é um comportamento indefinido se o compilador deve ou não aceitar esta instrução. (Hoje em dia, 1/0 não atende mais à definição de "expressão constante inteira").
fonte
O Padrão não imporia requisitos sobre o comportamento de uma implementação, caso fosse encontrado
#include "'foo'"
. Se o criador do compilador julgar que seria útil processar as diretivas de inclusão dessa forma (contendo os apóstrofos no nome do arquivo), executando o programa indicado com sua saída direcionada para um arquivo temporário e, em seguida, comportando-se como um#include
desse arquivo, então uma tentativa processar um programa que contém a linha acima pode executar o programafoo
, com quaisquer consequências resultantes.Portanto, em geral não há limite para o que pode acontecer como consequência de tentar traduzir um programa C, mesmo que ninguém faça esforço para executá-lo.
fonte