W praktyce, dlaczego różne Kompilatory obliczają różne wartości int x = ++i + ++i;?

Rozważ ten kod:

int i = 1;
int x = ++i + ++i;

Mamy kilka domysłów co kompilator może zrobić dla tego kodu, zakładając, że kompiluje.

  1. both ++i return 2, co daje x=4.
  2. jeden ++i zwraca 2, a drugi zwraca 3, co daje x=5.
  3. both ++i return 3, co daje x=6.
Dla mnie druga wydaje się najbardziej prawdopodobna. Jeden z dwóch operatorów ++ jest wykonywany za pomocą i = 1, i jest inkrementowane, a wynik 2 jest zwracany. Następnie drugi operator ++ jest wykonywany z i = 2, i jest zwiększany, a wynik 3 jest zwracany. Następnie 2 i 3 są dodawane razem, aby dać 5.

Jednak uruchomiłem ten kod w Visual Studio, a rezultatem było 6. Staram się lepiej zrozumieć kompilatory i zastanawiam się, co może prowadzić do wyniku 6. Moim jedynym podejściem jest to, że kod może być wykonywany z jakąś" wbudowaną " współbieżnością. Wywołano dwa operatory ++, każdy zwiększony i przed drugim zwróconym, a następnie oba zwrócone 3. Byłoby to sprzeczne z moim zrozumieniem stosu połączeń i musiałoby zostać wyjaśnione.

Jakie (rozsądne) rzeczy mógłby zrobić C++ kompilator, który doprowadziłby do wyniku 4 lub wyniku lub 6?

Uwaga

Ten przykład pojawił się jako przykład niezdefiniowanego zachowania w programowaniu Bjarne Stroustrupa: zasady i praktyka korzystanie z C++ (C++ 14).

Zobaczkomentarz cynamona .

Author: Peter Mortensen, 2020-06-04

15 answers

Kompilator pobiera Twój kod, dzieli go na bardzo proste instrukcje, a następnie łączy je i układa w sposób, który uważa za 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 tak, jak ją napisałem, jest tylko kilka porządkujących zależności tutaj: 1->2->3->4->5->10->11 oraz 1->6->7->8->9->10->11 muszą pozostać w ich względnej kolejności. Poza tym kompilator może swobodnie zmienić kolejność i być może wyeliminować redundancję.

Na przykład, możesz zamówić listę w następujący 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, jest martwy magazyn w 4: wartość jest natychmiast nadpisana. Ponadto tmp2 i tmp4 to 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. A powtórka może być wyeliminowani też:

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
Ten kod jest znacznie krótszy. Optymalizator jest zadowolony. Programista nie jest, ponieważ byłem tylko raz. UPS.

Spójrzmy na coś innego, co kompilator może zrobić zamiast tego: 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

I jeszcze raz zauważ, że czytam dwa razy, 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

Now 4 is a dead store:

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

I 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 ostatnią tymczasową:

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

I teraz otrzymujesz wynik, który daje 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 za nic.

 200
Author: Sebastian Redl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-04 11:18:33

Chociaż jest to UB (jak sugeruje OP), poniżej przedstawiono hipotetyczne sposoby, w jakie kompilator może uzyskać 3 wyniki. Wszystkie trzy dałyby ten sam poprawny wynik x, jeśli są używane z różnymi zmiennymi int i = 1, j = 1; zamiast jednej i tej samej i.

  1. oba ++zwracam 2, co daje x = 4.
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. jeden ++i zwraca 2, a drugi zwraca 3, co daje x=5.
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. zarówno ++zwracam 3, co powoduje x=6.
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6
 59
Author: dxiv,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-05 17:19:19
Dla mnie druga wydaje się najbardziej prawdopodobna.

Wybieram opcję # 4: obie ++i zdarzają się jednocześnie.

Nowsze procesory idą w kierunku ciekawych optymalizacji, a równoległa ocena kodu, gdzie jest dozwolona, jest kolejnym sposobem kompilatorów na tworzenie szybszego kodu. Ja widzę jako praktyczną implementację Kompilatory idące w kierunku równoległości.

Mogłem łatwo zobaczyć stan wyścigu powodujący niedeterministyczne zachowanie lub awarię autobusu z powodu tego samego spór o pamięć - wszystko dozwolone, ponieważ koder naruszył umowę C++ - stąd UB.

Moje pytanie brzmi: jakie (rozsądne) rzeczy mógłby zrobić kompilator C++, który doprowadziłby do wyniku 4 lub wyniku 6?

To może , ale nie liczy się w nim.

Nie używaj ++i + ++i ani nie oczekuj sensownych rezultatów.

 22
Author: chux - Reinstate Monica,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-04 02:05:31

Myślę, że prosta i prosta interpretacja (bez żadnej oferty na optymalizację kompilatora lub wielowątkowość) byłaby po prostu:

  1. Increment i
  2. Increment i
  3. dodaj i + i

Z i zwiększoną dwukrotnie, jego wartość wynosi 3, a po zsumowaniu suma wynosi 6.

Dla kontroli, rozważ to jako funkcję C++:

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

Oto kod asemblera, który otrzymuję ze kompilacji tej funkcji, używając stara wersja kompilatora GNU C++ (win32, gcc w wersji 3.4.2 (MinGW-special)). Nie ma tu fantazyjnych 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

Zauważ, że zmienna lokalna i znajduje się na stosie w jednym miejscu: address [ebp-4]. Ta lokalizacja jest zwiększana dwukrotnie(w 5.-8. linii funkcji assembly; włączając pozornie nadmiarowe ładunki tego adresu do eax). Następnie w 9-tych-dziesiątych liniach wartość ta jest ładowana do eax, a następnie dodawana do eax (czyli oblicza prąd i + i). Następnie jest on redundantnie kopiowany do stosu i z powrotem do eax jako wartość zwracana (która oczywiście będzie równa 6).

[19]} może być interesujące przyjrzeć się standardowi C++ (tutaj Stary:ISO/IEC 14882: 1998 (E)), który mówi o wyrażeniach, sekcja 5.4:

Z wyjątkiem sytuacji, gdy zaznaczono, kolejność ocen poszczególnych operandów operatory i podekspresje poszczególnych wyrażeń, a kolejność w których skutki uboczne biorą miejsce, jest nieokreślony.

Z przypisem:

Pierwszeństwo operatorów nie jest bezpośrednio określone, ale może być pochodzi ze składni.

Dwa przykłady nieokreślonego zachowania są podane w tym momencie, oba z udziałem operatora przyrostu (z których jeden to: i = ++i + 1).

Teraz, gdyby ktoś chciał, można: utworzyć klasę owijania liczb całkowitych (jak Java Integer); funkcje przeciążające operator+ i operator++ takie, że zwracają obiekty wartości pośredniej; i tym samym napisać {[17] } i uzyskać go, aby zwrócić obiekt posiadający 5. (Nie zamieściłem tu pełnego kodu ze względu na zwięzłość.)

Osobiście byłbym zaintrygowany, gdyby był przykład znanego kompilatora, który wykonał to zadanie w inny sposób niż Sekwencja pokazana powyżej. Wydaje mi się, że najprostszą implementacją byłoby wykonanie dwóch asemblerów incna prymitywnym typie przed wykonaniem operacji dodawania.

 17
Author: Daniel R. Collins,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-05 19:02:24

Rozsądną rzeczą, jaką może zrobić kompilator, jest wspólna eliminacja Podekspresji. 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 być obliczone tylko raz. Np. w a/(x+1) + b*(x+1) pod-wyrażenie x+1 można obliczyć raz.

Oczywiście kompilator musi wiedzieć, które pod-wyrażenia można zoptymalizować w ten sposób. Wywołanie rand() dwa razy powinno dać dwie losowe liczby. Nieinlinowane wywołania funkcji muszą w związku z tym są zwolnione z CSE. Jak zauważyłeś, nie ma reguły, która mówi, jak dwa wystąpienia i++ powinny być obsługiwane, więc nie ma powodu, aby zwolnić je z CSE.

Wynik może być rzeczywiście taki, że int x = ++i + ++i; jest zoptymalizowany do int __cse = i++; int x = __cse << 1. (CSE, po którym następuje wielokrotna redukcja siły)

 7
Author: MSalters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-04 13:15:59

W praktyce wywołujesz nieokreślone zachowanie. Wszystko może się zdarzyć, nie tylko rzeczy, które uważasz za "rozsądne", i często rzeczy robią zdarzają się, które nie uważasz za rozsądne. Wszystko jest z definicji "rozsądne".

Bardzo rozsądną kompilacją jest to, że kompilator zauważa, że wywołanie instrukcji wywoła niezdefiniowane zachowanie, dlatego instrukcja nie może zostać wykonana, dlatego jest tłumaczona na instrukcję, która celowo zawiesza działanie podanie. To bardzo rozsądne.

Downvoter: GCC zdecydowanie się z tobą nie zgadza.

 7
Author: gnasher729,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-27 11:45:24

Nie ma rozsądnej 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 ja uważam wynik 5 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 {[7] } muszą one nastąpić najpierw. I idziemy od lewej do prawej, więc... argh!Gdyby to było takie proste. Niestety, to Nie w tej sprawie. Robimy a nie iść od lewej do prawej, i to jest problem.

Odczyt położenia pamięci w dwóch rejestrach (lub inicjalizacja obu z tego samego dosłownego, optymalizacja podróży w obie strony do pamięci) jest bardzo rozsądną rzeczą dla kompilatora. Efekt ten będzie skutkował ukryciem dwóch różnych zmiennych, każda o wartości 2, która ostatecznie zostanie dodana do wyniku 4. Jest to "rozsądne" , ponieważ jest szybkie i wydajne, i jest zgodny z zarówno standardem, jak i z kodem.

Podobnie, lokalizacja pamięci może być odczytana raz (lub zmienna zainicjalizowana z literału) i zwiększona raz, a Kopia cienia w innym rejestrze może być zwiększona później, co skutkowałoby dodaniem 2 i 3 razem. To jest, powiedziałbym, granica rozsądne, choć całkowicie legalne. Uważam to za rozsądne, ponieważ nie jest to jedno lub drugie. To nie jest "rozsądny" sposób zoptymalizowany, ani nie jest to" rozsądny " dokładnie-pedantyczny sposób. Jest trochę pośrodku.

Zwiększenie lokalizacji pamięci dwukrotnie (co daje wartość 3), a następnie dodanie tej wartości do siebie w wyniku końcowym 6 jest uzasadnione, ale nie całkiem rozsądne, ponieważ robienie podróży pamięciowych nie jest dokładnie efektywne. Chociaż na procesorze z dobrym przekierowaniem sklepu, równie dobrze może to być "rozsądne", ponieważ sklep powinien być w większości niewidoczny...
Jako kompilator "wie", że to ta sama lokalizacja, równie dobrze może zwiększyć wartość dwukrotnie w rejestrze, a następnie dodać ją do siebie. Oba podejścia dałyby wynik 6.

Kompilator, zgodnie z brzmieniem standardu, może dać ci taki wynik, chociaż osobiście uznałbym 6 za notkę "fuck you" z wstrętnego działu, ponieważ jest to raczej nieoczekiwana rzecz (legalna lub nie, starając się zawsze dawać jak najmniej niespodzianek to dobra rzecz!). Chociaż, widząc, jak nieokreślone zachowanie jest zaangażowane, niestety nie można naprawdę spierać się o "nieoczekiwane", eh.

Więc, właściwie, jaki jest kod, który masz tam, do kompilatora? Zapytajmy clang, który pokaże nam, jeśli ładnie poprosimy (powołując się na -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ć, ten sam lvalue Var 0x2b3e430 ma prefiks ++ stosowany w dwóch miejscach, a te dwa znajdują się poniżej tego samego węzła w drzewie, co jest bardzo Nie-specjalnym operatorem ( + ), który nie ma nic mówi się o sekwencjonowaniu. Dlaczego to jest ważne? Czytaj dalej.

Zwróć uwagę na Ostrzeżenie: "wielokrotne niezrównoważone modyfikacje "i" . To nie brzmi dobrze. Co to znaczy? [podstawowe.exec] mówi nam o efektach ubocznych i sekwencjonowaniu, i 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ń są niezekwencjonowane . Dobrze., / align = "center" bgcolor = "# e0ffe0 " / cesarz chin / / align = center / ..

Ale czy zależy nam na sekwencjonowaniu-przed, nieokreślonym-sekwencjonowaniu, czy niezekwencjonowaniu? Kto chce wiedzieć?

Ten sam akapit mówi nam również, że niezrównoważone oceny mogą nakładać się na siebie i że gdy odnoszą się do tego samego miejsca pamięci (tak jest!) i to nie jest potencjalnie równoległe, wtedy zachowanie jest nieokreślone. Tu robi się naprawdę brzydko, bo to oznacza, że nic nie wiesz i nie masz żadnych gwarancji co do bycia "rozsądnym". Nierozsądna rzecz jest właściwie całkowicie dopuszczalna i "rozsądna".

 6
Author: Damon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-05 18:38:24

Istnieje reguła :

Pomiędzy poprzednim i następnym punktem sekwencji obiekt skalarny musi mieć jego przechowywana wartość zmodyfikowana co najwyżej raz przez ocenę wyrażenie, w przeciwnym razie zachowanie jest nieokreślone.

Zatem nawet x = 100 jest możliwym poprawnym wynikiem.

Dla mnie najbardziej logiczny wynik w przykładzie to 6, ponieważ zwiększamy wartość i dwa razy i dodajemy ją do siebie. Trudno jest zrobić dodanie przed obliczeniami wartości z obu stron"+".

Ale programiści kompilatora mogą zaimplementować dowolną inną logikę.

 1
Author: Slavenskij,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-10 05:05:30

Wygląda na to, że ++i zwraca wartość lvalue, ale i++ zwraca wartość rvalue.
Więc ten kod jest ok:

int i = 1;
++i = 10;
cout << i << endl;

Ten nie jest:

int i = 1;
i++ = 10;
cout << i << endl;

Powyższe dwa wyrażenia 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;

Kiedy patrzymy na demontaż, najpierw inkrementuje i, przepisuje i. gdy próbuje dodać robi to samo, inkrementuje i i przepisuje go. Następnie dodaje i do i.
Tutaj wpisz opis obrazka
Zauważyłem, że CLang i Embarcadero zachowują się inaczej. Nie jest to więc zgodne z pierwszą instrukcją, po first ++i zapisuje wynik w wartości R, a następnie dodaje go do second i++.

 0
Author: armagedescu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-05 07:03:08

Osobiście nigdy bym się nie spodziewał, że kompilator wypowie 6 w twoim przykładzie. Są już dobre i szczegółowe odpowiedzi na twoje pytanie. Spróbuję krótkiej wersji.

Zasadniczo, {[0] } jest procesem dwuetapowym w tym kontekście:

  1. zwiększ wartość i
  2. odczytaj wartość i

W kontekście ++i + ++i obie strony dodawania mogą być oceniane W dowolnej kolejności zgodnie z normą. Oznacza to, że dwie przyrosty są uważany za niezależny. Ponadto nie ma zależności między tymi dwoma terminami. Przyrost i odczyt i mogą być zatem przeplatane. Daje to potencjalny porządek:

  1. przyrost i dla lewego operandu
  2. przyrost i dla prawego operanda
  3. Read back i for the left operand
  4. Read back i for the right operand
  5. suma dwóch: daje 6

Teraz, gdy myślę o tym, 6 ma największy sens według standard. Dla wyniku 4 potrzebujemy PROCESORA, który najpierw odczytuje i niezależnie, a następnie zwiększa i zapisuje wartość z powrotem w tym samym miejscu; w zasadzie warunek race. Dla wartości 5 potrzebujemy kompilatora, który wprowadza tymczasowe.

Ale standard mówi, że ++i zwiększa zmienną przed jej zwróceniem, tzn. przed rzeczywistym wykonaniem bieżącej linii kodu. Operator sumy + musi sumować i + i po zastosowaniu przyrostów. Powiedziałbym, że C++ musi działać na zmiennych, a nie na wartości semantycznej. Stąd dla mnie 6 ma teraz największy sens, ponieważ opiera się na semantyce języka, a nie na modelu wykonawczym procesorów.

 0
Author: Simon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-13 08:50:34
#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();
}
 0
Author: John Linq,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-15 01:58:25

Cóż to zależy od konstrukcji kompilatora.Dlatego odpowiedź będzie zależeć od sposobu dekodowania instrukcji przez kompilator.Używanie dwóch różnych zmiennych ++x I ++y zamiast stworzenie logiki byłoby lepszym wyborem. Uwaga:ouput zależy od wersji najnowszej wersji języka w ms visual studio, jeśli jego updated.So jeśli reguły się zmieniły, to wyjście

 0
Author: sam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-22 21:35:50

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
 0
Author: MAC27,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-27 19:58:53

Z tego linku Kolejność oceny :

Kolejność oceny operandów dowolnego operatora C, w tym kolejność oceny argumentów funkcji w wywołaniu funkcji wyrażeń, a kolejność oceny podwyrażeń w obrębie każde wyrażenie jest nieokreślone(z wyjątkiem przypadków zaznaczonych poniżej). kompilator oceni je w dowolnej kolejności i może wybrać inne zamówienie, gdy to samo wyrażenie jest obliczane ponownie .

From, the quotes, it is jasne, że kolejność oceny nie jest określona przez standardy C. Różne Kompilatory implementują różne rozkazy ewalauacji. Kompilator może dowolnie oceniać takie wyrażenia w dowolnej kolejności. Dlatego różne Kompilatory dają różne wyniki dla wyrażeń wymienionych w pytaniu.

Ale jeśli punkt sekwencji jest obecny pomiędzy podwyrażeniami Exp1 i Exp2, to zarówno obliczanie wartości, jak i efekty uboczne Exp1 są sekwencjonowane-przed każdym obliczaniem wartości i poboczem efekt Exp2.

 0
Author: Krishna Kanth Yenumula,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-12-23 09:04:06

W praktyce powołujesz się na nieokreślone zachowanie. Wszystko może się zdarzyć, nie tylko rzeczy, które uważasz za "rozsądne", i często rzeczy robią zdarzają się, które nie uważasz za rozsądne. Wszystko jest z definicji "rozsądne".

Bardzo rozsądną kompilacją jest to, że kompilator zauważa, że wywołanie instrukcji wywoła nieokreślone zachowanie, dlatego instrukcja nie może być kiedykolwiek wykonana, dlatego jest tłumaczona na instrukcję, która celowo ulega awarii Twoje podanie. To bardzo rozsądne. W końcu kompilator wie, że ta awaria nigdy się nie wydarzy.

 -4
Author: gnasher729,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-06 19:00:56