Co sprawiło, że i = i ++ + 1; legalny w C ++ 17?

186

Zanim zaczniesz krzyczeć niezdefiniowane zachowanie, jest to wyraźnie wymienione w N4659 (C ++ 17)

  i = i++ + 1;        // the value of i is incremented

Jeszcze w N3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

Co się zmieniło?

Z tego, co mogę zebrać, z [N4659 basic.exec]

O ile nie zaznaczono inaczej, oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń nie mają konsekwencji. [...] Obliczenia wartości operandów operatora są sekwencjonowane przed obliczeniem wartości wyniku operatora. Jeśli efekt uboczny w lokalizacji pamięci nie ma wpływu na inny efekt uboczny na tę samą lokalizację pamięci lub obliczenia wartości z wykorzystaniem wartości dowolnego obiektu w tej samej lokalizacji pamięci i nie są potencjalnie zbieżne, zachowanie jest niezdefiniowane.

Gdzie wartość jest zdefiniowana w [N4659 basic.type]

W przypadku typów, które można w prosty sposób skopiować, reprezentacja wartości jest zbiorem bitów w reprezentacji obiektu, który określa wartość , która jest jednym dyskretnym elementem zestawu wartości zdefiniowanego w implementacji

Z [N3337 basic.exec]

O ile nie zaznaczono inaczej, oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń nie mają konsekwencji. [...] Obliczenia wartości operandów operatora są sekwencjonowane przed obliczeniem wartości wyniku operatora. Jeśli efekt uboczny na obiekcie skalarnym nie ma wpływu na inny efekt uboczny na ten sam obiekt skalarny lub obliczenie wartości z wykorzystaniem wartości tego samego obiektu skalarnego, zachowanie jest niezdefiniowane.

Podobnie wartość jest zdefiniowana w [N3337 basic.type]

W przypadku typów, które można w prosty sposób skopiować, reprezentacja wartości jest zbiorem bitów w reprezentacji obiektu, który określa wartość , która jest jednym dyskretnym elementem zestawu wartości zdefiniowanego w implementacji.

Są identyczne, z wyjątkiem wzmianki o współbieżności, która nie ma znaczenia, oraz przy użyciu lokalizacji pamięci zamiast obiektu skalarnego , gdzie

Typy arytmetyczne, typy wyliczeń, typy wskaźników, typy wskaźników do typów elementów std::nullptr_toraz wersje tych typów kwalifikowane przez CV są wspólnie nazywane typami skalarnymi.

Co nie wpływa na przykład.

Od [N4659 expr.ass]

Operator przypisania (=) i operatory złożonego przypisania grupują wszystkie grupy od prawej do lewej. Wszystkie wymagają modyfikowalnej wartości jako lewego operandu i zwracają wartość odnoszącą się do lewego operandu. Wynik we wszystkich przypadkach jest polem bitowym, jeśli lewy operand jest polem bitowym. We wszystkich przypadkach przypisanie jest sekwencjonowane po obliczeniu wartości prawego i lewego operandu, a przed obliczeniem wartości wyrażenia przypisania. Prawy operand jest sekwencjonowany przed lewym operandem.

Od [N3337 expr.ass]

Operator przypisania (=) i operatory złożonego przypisania grupują wszystkie grupy od prawej do lewej. Wszystkie wymagają modyfikowalnej wartości jako lewego operandu i zwracają wartość odnoszącą się do lewego operandu. Wynik we wszystkich przypadkach jest polem bitowym, jeśli lewy operand jest polem bitowym. We wszystkich przypadkach przypisanie jest sekwencjonowane po obliczeniu wartości prawego i lewego operandu, a przed obliczeniem wartości wyrażenia przypisania.

Jedyną różnicą jest brak ostatniego zdania w N3337.

Ostatnie zdanie nie powinno mieć jednak żadnego znaczenia, ponieważ lewy operand inie jest ani „innym efektem ubocznym”, ani „wykorzystaniem wartości tego samego obiektu skalarnego”, ponieważ wyrażenie id jest wartością.

Przechodzień
źródło
23
Zidentyfikowałeś powód: W C ++ 17 prawy operand jest sekwencjonowany przed lewym operandem. W C ++ 11 nie było takiego sekwencjonowania. Jakie dokładnie jest twoje pytanie?
Robᵩ
4
@ Robᵩ Zobacz ostatnie zdanie.
Przechodzień Do
7
Czy ktoś ma link do motywacji tej zmiany? Chciałbym, aby analizator statyczny był w stanie powiedzieć „nie chcesz tego robić” w obliczu kodu podobnego i = i++ + 1;.
7
@NeilButterworth, pochodzi z artykułu p0145r3.pdf : „Udoskonalenie zlecenia oceny wyrażeń dla Idiomatic C ++”.
xaizek
9
@ NeilButterworth, sekcja 2 mówi, że jest to sprzeczne z intuicją i nawet eksperci nie postępują właściwie we wszystkich przypadkach. To właściwie cała ich motywacja.
xaizek,

Odpowiedzi:

144

W C ++ 11 czynność „przypisania”, tj. Efekt uboczny modyfikacji LHS, jest sekwencjonowana po obliczeniu wartości prawego operandu. Należy zauważyć, że jest to stosunkowo „słaba” gwarancja: powoduje sekwencjonowanie tylko w odniesieniu do obliczania wartości RHS. Nie mówi nic o skutkach ubocznych, które mogą występować w RHS, ponieważ występowanie skutków ubocznych nie jest częścią obliczania wartości . Wymagania C ++ 11 nie ustanawiają względnego sekwencjonowania między aktem przypisania a jakimikolwiek skutkami ubocznymi RHS. To właśnie tworzy potencjał dla UB.

Jedyną nadzieją w tym przypadku są wszelkie dodatkowe gwarancje udzielone przez określonych operatorów stosowanych w RHS. Gdyby RHS użył prefiksu ++, właściwości sekwencjonowania specyficzne dla formy prefiksu ++uratowałyby dzień w tym przykładzie. Ale postfiks ++to inna historia: nie daje takich gwarancji. W C ++ 11 skutki uboczne =i postfiks ++kończą się w tym przykładzie bez konsekwencji w stosunku do siebie. I to jest UB.

W C ++ 17 do specyfikacji operatora przypisania dodano dodatkowe zdanie:

Prawy operand jest sekwencjonowany przed lewym operandem.

W połączeniu z powyższym stanowi bardzo silną gwarancję. Sekwencjonuje wszystko , co dzieje się w RHS (w tym wszelkie skutki uboczne) przed wszystkim , co dzieje się w LHS. Ponieważ faktyczne przypisanie jest sekwencjonowane po LHS (i RHS), to dodatkowe sekwencjonowanie całkowicie izoluje akt przypisania od jakichkolwiek skutków ubocznych obecnych w RHS. To silniejsze sekwencjonowanie eliminuje powyższe UB.

(Zaktualizowano, aby uwzględnić komentarze @John Bollinger.)

Mrówka
źródło
3
Czy naprawdę poprawne jest uwzględnienie „faktycznego aktu przypisania” w efektach objętych „operandem lewej ręki” w tym fragmencie? Standard ma oddzielny język dotyczący sekwencji faktycznego przypisania. Przyjmuję, że fragment, który przedstawiłeś, jest ograniczony do sekwencjonowania podwyrażeń lewej i prawej ręki, co nie wydaje się wystarczające, w połączeniu z resztą tego rozdziału, do dobrego wsparcia - zdefiniowanie oświadczenia PO.
John Bollinger,
11
Korekta: rzeczywiste przypisanie jest nadal sekwencjonowane po obliczeniu wartości lewego operandu, a ocena lewego operandu jest sekwencjonowana po (pełnej) ocenie prawego operandu, więc tak, ta zmiana jest wystarczająca do obsługi dobrze zdefiniowanej OP zapytany o. W takim razie po prostu spieram szczegóły, ale mają one znaczenie, ponieważ mogą mieć różne implikacje dla różnych kodów.
John Bollinger,
3
@JohnBollinger: Ciekawe, że autorzy Standardu dokonaliby zmiany, która pogarsza efektywność nawet prostego generowania kodu i historycznie nie była konieczna, a jednak nie chce zdefiniować innych zachowań, których brak jest znacznie większym problemem, i które rzadko stanowiłyby jakąkolwiek znaczącą przeszkodę dla wydajności.
supercat,
1
@Kaz: W przypadku przypisań złożonych wykonywanie oceny wartości po lewej stronie po prawej stronie pozwala x -= y;na przetwarzanie czegoś takiego jak mov eax,[y] / sub [x],eaxzamiast mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Nie widzę w tym nic okresowego. Gdyby trzeba było określić uporządkowanie, najbardziej wydajnym uporządkowaniem byłoby prawdopodobnie wykonanie wszystkich obliczeń niezbędnych do identyfikacji obiektu po lewej stronie, a następnie oceny prawego operandu, a następnie wartości lewego obiektu, ale to wymagałoby posiadania terminu za czynność rozwiązania idiotyki lewego obiektu.
supercat
1
@Kaz: Jeśli xi ybyły volatile, które miałyby skutki uboczne. Co więcej, te same rozważania miałyby zastosowanie do x += f();, w przypadku f()modyfikacji x.
supercat
33

Zidentyfikowałeś nowe zdanie

Prawy operand jest sekwencjonowany przed lewym operandem.

i poprawnie zidentyfikowałeś, że ocena lewego operandu jako wartości jest nieistotna. Jednak sekwencjonowanie wcześniej określa się jako relację przechodnią. Zatem kompletny prawy operand (łącznie z przyrostem) jest sekwencjonowany również przed przypisaniem. W C ++ 11 tylko obliczenie wartości prawego operandu zostało zsekwencjonowane przed przypisaniem.


źródło
7

W starszych standardach C ++ i C11 definicja tekstu operatora przypisania kończy się na tekście:

Oceny operandów nie są konsekwentne.

Oznacza to, że skutki uboczne w operandach nie są konsekwentne, a zatem zdecydowanie niezdefiniowane zachowanie, jeśli używają tej samej zmiennej.

Ten tekst został po prostu usunięty w C ++ 11, pozostawiając go nieco niejednoznaczną. Czy to UB czy nie? Zostało to wyjaśnione w C ++ 17, gdzie dodali:

Prawy operand jest sekwencjonowany przed lewym operandem.


Na marginesie, nawet w starszych standardach wszystko to było bardzo jasne, przykład z C99:

Kolejność oceny operandów jest nieokreślona. W przypadku próby zmodyfikowania wyniku operatora przypisania lub uzyskania dostępu do niego po następnym punkcie sekwencji zachowanie jest niezdefiniowane.

Zasadniczo w C11 / C ++ 11 zawiedli, kiedy usunęli ten tekst.

Lundin
źródło
1

To są dalsze informacje do innych odpowiedzi i zamieszczam je, ponieważ często pytany jest również poniższy kod .

Wyjaśnienie w pozostałych odpowiedziach jest poprawne i dotyczy również następującego kodu, który jest teraz dobrze zdefiniowany (i nie zmienia przechowywanej wartości i):

i = i++;

+ 1Jest czerwony śledź i to naprawdę nie jest jasne, dlaczego standardowa używali go w swoich przykładach, chociaż ja pamietam ludzi argumentując na listach dyskusyjnych przed c ++ 11, że może + 1to różnicy z powodu zmuszania wcześnie lwartości konwersję na prawy- strona dłoni. Z pewnością nic z tego nie ma zastosowania w C ++ 17 (i prawdopodobnie nigdy nie miało zastosowania w żadnej wersji C ++).

MM
źródło