Większość rodzajów UB, o które zwykle się martwimy, takich jak NULL-deref lub dzielenie przez zero, to UB środowiska uruchomieniowego . Kompilowanie funkcji, która spowodowałaby UB w czasie wykonywania, gdyby została wykonana, nie może powodować awarii kompilatora. Chyba że może udowodnić, że funkcja (i ta ścieżka przez funkcję) z pewnością zostanie wykonana przez program.
(Druga myśl: może nie uważałem, że szablon / constexpr wymaga oceny w czasie kompilacji. Możliwe, że UB podczas tego może powodować dowolną dziwność podczas tłumaczenia, nawet jeśli wynikowa funkcja nigdy nie jest wywoływana.)
Zachowanie podczas tłumaczenia części cytatu ISO C ++ w odpowiedzi @ StoryTeller jest podobne do języka używanego w standardzie ISO C. C nie zawiera szablonów ani constexpr
obowiązkowej ewaluacji w czasie kompilacji.
Ale zabawny fakt : ISO C mówi w notatce, że jeśli tłumaczenie zostanie zakończone, musi zawierać komunikat diagnostyczny. Lub „zachowując się podczas tłumaczenia ... w udokumentowany sposób”. Nie sądzę, aby „całkowite zignorowanie sytuacji” można było odczytać jako zatrzymanie tłumaczenia.
Stara odpowiedź, napisana zanim dowiedziałem się o UB czasu tłumaczenia. Jest to jednak prawdziwe dla runtime-UB, a zatem potencjalnie nadal użyteczne.
Nie ma czegoś takiego jak UB, co dzieje się w czasie kompilacji. Może to być widoczne dla kompilatora na określonej ścieżce wykonania, ale w terminologii C ++ nie miało to miejsca, dopóki wykonanie nie osiągnie tej ścieżki wykonania za pośrednictwem funkcji.
Błędy w programie, które uniemożliwiają nawet kompilację, to nie UB, lecz błędy składniowe. Taki program jest „źle sformułowany” w terminologii C ++ (jeśli mam poprawny standard). Program może być dobrze sformułowany, ale zawiera UB. Różnica między niezdefiniowanym zachowaniem a źle sformułowanym, nie jest wymagana żadna wiadomość diagnostyczna
O ile czegoś nie rozumiem, ISO C ++ wymaga, aby ten program poprawnie się skompilował i wykonał, ponieważ wykonanie nigdy nie osiąga dzielenia przez zero. (W praktyce ( Godbolt ), dobre kompilatory po prostu tworzą działające pliki wykonywalne. Gcc / clang ostrzegają, x / 0
ale nie przed tym, nawet podczas optymalizacji. Ale i tak próbujemy określić, jak niski ISO C ++ pozwala na jakość implementacji. Więc sprawdzanie gcc / clang nie jest użytecznym testem poza potwierdzeniem, że napisałem program poprawnie.)
int cause_UB() {
int x=0;
return 1 / x;
}
int main(){
if (0)
cause_UB();
}
Przykład użycia może obejmować preprocesor C lub constexpr
zmienne i rozgałęzianie się na tych zmiennych, co prowadzi do bzdur na niektórych ścieżkach, które nigdy nie są osiągane dla tych wyborów stałych.
Można założyć, że ścieżki wykonania, które powodują, że UB jest widoczny w czasie kompilacji, nigdy nie zostaną podjęte, np. Kompilator dla x86 może wyemitować ud2
(powodując wyjątek niedozwolonej instrukcji) jako definicję dla cause_UB()
. Lub w ramach funkcji, jeśli jedna strona if()
prowadzi do możliwego do udowodnienia UB, gałąź można usunąć.
Ale kompilator nadal musi skompilować wszystko inne w rozsądny i poprawny sposób. Wszystkie ścieżki, które nie napotykają (lub nie można udowodnić, że napotykają) UB muszą być nadal skompilowane do asm, który wykonuje się tak, jakby działał na abstrakcyjnej maszynie C ++.
Można argumentować, że bezwarunkowy UB widoczny w czasie kompilacji w programie main
jest wyjątkiem od tej reguły. Lub w inny sposób możliwe do udowodnienia w czasie kompilacji, że wykonanie zaczynające się od main
faktycznie osiąga gwarantowany UB.
Nadal twierdzę, że legalne zachowania kompilatora obejmują wytwarzanie granatu, który eksploduje po uruchomieniu. Lub, co bardziej prawdopodobne, definicja main
tego składa się z jednej nielegalnej instrukcji. Twierdzę, że jeśli nigdy nie uruchomisz programu, nie było jeszcze żadnego UB. Sam kompilator nie może eksplodować, IMO.
Funkcje zawierające możliwe lub możliwe do udowodnienia UB wewnątrz gałęzi
UB wzdłuż dowolnej ścieżki wykonania sięga wstecz w czasie, aby „zanieczyścić” cały poprzedni kod. Jednak w praktyce kompilatory mogą skorzystać z tej reguły tylko wtedy, gdy mogą faktycznie udowodnić, że ścieżki wykonania prowadzą do UB widocznego w czasie kompilacji. na przykład
int minefield(int x) {
if (x == 3) {
*(char*)nullptr = x/0;
}
return x * 5;
}
Kompilator musi zrobić asm, który działa dla wszystkich x
innych niż 3, aż do punktów, w których x * 5
powoduje przepełnienie podpisu UB na INT_MIN i INT_MAX. Jeśli ta funkcja nie jest nigdy wywoływana z x==3
, program oczywiście nie zawiera UB i musi działać tak, jak napisano.
Równie dobrze moglibyśmy napisać if(x == 3) __builtin_unreachable();
w GNU C, aby powiedzieć kompilatorowi, że x
zdecydowanie nie jest to 3.
W praktyce kod „pola minowego” jest wszędzie w normalnych programach. np. każde dzielenie przez liczbę całkowitą obiecuje kompilatorowi, że jest ona niezerowa. Każde deref wskaźnika obiecuje kompilatorowi, że nie ma wartości NULL.