Rozważ ten kod:
int i = 1;
int x = ++i + ++i;
Mamy pewne przypuszczenia, co kompilator mógłby zrobić dla tego kodu, zakładając, że się kompiluje.
- oba
++i
zwracają2
, w wyniku czegox=4
. - jeden
++i
wraca,2
a drugi wraca3
, w wyniku czegox=5
. - oba
++i
zwracają3
, w wyniku czegox=6
.
Dla mnie druga wydaje się najbardziej prawdopodobna. Wykonywany jest jeden z dwóch ++
operatorów i = 1
, a i
jest zwiększany i 2
zwracany jest wynik . Następnie ++
wykonywany jest drugi operator with i = 2
, i
jest zwiększany i 3
zwracany jest wynik . Następnie 2
i 3
są dodawane razem, aby dać 5
.
Jednak uruchomiłem ten kod w Visual Studio i wynik był 6
. Próbuję lepiej zrozumieć kompilatory i zastanawiam się, co może doprowadzić do wyniku 6
. Domyślam się tylko, że kod mógłby zostać wykonany z pewną "wbudowaną" współbieżnością. ++
Zostały wywołane dwa operatory, każdy z nich został zwiększony, i
zanim drugi zwrócił, a następnie oba zwrócone 3
. Byłoby to sprzeczne z moim rozumieniem stosu wywołań i musiałoby zostać wyjaśnione.
Jakie (rozsądne) rzeczy mógłby C++
zrobić kompilator, co doprowadziłoby do wyniku 4
lub wyniku lub 6
?
Uwaga
Ten przykład pojawił się jako przykład niezdefiniowanego zachowania w książce Bjarne Stroustrup Programming: Principles and Practice using C ++ (C ++ 14).
Zobacz komentarz cynamonu .
źródło
Odpowiedzi:
Kompilator pobiera kod, dzieli go na bardzo proste instrukcje, a następnie ponownie łączy i układa je w sposób, który wydaje mu się optymalny.
Kod
int i = 1; int x = ++i + ++i;
składa się z następujących instrukcji:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 5. read i as tmp2 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
Ale pomimo tego, że jest to lista numerowana w sposób, w jaki ją napisałem, istnieje tylko kilka zależności w kolejności : 1-> 2-> 3-> 4-> 5-> 10-> 11 i 1-> 6-> 7- > 8-> 9-> 10-> 11 muszą pozostać we względnej kolejności. Poza tym kompilator może dowolnie zmieniać kolejność i być może eliminować nadmiarowość.
Na przykład możesz uporządkować listę w ten sposób:
1. store 1 in i 2. read i as tmp1 6. read i as tmp3 3. add 1 to tmp1 7. add 1 to tmp3 4. store tmp1 in i 8. store tmp3 in i 5. read i as tmp2 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
Dlaczego kompilator może to zrobić? Ponieważ nie ma sekwencjonowania skutków ubocznych przyrostu. Ale teraz kompilator może uprościć: na przykład w 4 znajduje się martwy magazyn: wartość jest natychmiast zastępowana. Ponadto tmp2 i tmp4 to tak naprawdę to samo.
1. store 1 in i 2. read i as tmp1 6. read i as tmp3 3. add 1 to tmp1 7. add 1 to tmp3 8. store tmp3 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
A teraz wszystko, co ma związek z tmp1, to martwy kod: nigdy nie jest używany. Ponowne przeczytanie i można również wyeliminować:
1. store 1 in i 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 10. add tmp3 and tmp3, as tmp5 11. store tmp5 in x
Spójrz, ten kod jest znacznie krótszy. Optymalizator jest zadowolony. Programista nie jest, ponieważ zostałem zwiększony tylko raz. Ups.
Spójrzmy na coś innego, co może zrobić kompilator: wróćmy do oryginalnej wersji.
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 5. read i as tmp2 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
Kompilator może zmienić kolejność w następujący sposób:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 5. read i as tmp2 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
a potem jeszcze raz zauważ, że czytano i dwukrotnie, więc wyeliminuj jedną z nich:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
To miłe, ale może pójść dalej: może ponownie użyć tmp1:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 6. read i as tmp1 7. add 1 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Wtedy może wyeliminować ponowne odczytanie i w 6:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 7. add 1 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Teraz 4 to martwy sklep:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 7. add 1 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
a teraz 3 i 7 można połączyć w jedną instrukcję:
1. store 1 in i 2. read i as tmp1 3+7. add 2 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Wyeliminuj ostatnie tymczasowe:
1. store 1 in i 2. read i as tmp1 3+7. add 2 to tmp1 8. store tmp1 in i 10. add tmp1 and tmp1, as tmp5 11. store tmp5 in x
A teraz otrzymujesz wynik, jaki daje Ci Visual C ++.
Zauważ, że w obu ścieżkach optymalizacji ważne zależności kolejności zostały zachowane, o ile instrukcje nie zostały usunięte bez robienia niczego.
źródło
Chociaż jest to UB (jak sugerował OP), poniżej przedstawiono hipotetyczne sposoby, w jakie kompilator może uzyskać 3 wyniki. Wszystkie trzy dałyby ten sam poprawny
x
wynik, gdyby były użyte z różnymiint i = 1, j = 1;
zmiennymi zamiast jednej i tej sameji
.int i = 1; int i1 = i, i2 = i; // i1 = i2 = 1 ++i1; // i1 = 2 ++i2; // i2 = 2 int x = i1 + i2; // x = 4
int i = 1; int i1 = ++i; // i1 = 2 int i2 = ++i; // i2 = 3 int x = i1 + i2; // x = 5
int i = 1; int &i1 = i, &i2 = i; ++i1; // i = 2 ++i2; // i = 3 int x = i1 + i2; // x = 6
źródło
i
. Wiedząc, że może się to zdarzyć tylko raz, emituje to tylko raz. W przypadku opcji 2 kod jest tłumaczony na kod maszynowy dosłownie, tak jak mógłby to zrobić projekt klasy kompilatora uczelni. Opcja 3 jest podobna do opcji 1, ale utworzyła dwie kopie preinkrementacji. Musiał użyć wektora, a nie zbioru. :-)Idę na opcję nr 4: Obie
++i
miejsce jednocześnie.Nowsze procesory zmierzają w kierunku kilku interesujących optymalizacji i równoległej oceny kodu, o ile jest to dozwolone, tak jak tutaj, to kolejny sposób, w jaki kompilatory nadal tworzą szybszy kod. Postrzegam jako praktyczną realizację , kompilatory zmierzające w kierunku równoległości.
Z łatwością mogłem zobaczyć stan wyścigu powodujący niedeterministyczne zachowanie lub błąd magistrali z powodu tej samej rywalizacji o pamięć - wszystko dozwolone, ponieważ koder naruszył kontrakt C ++ - stąd UB.
to dało , ale nie licz na to.
Nie używaj
++i + ++i
ani nie oczekuj rozsądnych rezultatów.źródło
i
do dwóch rejestrów, inkrementować oba rejestry i zapisać je z powrotem. Procesor nie ma możliwości rozwiązania tego problemu. Podstawowym problemem jest to, że ani C ++, ani nowoczesne procesory nie są ściśle sekwencyjne. C ++ jawnie ma sekwencjonowanie wydarzyło się przed i zdarza się po, aby domyślnie zezwolić na zdarzenie w tym samym czasie.4
lub5
, - w zależności od modelu / szybkości wykonania wątku procesora, więc jest to UB w sercu.Myślę, że prosta i nieskomplikowana interpretacja (bez żadnej oferty optymalizacji kompilatora lub wielowątkowości) byłaby po prostu:
i
i
i
+i
Przy
i
dwukrotnym zwiększeniu jego wartość wynosi 3, a po dodaniu suma wynosi 6.Dla sprawdzenia potraktuj to jako funkcję C ++:
int dblInc () { int i = 1; int x = ++i + ++i; return x; }
Oto kod asemblera, który otrzymuję z kompilacji tej funkcji, używając starej wersji kompilatora GNU C ++ (win32, wersja gcc 3.4.2 (mingw-special)). Nie ma tu żadnych wymyślnych optymalizacji ani wielowątkowości:
__Z6dblIncv: push ebp mov ebp, esp sub esp, 8 mov DWORD PTR [ebp-4], 1 lea eax, [ebp-4] inc DWORD PTR [eax] lea eax, [ebp-4] inc DWORD PTR [eax] mov eax, DWORD PTR [ebp-4] add eax, DWORD PTR [ebp-4] mov DWORD PTR [ebp-8], eax mov eax, DWORD PTR [ebp-8] leave ret
Zwróć uwagę, że zmienna lokalna
i
znajduje się na stosie tylko w jednym miejscu: adres[ebp-4]
. Ta lokalizacja jest zwiększana dwukrotnie (w wierszach od 5 do 8 funkcji asemblera; w tym pozornie nadmiarowe ładunki tego adresu doeax
). Następnie w wierszach od 9 do 10 ta wartość jest ładowanaeax
, a następnie dodawanaeax
(to znaczy oblicza prądi + i
). Następnie jest kopiowany do stosu i z powrotem doeax
jako wartość zwracana (która oczywiście będzie równa 6).Warto przyjrzeć się normie C ++ (tutaj, starej: ISO / IEC 14882: 1998 (E)), która mówi o wyrażeniach, sekcja 5.4:
Z przypisem:
W tym miejscu podano dwa przykłady nieokreślonego zachowania, oba obejmujące operator inkrementacji (jednym z nich jest
i = ++i + 1
:).Teraz, gdyby ktoś chciał, można: Utworzyć klasę opakowującą liczbę całkowitą (taką jak Java Integer); funkcje przeciążeniowe
operator+
ioperator++
zwracają obiekty wartości pośredniej; a tym samym napisz++iObj + ++iObj
i spraw, aby zwrócił obiekt zawierający 5. (nie włączyłem tutaj pełnego kodu ze względu na zwięzłość).Osobiście byłbym zaintrygowany, gdyby istniał przykład dobrze znanego kompilatora, który wykonał zadanie w inny sposób niż sekwencja pokazana powyżej. Wydaje mi się, że najprostszą implementacją byłoby wykonanie dwóch asemblerowych kodów
inc
na typie pierwotnym przed wykonaniem operacji dodawania.źródło
Rozsądną rzeczą, jaką może zrobić kompilator, jest Common Subexpression Elimination. Jest to już powszechna optymalizacja w kompilatorach: jeśli podwyrażenie, takie jak
(x+1)
występuje więcej niż raz w większym wyrażeniu, musi zostać obliczone tylko raz. Npa/(x+1) + b*(x+1)
x+1
sub ekspresji może być obliczona tylko raz.Oczywiście kompilator musi wiedzieć, które wyrażenia podrzędne można w ten sposób zoptymalizować. Dzwonienie
rand()
dwa razy powinno dać dwie losowe liczby. Z tego powodu wywołania funkcji nieliniowe muszą być wyłączone z CSE. Jak zauważyłeś, nie ma reguły, która mówi, jak należy postępować z dwoma wystąpieniamii++
, więc nie ma powodu, aby wyłączać je z CSE.Wynik może rzeczywiście być
int x = ++i + ++i;
zoptymalizowany doint __cse = i++; int x = __cse << 1
. (CSE, po którym następuje wielokrotne zmniejszenie wytrzymałości)źródło
W praktyce wywołujesz niezdefiniowane zachowanie. Wszystko może się zdarzyć, nie tylko rzeczy, które uważasz za „rozsądne” i często rzeczy zrobić zdarzyć, że nie biorą pod uwagę uzasadnione. Wszystko jest z definicji „rozsądne”.
Bardzo rozsądną kompilacją jest to, że kompilator zauważa, że wykonanie instrukcji wywoła niezdefiniowane zachowanie, dlatego instrukcja nie może zostać wykonana, dlatego jest tłumaczona na instrukcję, która celowo powoduje zawieszenie aplikacji. To bardzo rozsądne.
Downvoter: GCC zdecydowanie się z tobą nie zgadza.
źródło
Nie ma uzasadnione rzeczy, którą kompilator mógłby zrobić, aby uzyskać wynik 6, ale jest to możliwe i uzasadnione. Wynik 4 jest całkowicie rozsądny, a wynik 5 na granicy uważam za rozsądny. Wszystkie są całkowicie legalne.
Hej, czekaj! Czy nie jest jasne, co musi się stać? Dodawanie wymaga wyników dwóch przyrostów, więc oczywiście musi to nastąpić najpierw. Idziemy od lewej do prawej, więc ... argh! Gdyby to było takie proste. Niestety tak nie jest. Mamy nie idź w lewo w prawo, i to jest problem.
Odczytanie lokalizacji pamięci do dwóch rejestrów (lub zainicjowanie ich obu z tego samego literału, optymalizacja podróży w obie strony do pamięci) jest bardzo rozsądną rzeczą dla kompilatora. Skutkuje to w efekcie potajemnie występowaniem dwóch różnych zmiennych, z których każda ma wartość 2, które ostatecznie zostaną dodane do wyniku 4. Jest to „rozsądne”, ponieważ jest szybkie i wydajne oraz zgodne z obydwoma standard i kod.
Podobnie, lokalizacja pamięci może być odczytana raz (lub zmienna zainicjowana z literału) i raz inkrementowana, a kopia w tle w innym rejestrze mogłaby zostać zwiększona po tym, co spowodowałoby dodanie 2 i 3. Jest to, powiedziałbym, rozsądne granice , chociaż całkowicie legalne. Uważam to za rozsądne, ponieważ nie jest to ani jedno, ani drugie. Nie jest to ani „rozsądny” zoptymalizowany sposób, ani też „rozsądny” dokładnie pedantyczny sposób. Jest trochę pośrodku.
Dwukrotne zwiększenie lokalizacji pamięci (w wyniku czego uzyskuje się wartość 3), a następnie dodanie tej wartości do siebie w celu uzyskania wyniku końcowego równego 6 jest uzasadnione, ale nie całkiem rozsądne, ponieważ wykonywanie podróży w obie strony pamięci nie jest dokładnie wydajne. Chociaż na procesorze z dobrym przekazywaniem do magazynu, równie dobrze byłoby to zrobić, ponieważ sklep powinien być w większości niewidoczny ...
Ponieważ kompilator „wie”, że jest to ta sama lokalizacja, równie dobrze może zdecydować się na zwiększenie wartość dwukrotnie w rejestrze, a następnie dodaj ją również do siebie. Każde podejście dałoby wynik 6.
Kompilator może, zgodnie z brzmieniem normy, dać ci taki wynik, chociaż osobiście uważałbym 6 za notatkę "pieprzyć cię" z Wydziału Wstrętnego, ponieważ jest to raczej nieoczekiwana rzecz (legalna lub nie, staranie się, aby zawsze sprawiać jak najmniej niespodzianek, to dobra rzecz!). Chociaż, widząc, jak w grę wchodzi Undefined Behavior, nie można niestety spierać się o „nieoczekiwane”, eh.
Więc właściwie jaki jest kod, który tam masz, dla kompilatora? Zapytajmy clang, który pokaże nam, czy ładnie poprosimy (wywołując
-ast-dump -fsyntax-only
):ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced] int x = ++i + ++i; ^ ~~ (some lines omitted) `-CompoundStmt 0x2b3e628 <line:2:1, line:5:1> |-DeclStmt 0x2b3e4b8 <line:3:1, col:10> | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit | `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1 `-DeclStmt 0x2b3e610 <line:4:1, col:18> `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+' |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue> | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++' | `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int' `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue> `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++' `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'
Jak widać, to samo
lvalue Var 0x2b3e430
ma prefiks++
zastosowany w dwóch lokalizacjach, a te dwa znajdują się poniżej tego samego węzła w drzewie, co jest bardzo nietypowym operatorem (+), o którym nie mówi się nic specjalnego o sekwencjonowaniu. Dlaczego to jest ważne? Cóż, czytaj dalej.Zwróć uwagę na ostrzeżenie: „wielokrotne niepisane modyfikacje 'i'” . Och, to nie brzmi dobrze. Co to znaczy? [basic.exec] mówi nam o skutkach ubocznych i sekwencjonowaniu oraz mówi nam (paragraf 10), że domyślnie, o ile wyraźnie nie zaznaczono inaczej, oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń nie są sekwencjonowane . Cóż, cholera, tak jest w przypadku
operator+
- nic nie jest powiedziane inaczej, więc ...Ale czy obchodzi nas zsekwencjonowanie przed, nieokreślone, czy nie zsekwencjonowane? Kto w ogóle chce wiedzieć?
Ten sam akapit mówi nam również, że oceny bez kolejności mogą się pokrywać i że kiedy odnoszą się do tej samej lokalizacji pamięci (tak jest!) I nie są potencjalnie współbieżne, to zachowanie jest niezdefiniowane. Tutaj robi się naprawdę brzydko, ponieważ oznacza to, że nic nie wiesz i nie masz żadnych gwarancji, że będziesz „rozsądny”. Nierozsądna rzecz jest w rzeczywistości całkowicie dopuszczalna i „rozsądna”.
źródło
Obowiązuje zasada :
Zatem nawet x = 100 jest możliwym ważnym wynikiem.
Dla mnie najbardziej logicznym wynikiem w przykładzie jest 6, ponieważ dwukrotnie zwiększamy wartość i, a oni dodają ją do siebie. Trudno jest dodać wartości przed obliczeniami z obu stron „+”.
Jednak programiści kompilatorów mogą zaimplementować dowolną inną logikę.
źródło
Wygląda na to, że ++ i zwraca lwartość, ale i ++ zwraca wartość r.
Więc ten kod jest w porządku:
int i = 1; ++i = 10; cout << i << endl;
To nie jest:
int i = 1; i++ = 10; cout << i << endl;
Powyższe dwie instrukcje są zgodne z VisualC ++, GCC7.1.1, CLang i Embarcadero.
Dlatego twój kod w VisualC ++ i GCC7.1.1 jest podobny do następującego
int i = 1; ... do something there for instance: ++i; ++i; ... int x = i + i;
Patrząc na demontaż, najpierw inkrementuje i, przepisuje i. Kiedy próbujesz dodać, robi to samo, zwiększa i i przepisuje. Następnie dodaje i do i. Zauważyłem, że CLang i Embarcadero działają inaczej. Czyli nie jest zgodne z pierwszą instrukcją, po pierwszym ++ i zapisuje wynik w wartości r, a następnie dodaje go do drugiego i ++.
źródło
Osobiście nigdy nie spodziewałbym się, że kompilator w Twoim przykładzie wyświetli 6. Na Twoje pytanie są już dobre i szczegółowe odpowiedzi. Spróbuję krótkiej wersji.
Zasadniczo
++i
jest to proces dwuetapowy w tym kontekście:i
i
W kontekście
++i + ++i
dwóch stron dodatek może być oceniany w dowolnej kolejności zgodnie z normą. Oznacza to, że te dwa przyrosty są uważane za niezależne. Nie ma też zależności między tymi dwoma terminami. W związku z tym przyrost i odczyti
mogą być przeplatane. To daje potencjalny porządek:i
dla lewego operandui
dla właściwego operandui
lewy operandi
dla odpowiedniego operanduTeraz, kiedy o tym myślę, 6 ma największy sens według normy. Dla wyniku 4 potrzebujemy CPU, który najpierw odczytuje
i
niezależnie, a następnie zwiększa i zapisuje wartość z powrotem w tym samym miejscu; w zasadzie stan wyścigu. Dla wartości 5 potrzebujemy kompilatora, który wprowadza tymczasowe.Ale standard mówi, że
++i
zwiększa zmienną przed jej zwróceniem, tj. Przed faktycznym wykonaniem bieżącej linii kodu. Operator sum+
musi sumowaći + i
po zastosowaniu przyrostów. Powiedziałbym, że C ++ musi pracować na zmiennych, a nie na semantyce wartości. Dlatego według mnie 6 ma teraz największy sens, ponieważ opiera się na semantyce języka, a nie na modelu wykonawczym procesorów.źródło
#include <stdio.h> void a1(void) { int i = 1; int x = ++i; printf("i=%d\n",i); printf("x=%d\n",x); x = x + ++i; // Here printf("i=%d\n",i); printf("x=%d\n",x); } void b2(void) { int i = 1; int x = ++i; printf("i=%d\n",i); printf("x=%d\n",x); x = i + ++i; // Here printf("i=%d\n",i); printf("x=%d\n",x); } void main(void) { a1(); // b2(); }
źródło
cóż, zależy to od projektu kompilatora, dlatego odpowiedź będzie zależała od sposobu dekodowania instrukcji przez kompilator. Lepszym wyborem byłoby użycie dwóch różnych zmiennych ++ x i ++ y zamiast tego do stworzenia logiki. uwaga: wynik zależy od najnowszej wersji języka w ms Visual Studio, jeśli zostanie zaktualizowana, więc jeśli zasady uległy zmianie, wynik
źródło
Spróbuj tego
int i = 1; int i1 = i, i2 = i; // i1 = i2 = 1 ++i1; // i1 = 2 ++i2; // i2 = 2 int x = i1 + i2; // x = 4
źródło
Z tego linku kolejność oceny :
Z cytatów jasno wynika, że kolejność oceny nie jest określona przez standardy C. Różne kompilatory realizują różne porządki oceny. Kompilator może oceniać takie wyrażenia w dowolnej kolejności. Dlatego różne kompilatory podają różne dane wyjściowe dla wyrażenia wymienionego w pytaniu.
Ale jeśli punkt sekwencji jest obecny między podwyrażeniami Exp1 i Exp2, to zarówno obliczanie wartości, jak i efekty uboczne Exp1 są sekwencjonowane - przed każdym obliczeniem wartości i efektem ubocznym Exp2.
źródło
W praktyce wywołujesz niezdefiniowane zachowanie. Wszystko może się zdarzyć, nie tylko rzeczy, które uważasz za „rozsądne” i często rzeczy nie zdarzyć, że nie biorą pod uwagę uzasadnione. Wszystko jest z definicji „rozsądne”.
Bardzo rozsądną kompilacją jest to, że kompilator zauważa, że wykonanie instrukcji wywoła niezdefiniowane zachowanie, dlatego instrukcja nie może być nigdy wykonana, dlatego jest tłumaczona na instrukcję, która celowo powoduje zawieszenie aplikacji. To bardzo rozsądne. W końcu kompilator wie, że ta awaria nigdy się nie wydarzy.
źródło