Powiedzmy, że zamierzam skompilować słabo napisany kod źródłowy w C ++, który wywołuje niezdefiniowane zachowanie, a zatem (jak mówią) „wszystko może się zdarzyć”.
Z punktu widzenia tego, co specyfikacja języka C ++ uważa za dopuszczalne w kompilatorze zgodnym z wymaganiami, robi „cokolwiek” w tym scenariuszu, w tym awarię kompilatora (lub kradzież moich haseł, lub inne niewłaściwe działanie lub błędy w czasie kompilacji) lub zakres niezdefiniowanego zachowania ograniczony konkretnie do tego, co może się stać, gdy wynikowy plik wykonywalny zostanie uruchomiony?
c++
language-lawyer
undefined-behavior
Jeremy Friesner
źródło
źródło
Odpowiedzi:
Normatywna definicja niezdefiniowanego zachowania jest następująca:
Chociaż sama notatka nie jest normatywna, opisuje szereg implementacji zachowań, które są znane. Zatem awaria kompilatora (która jest nagłym zakończeniem tłumaczenia) jest zgodna z tą uwagą. Ale tak naprawdę, jak mówi tekst normatywny, standard nie nakłada żadnych ograniczeń ani dla wykonania, ani dla tłumaczenia. Jeśli implementacja kradnie Twoje hasła, nie jest to naruszenie żadnej umowy zawartej w standardzie.
źródło
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; // 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(); }
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ę dlacause_UB()
. Lub w ramach funkcji, jeśli jedna stronaif()
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ę odmain
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órychx * 5
powoduje przepełnienie podpisu UB na INT_MIN i INT_MAX. Jeśli ta funkcja nie jest nigdy wywoływana zx==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, żex
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.
źródło
Co oznacza tutaj „legalny”? Wszystko, co nie jest sprzeczne ze standardem C lub C ++, jest legalne zgodnie z tymi standardami. Jeśli wykonasz polecenie
i = i++;
i w rezultacie dinozaury przejmą władzę nad światem, nie jest to sprzeczne ze standardami. Jest to jednak sprzeczne z prawami fizyki, więc tak się nie stanie :-)Jeśli niezdefiniowane zachowanie powoduje awarię kompilatora, nie narusza to standardu C lub C ++. Oznacza to jednak, że jakość kompilatora mogłaby (i prawdopodobnie powinna) ulec poprawie.
W poprzednich wersjach standardu C występowały stwierdzenia, które były błędami lub nie były zależne od nieokreślonego zachowania:
char* p = 1 / 0;
Przypisywanie stałej 0 do znaku * jest dozwolone. Zezwolenie na niezerową stałą nie jest. Ponieważ wartość 1/0 jest niezdefiniowanym zachowaniem, niezdefiniowanym zachowaniem jest to, czy kompilator powinien, czy nie powinien zaakceptować tej instrukcji. (W dzisiejszych czasach 1/0 nie spełnia już definicji „wyrażenia liczb całkowitych stałych”).
źródło