Dlaczego operator trójskładnikowy z przecinkami ocenia tylko jedno wyrażenie w prawdziwym przypadku?


119

Obecnie uczę się C ++ z książką C ++ Primer, a jedno z ćwiczeń w książce to:

Wyjaśnij, co robi to wyrażenie: someValue ? ++x, ++y : --x, --y

Co wiemy? Wiemy, że operator trójskładnikowy ma wyższy priorytet niż operator przecinka. W przypadku operatorów binarnych było to dość łatwe do zrozumienia, ale z operatorem trójskładnikowym trochę się zmagam. W przypadku operatorów binarnych „mający wyższy priorytet” oznacza, że ​​możemy używać nawiasów wokół wyrażenia o wyższym priorytecie i nie zmieni to wykonania.

Dla operatora trójskładnikowego zrobiłbym:

(someValue ? ++x, ++y : --x, --y)

w efekcie uzyskując ten sam kod, który nie pomaga mi w zrozumieniu, w jaki sposób kompilator pogrupuje kod.

Jednak z testów za pomocą kompilatora C ++ wiem, że wyrażenie kompiluje się i nie wiem, co :operator mógłby sam oznaczać. Tak więc kompilator wydaje się poprawnie interpretować operator trójskładnikowy.

Następnie wykonałem program na dwa sposoby:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Prowadzi do:

11 10

Z drugiej strony z someValue = falsenim drukuje:

9 9

Dlaczego kompilator C ++ miałby generować kod, który dla prawdziwej gałęzi operatora trójskładnikowego tylko zwiększa x, podczas gdy dla fałszywej gałęzi trójskładnikowej zmniejsza zarówno xi y?

Poszedłem nawet do umieszczenia nawiasów wokół prawdziwej gałęzi w następujący sposób:

someValue ? (++x, ++y) : --x, --y;

ale nadal skutkuje 11 10.


5
„Pierwszeństwo” to po prostu pojawiające się zjawisko w C ++. Prościej może być po prostu spojrzeć bezpośrednio na gramatykę języka i zobaczyć, jak działają wyrażenia.
Kerrek SB

26
Nie obchodzi nas , że dużo o zasadach. :-) Fakt, że musisz tutaj o to zapytać, oznacza, że ​​kod nigdy nie przejdzie przeglądu kodu przez innych programistów. To sprawia, że ​​wiedza o tym, jak to faktycznie działa, jest mniej niż przydatna. O ile oczywiście nie chcesz wziąć udziału w konkursie Obfuscated C.
Bo Persson

5
@BoPersson bez takich przykładów, z których można by się uczyć, przyszli recenzenci nigdy nie dowiedzą się, dlaczego powinni odrzucić to z produkcji.
Leushenko

8
@Leushenko - I tak powinny dzwonić dzwonki ostrzegawcze. Wielokrotne przyrosty i ubytki w tej samej instrukcji (ding, ding, ding!). Operator trójargumentowy, kiedy można użyć if-else (ding, ding, ding!). Czekaj, czy te przecinki są przerażającym operatorem przecinka? (ding, DING, DING!) Czy przy tych wszystkich operatorach może istnieć jakieś pierwszeństwo? (ding, ding, ding!) Więc nigdy nie będziemy w stanie tego użyć. Więc po co tracić czas na zastanawianie się, co to robi, jeśli cokolwiek?
Bo Persson

4
Drobna nitka: nazwa ?jest operatorem warunkowym . Termin operator trójskładnikowy oznacza po prostu operator z trzema argumentami. Operator warunkowy jest jednym z przykładów operatorów trójskładnikowych, ale język mógłby (teoretycznie) mieć wiele operatorów trójskładnikowych.
bta

Odpowiedzi:


122

Jak powiedział @Rakete w swojej doskonałej odpowiedzi, jest to trudne. Chciałbym do tego trochę dodać.

Operator trójskładnikowy musi mieć postać:

? wyrażenie przypisania wyrażenia logicznego lub : wyrażenia

Mamy więc następujące mapowania:

  • someValue: logiczne lub wyrażenie
  • ++x, ++y: wyrażenie
  • ??? jest wyrażenie przypisania --x, --y czy tylko --x?

W rzeczywistości dzieje się tak tylko --xdlatego, że wyrażenie przypisania nie może być analizowane jako dwa wyrażenia oddzielone przecinkiem (zgodnie z regułami gramatycznymi C ++), więc --x, --ynie może być traktowane jako wyrażenie przypisania .

Co powoduje, że część wyrażenia trójskładnikowego (warunkowego) wygląda następująco:

someValue?++x,++y:--x

Ze względu na czytelność pomocne może ++x,++ybyć rozważenie obliczenia w taki sposób, jakby był w nawiasach (++x,++y); wszystko, co znajduje się pomiędzy ?i :będzie sekwencjonowane po warunku. (Umieszczę je w nawiasach do końca postu).

i oceniane w tej kolejności:

  1. someValue?
  2. (++x,++y)lub --x(w zależności od boolwyniku 1.)

To wyrażenie jest następnie traktowane jako lewe wyrażenie podrzędne do operatora przecinka, przy czym prawe wyrażenie podrzędne jest następujące --y:

(someValue?(++x,++y):--x), --y;

Co oznacza, że ​​lewa strona jest wyrażeniem wartości odrzuconej , co oznacza, że ​​jest zdecydowanie oceniana, ale potem oceniamy prawą stronę i zwracamy ją.

Więc co się dzieje, kiedy someValuejest true?

  1. (someValue?(++x,++y):--x)wykonuje i zwiększa xoraz ybyć 11i11
  2. Lewe wyrażenie zostaje odrzucone (chociaż efekty uboczne przyrostu pozostają)
  3. Obliczamy prawą stronę operatora przecinka:, --yktóry następnie zmniejsza ysię z powrotem do10

Aby „naprawić” to zachowanie, możesz pogrupować --x, --yje w nawiasy, aby przekształcić je w wyrażenie podstawowe, które jest prawidłowym wpisem dla wyrażenia przypisania *:

someValue?++x,++y:(--x, --y);

* To dość zabawny długi łańcuch, który łączy wyrażenie przypisania z powrotem z wyrażeniem podstawowym:

wyrażenie-przypisania --- (może składać się z) -> wyrażenie-warunkowe -> wyrażenie - logiczne -> wyrażenie - logiczne -> wyrażenie - włączające -> wyrażenie - wyłączne - -> wyrażenie-i -> wyrażenie-równości -> wyrażenie-relacyjne -> wyrażenie-przesunięcia -> wyrażenie-addytywne -> wyrażenie-multiplikatywne -> wyrażenie-pm -> wyrażenie-rzutowane -> jednoargumentowe-wyrażenie -> postfiks-wyrażenie -> podstawowe-wyrażenie


10
Dziękuję za trud rozwikłania reguł gramatycznych; pokazuje to, że w gramatyce C ++ jest coś więcej niż w większości podręczników.
sdenham

4
@sdenham: Kiedy ludzie pytają, dlaczego „języki zorientowane na wyrażenia” są fajne (tj. kiedy { ... }można je traktować jako wyrażenie), mam teraz odpowiedź => chodzi o to, aby uniknąć konieczności wprowadzania operatora przecinka, który zachowuje się w tak podstępny sposób.
Matthieu M.

Czy możesz podać link do przeczytania o assignment-expressionłańcuchu?
MiP

@MiP: Podniosłem go z samego wzorca, można go znaleźć pod gramem. Wyrażenie
AndyG

88

Wow, to trudne.

Kompilator widzi twoje wyrażenie jako:

(someValue ? (++x, ++y) : --x), --y;

Operator trójskładnikowy potrzebuje a :, nie może stać sam w tym kontekście, ale po nim nie ma powodu, dla którego przecinek miałby należeć do fałszywego przypadku.

Teraz może mieć większy sens, dlaczego otrzymujesz takie wyjście. Jeśli someValueto prawda, to ++x, ++yi --yzostanie wykonany, które nie skutecznie zmienić y, ale dodaje jeden do x.

Jeśli someValuejest fałszywe, to --xi --ysą wykonywane, zmniejszając je o jeden.


42

Dlaczego kompilator C ++ miałby generować kod, który dla prawdziwej gałęzi operatora trójskładnikowego tylko zwiększa x

Źle zinterpretowałeś, co się stało. Prawdziwa gałąź zwiększa zarówno xi y. Jednak,y jest dekrementowany natychmiast po tym, bezwarunkowo.

Oto jak to się dzieje: ponieważ operator warunkowy ma wyższy priorytet niż operator przecinka w C ++ , kompilator analizuje wyrażenie w następujący sposób:

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Zwróć uwagę na słowo „osierocony” --ypo przecinku. To właśnie prowadzi do ubytku y, który został początkowo zwiększony.

Poszedłem nawet do umieszczenia nawiasów wokół prawdziwej gałęzi w następujący sposób:

someValue ? (++x, ++y) : --x, --y;

Byłeś na właściwej ścieżce, ale umieściłeś w nawiasach niewłaściwą gałąź: możesz to naprawić, umieszczając w nawiasach gałąź else, na przykład:

someValue ? ++x, ++y : (--x, --y);

Demo (wydruki 11 11)


5

Twoim problemem jest to, że trójskładnikowe wyrażenie nie ma tak naprawdę wyższego priorytetu niż przecinek. W rzeczywistości C ++ nie może być dokładnie opisany po prostu przez pierwszeństwo - i to właśnie interakcja między operatorem trójskładnikowym a przecinkiem jest przyczyną podziału.

a ? b++, c++ : d++

jest traktowany jako:

a ? (b++, c++) : d++

(przecinek zachowuje się tak, jakby miał wyższy priorytet). Z drugiej strony,

a ? b++ : c++, d++

jest traktowany jako:

(a ? b++ : c++), d++

a operator trójskładnikowy ma wyższy priorytet.


Myślę, że jest to nadal w sferze pierwszeństwa, ponieważ istnieje tylko jedna poprawna analiza środkowej linii, prawda? Wciąż pomocny przykład
sudo rm -rf slash

2

Punkt, który został przeoczony w odpowiedziach (choć został poruszony w komentarzach), to fakt, że operator warunkowy jest niezmiennie używany (zamierzony przez projekt?) W rzeczywistym kodzie jako skrót do przypisywania jednej z dwóch wartości do zmiennej.

Zatem szerszy kontekst wyglądałby tak:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Co jest absurdalne, więc zbrodnie są wielorakie:

  • Język dopuszcza absurdalne efekty uboczne w zadaniu.
  • Kompilator nie ostrzegł Cię, że robisz dziwne rzeczy.
  • Wydaje się, że książka skupia się na „podstępnych” pytaniach. Można mieć tylko nadzieję, że odpowiedź z tyłu brzmiała: „To, co robi to wyrażenie, zależy od dziwnych skrajnych przypadków w wymyślonym przykładzie, aby wywołać skutki uboczne, których nikt się nie spodziewa. Nigdy tego nie rób”.

1
Przypisanie jednej z dwóch zmiennych jest typowym przypadkiem operatora trójskładnikowego, ale zdarzają się sytuacje, w których warto mieć postać wyrażenia if(na przykład wyrażenie inkrementacji w pętli for). Większy kontekst może być for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))z pętlą, którą można modyfikować xi yniezależnie.
Martin Bonner wspiera Monikę

@MartinBonner Nie jestem przekonany, a ten przykład wydaje się całkiem dobrze oddawać punkt widzenia Bo-Perrsona, cytując Tony'ego Hoare'a.
Taryn
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.