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”.