Jaka jest poprawna odpowiedź na cout << a ++ << a ;?

98

Niedawno w wywiadzie pojawiło się następujące pytanie typu obiektywnego.

int a = 0;
cout << a++ << a;

Odpowiedzi:

za. 10
b. 01
c. niezdefiniowane zachowanie

Odpowiedziałem na wybór b, tj. Wyjście będzie „01”.

Ale ku memu zdziwieniu później ankieter powiedział mi, że prawidłowa odpowiedź to opcja c: nieokreślona.

Teraz znam koncepcję punktów sekwencji w C ++. Zachowanie jest niezdefiniowane dla następującej instrukcji:

int i = 0;
i += i++ + i++;

ale zgodnie z moim zrozumieniem dla tego stwierdzenia cout << a++ << a, ostream.operator<<()zostanie wywołane dwukrotnie, najpierw z, ostream.operator<<(a++)a później ostream.operator<<(a).

Wynik sprawdziłem też na kompilatorze VS2010 i na wyjściu jest też '01'.

pravs
źródło
30
Czy poprosiłeś o wyjaśnienie? Często przeprowadzam rozmowy kwalifikacyjne z potencjalnymi kandydatami i jestem dość zainteresowany otrzymywaniem pytań, to pokazuje zainteresowanie.
Brady
3
@jrok To niezdefiniowane zachowanie. Wszystko, co robi implementacja (w tym wysyłanie obraźliwego e-maila w Twoim imieniu do szefa), jest zgodne.
James Kanze
2
To pytanie domaga się odpowiedzi w C ++ 11 ( aktualna wersja C ++), która nie wspomina o punktach sekwencji. Niestety nie mam wystarczającej wiedzy na temat zamiany punktów sekwencji w C ++ 11.
CB Bailey
3
Gdyby nie było nieokreślone, to na pewno nie mogłoby być 10, byłoby albo 01albo 00. ( c++będzie zawsze szacowana do wartości, która cmiała przed zwiększeniem). I nawet gdyby nie było nieokreślone, nadal byłoby strasznie zagmatwane.
lewej około
2
Wiesz, kiedy przeczytałem tytuł „cout << c ++ << c”, przez chwilę pomyślałem o nim jako o związku między językami C i C ++ i jakimś innym o nazwie „cout”. Wiesz, jakby ktoś mówił, jak sądził, że „cout” jest znacznie gorszy od C ++ i że C ++ jest znacznie gorszy od C - i prawdopodobnie przez przechodniość ten „cout” był bardzo, bardzo gorszy od C. :)
tchrist

Odpowiedzi:

145

Możesz myśleć o:

cout << a++ << a;

Tak jak:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++ gwarantuje, że wszystkie efekty uboczne poprzednich ocen zostaną wykonane w punktach sekwencji . Nie ma punktów sekwencji pomiędzy oceną argumentów funkcji, co oznacza, że ​​argument ten amoże być oceniany przed argumentem std::operator<<(std::cout, a++)lub po nim. Wynik powyższego jest więc nieokreślony.


Aktualizacja C ++ 17

W C ++ 17 zasady zostały zaktualizowane. W szczególności:

W wyrażeniu operatora przesunięcia E1<<E2i E1>>E2każde obliczenie wartości i efekt uboczny E1jest sekwencjonowane przed każdym obliczeniem wartości i efektem ubocznym E2.

Co oznacza, że ​​wymaga kodu do wygenerowania wyniku b, który generuje 01.

Więcej informacji można znaleźć w dokumencie P0145R3 Refining Expression Evaluation Order for Idiomatic C ++ .

Maxim Egorushkin
źródło
@Maxim: Dzięki za wyjaśnienie. Z wywołaniami, które otrzymałeś, byłoby to niezdefiniowane zachowanie. Ale teraz mam jeszcze jedno pytanie (może jedno siller, a brakuje mi czegoś podstawowego i głośno myślącego) Jak wywnioskowałeś, że globalna wersja std :: operator << () zostanie wywołana zamiast ostream :: operator < <() wersja członka. Podczas debugowania ląduję w wersji członkowskiej wywołania ostream :: operator << (), a nie w wersji globalnej i to jest powód, dla którego początkowo myślałem, że odpowiedź będzie brzmiała 01
pravs
@Maxim Nie to, że robi coś innego, ale ponieważ cma typ int, operator<<oto funkcje składowe .
James Kanze
2
@pravs: operator<<to, czy jest funkcją składową, czy wolnostojącą, nie wpływa na punkty sekwencji.
Maxim Egorushkin
11
„Punkt sekwencji” nie jest już używany w standardzie C ++. Było nieprecyzyjne i zostało zastąpione relacją „zsekwencjonowano przed / zsekwencjonowano po”.
Rafał Dowgird
2
So the result of the above is undefined.Twoje wyjaśnienie jest dobre tylko dla nieokreślonych , a nie dla nieokreślonych . James Kanze wyjaśnił jednak, że jego odpowiedź jest tym bardziej nieokreślona .
Deduplicator
68

Technicznie rzecz biorąc, ogólnie jest to niezdefiniowane zachowanie .

Ale są dwa ważne aspekty odpowiedzi.

Instrukcja kodu:

std::cout << a++ << a;

jest oceniany jako:

std::operator<<(std::operator<<(std::cout, a++), a);

Norma nie definiuje kolejności obliczania argumentów funkcji.
Więc albo:

  • std::operator<<(std::cout, a++) jest oceniany jako pierwszy lub
  • ajest oceniany jako pierwszy lub
  • może to być dowolna realizacja zdefiniowana w kolejności.

To zamówienie jest nieokreślone [Ref 1] zgodnie z normą.

[Odn. 1] C ++ 03 5.2.2 Wywołanie funkcji,
pkt 8

Kolejność oceny argumentów jest nieokreślona . Wszystkie skutki uboczne obliczeń wyrażeń argumentów obowiązują przed wprowadzeniem funkcji. Kolejność obliczania wyrażenia postfiksowego i listy wyrażeń argumentów jest nieokreślona.

Co więcej, nie ma punktu sekwencji między oceną argumentów funkcji, ale punkt sekwencji istnieje tylko po ocenie wszystkich argumentów [Ref 2] .

[Odn. 2] C ++ 03 1.9 Wykonanie programu [wprowadzenie.wykonanie]: Par.
17:

Podczas wywoływania funkcji (niezależnie od tego, czy funkcja jest wbudowana, czy nie), po ocenie wszystkich argumentów funkcji (jeśli istnieją) występuje punkt sekwencji, który ma miejsce przed wykonaniem jakichkolwiek wyrażeń lub instrukcji w treści funkcji.

Zauważ, że tutaj wartość cjest dostępna więcej niż jeden raz bez interweniującego punktu sekwencji, w związku z tym standard mówi:

[Odn. 3] C ++ 03 5 Wyrażenia [wyraż]:
par. 4:

....
Pomiędzy poprzednim a następnym punktem sekwencji obiekt skalarny będzie miał swoją przechowywaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia. Ponadto dostęp do wcześniejszej wartości jest możliwy tylko w celu określenia wartości, która ma być przechowywana . Wymagania niniejszego punktu muszą być spełnione dla każdego dopuszczalnego uporządkowania podwyrażeń pełnego wyrażenia; w przeciwnym razie zachowanie jest niezdefiniowane .

Kod modyfikuje cwięcej niż jeden raz bez interwencji punktu sekwencji i nie jest uzyskiwany dostęp do niego w celu określenia wartości przechowywanego obiektu. Jest to wyraźne naruszenie powyższej klauzuli, a zatem wynik zgodnie z zaleceniami normy to Undefined Behavior [Ref 3] .

Alok Save
źródło
1
Technicznie rzecz biorąc, zachowanie jest niezdefiniowane, ponieważ następuje modyfikacja obiektu i uzyskiwanie do niego dostępu w innym miejscu bez interwencji punktu sekwencji. Niezdefiniowany nie jest nieokreślony; pozostawia to jeszcze większe pole manewru wdrożeniu.
James Kanze
1
@Als Yes. Nie widziałem twoich zmian (chociaż reagowałem na stwierdzenie jroka, że ​​program nie może zrobić czegoś dziwnego - może). Twoja zredagowana wersja jest dobra, ale moim zdaniem kluczowym słowem jest częściowe uporządkowanie ; punkty sekwencji wprowadzają tylko częściowe uporządkowanie.
James Kanze
1
@ Dzięki za obszerny opis, naprawdę bardzo pomocny !!
pravs
4
Nowy standard C ++ 0x mówi zasadniczo to samo, ale w różnych sekcjach i w innym brzmieniu :) Cytat: (1.9 Wykonanie programu [intro.execution], par. 15): „Jeśli efekt uboczny na obiekcie skalarnym nie ma kolejności albo inny efekt uboczny tego samego obiektu skalarnego, albo obliczenie wartości przy użyciu wartości tego samego obiektu skalarnego, zachowanie jest nieokreślone. "
Rafał Dowgird
2
Uważam, że w tej odpowiedzi jest błąd. "std :: cout << c ++ << c;" nie można przetłumaczyć na „std :: operator << (std :: operator << (std :: cout, c ++), c)”, ponieważ std :: operator << (std :: ostream &, int) nie istnieje. Zamiast tego tłumaczy się na "std :: cout.operator << (c ++). Operator (c);", który w rzeczywistości ma punkt sekwencji między oceną "c ++" i "c" (przeciążony operator jest uważany za wywołanie funkcji i dlatego istnieje punkt sekwencji, gdy wywołanie funkcji zwraca). W związku z tym zachowanie i realizacji zamówienia jest określony.
Christopher Smith,
20

Punkty sekwencji definiują tylko częściowe uporządkowanie. W twoim przypadku masz (po rozwiązaniu problemu):

std::cout.operator<<( a++ ).operator<<( a );

Istnieje punkt sekwencji między a++pierwszym wywołaniem std::ostream::operator<<a i punktem sekwencji między drugim aa drugim wywołaniem std::ostream::operator<<, ale nie ma punktu sekwencji między a++a a; jedynymi ograniczeniami porządkowania są te, które a++są w pełni oceniane (w tym skutki uboczne) przed pierwszym wywołaniem operator<<, a drugie w apełni oceniane przed drugim wywołaniem operator<<. (Istnieją również przyczynowe ograniczenia porządku: drugie wywołanie operator<<nie może poprzedzać pierwszego, ponieważ wymaga jako argumentu wyników pierwszego). § 5/4 (C ++ 03) stwierdza:

O ile nie zaznaczono, kolejność oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń oraz kolejność, w jakiej występują skutki uboczne, jest nieokreślona. Pomiędzy poprzednim a następnym punktem sekwencji obiekt skalarny będzie miał swoją przechowywaną wartość zmodyfikowaną co najmniej raz przez ocenę wyrażenia. Ponadto dostęp do wcześniejszej wartości jest możliwy tylko w celu określenia wartości, która ma być przechowywana. Wymagania niniejszego punktu muszą być spełnione dla każdego dopuszczalnego uporządkowania podwyrażeń pełnego wyrażenia; w przeciwnym razie zachowanie jest niezdefiniowane.

Jedną z dopuszczalnych porządków twojej wypowiedzi jest a++, apo pierwsze wywołanie operator<<, drugie wywołanie operator<<; to modyfikuje przechowywaną wartość a( a++) i uzyskuje do niej dostęp w inny sposób niż w celu określenia nowej wartości (drugiej a), zachowanie jest niezdefiniowane.

James Kanze
źródło
Jeden haczyk z Twojej wyceny standardu. „Z wyjątkiem miejsca, w którym zaznaczono”, IIRC, zawiera wyjątek, gdy mamy do czynienia z przeciążonym operatorem, który traktuje operator jako funkcję i dlatego tworzy punkt sekwencji między pierwszym a drugim wywołaniem std :: ostream :: operator << (int ). Proszę, popraw mnie jeśli się mylę.
Christopher Smith
@ChristopherSmith Przeciążony operator zachowuje się jak wywołanie funkcji. Jeśli byłby ctyp użytkownika ze zdefiniowanym przez użytkownika ++, zamiast tego int, wyniki byłyby nieokreślone, ale nie byłoby niezdefiniowanego zachowania.
James Kanze
1
@ChristopherSmith Gdzie widzisz punkt sekwencji między tymi dwoma cw foo(foo(bar(c)), c)? Istnieje punkt sekwencji, gdy wywoływane są funkcje i kiedy zwracają, ale nie jest wymagane wywołanie funkcji między wartościami tych dwóch c.
James Kanze
1
@ChristopherSmith Gdyby cbył to UDT, przeciążone operatory byłyby wywołaniami funkcji i wprowadzałyby punkt sekwencji, więc zachowanie nie byłoby niezdefiniowane. Ale nadal byłoby nieokreślone, czy wyrażenie podrzędne czostało ocenione przed, czy po c++, więc to, czy otrzymałeś wersję zwiększoną, czy nie, nie byłoby określone (i teoretycznie nie musiałoby być takie samo za każdym razem).
James Kanze
1
@ChristopherSmith Wszystko przed punktem sekwencji nastąpi przed czymkolwiek po punkcie sekwencji. Ale punkty sekwencji definiują tylko częściowe uporządkowanie. W wyrażeniu mowa, na przykład, nie ma sensu sekwencji między podsystemami ekspresji ci c++, zatem dwa mogą występować w dowolnej kolejności. Jeśli chodzi o średniki ... Powodują punkt sekwencji tylko wtedy, gdy są pełnymi wyrażeniami. Innymi ważnymi punktami sekwencji są wywołanie funkcji: f(c++)zobaczy zwiększony cin fi operator przecinka &&, ||a ?:także spowoduje punkty sekwencji.
James Kanze
4

Prawidłowa odpowiedź to zadać pytanie. To stwierdzenie jest nie do przyjęcia, ponieważ czytelnik nie widzi jasnej odpowiedzi. Innym sposobem spojrzenia na to jest to, że wprowadziliśmy efekty uboczne (c ++), które znacznie utrudniają interpretację instrukcji. Zwięzły kod jest świetny, pod warunkiem, że ma jasne znaczenie.

Paul Marrington
źródło
4
Pytanie może wykazywać słabą praktykę programowania (a nawet nieprawidłowy C ++). Ale odpowiedź ma odpowiedzieć na pytanie, co jest nie tak i dlaczego jest nie tak. Komentarz do pytania nie jest odpowiedzią, nawet jeśli jest całkowicie uzasadniony. W najlepszym przypadku może to być komentarz, a nie odpowiedź.
PP,