Warum wertet der ternäre Operator mit Kommas im wahren Fall nur einen Ausdruck aus?

119

Ich lerne gerade C ++ mit dem Buch C ++ Primer und eine der Übungen im Buch ist:

Erklären Sie, was der folgende Ausdruck bewirkt: someValue ? ++x, ++y : --x, --y

Was wissen wir? Wir wissen, dass der ternäre Operator eine höhere Priorität hat als der Kommaoperator. Mit binären Operatoren war das ziemlich leicht zu verstehen, aber mit dem ternären Operator habe ich ein bisschen Probleme. Bei binären Operatoren bedeutet "höhere Priorität", dass wir Klammern um den Ausdruck mit höherer Priorität verwenden können und die Ausführung nicht geändert wird.

Für den ternären Operator würde ich tun:

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

Dies führt effektiv zu demselben Code, der mir nicht hilft zu verstehen, wie der Compiler den Code gruppiert.

Durch das Testen mit einem C ++ - Compiler weiß ich jedoch, dass der Ausdruck kompiliert wird und ich weiß nicht, wofür ein :Operator für sich stehen könnte. Der Compiler scheint den ternären Operator also richtig zu interpretieren.

Dann habe ich das Programm auf zwei Arten ausgeführt:

#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;
}

Ergebnisse in:

11 10

Während auf der anderen Seite someValue = falsedamit druckt:

9 9

Warum sollte der C ++ - Compiler Code generieren, der für den wahren Zweig des ternären Operators nur inkrementiert x, während er für den falschen Zweig des ternären Operators sowohl xals auch dekrementiert y?

Ich ging sogar so weit, den wahren Zweig wie folgt in Klammern zu setzen:

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

aber es ergibt sich immer noch 11 10.

Aufziehvogel
quelle
5
"Vorrang" ist nur ein auftauchendes Phänomen in C ++. Es kann einfacher sein, einfach die Sprachgrammatik direkt zu betrachten und zu sehen, wie Ausdrücke funktionieren.
Kerrek SB
26
Wir interessieren uns nicht , dass viel über die Prinzipien. :-) Die Tatsache, dass Sie dies hier fragen müssen, zeigt an, dass der Code niemals eine Codeüberprüfung durch Ihre Programmierkollegen bestehen wird. Das macht das Wissen darüber, wie dies tatsächlich funktioniert, weniger als nützlich. Es sei denn, Sie möchten am Obfuscated C Contest teilnehmen .
Bo Persson
5
@BoPersson ohne Beispiele wie diese, aus denen man lernen kann, werden zukünftige Rezensenten nie erfahren, warum sie dies aus der Produktion ablehnen sollten.
Leushenko
8
@Leushenko - Die Warnglocken sollten sowieso läuten. Mehrere Inkremente und Dekremente in derselben Anweisung (Ding, Ding, Ding!). Ein ternärer Operator, wenn Sie if-else verwenden könnten (Ding, Ding, Ding!). Warten Sie, sind diese Kommas der gefürchtete Kommaoperator? (ding, DING, DING!) Könnte es bei all diesen Operatoren einen Vorrang geben? (Ding, Ding, Ding!) Also werden wir das niemals benutzen können. Warum dann Zeit damit verschwenden, herauszufinden, was es tut, wenn überhaupt?
Bo Persson
4
Minor nit: Der Name für ?ist der bedingte Operator . Der Begriff ternärer Operator bedeutet einfach einen Operator mit drei Operanden. Der bedingte Operator ist ein Beispiel für einen ternären Operator, aber eine Sprache könnte (theoretisch) mehrere ternäre Operatoren haben.
bta

Antworten:

122

Wie @Rakete in ihrer ausgezeichneten Antwort sagte, ist dies schwierig. Ich möchte das noch etwas ergänzen.

Der ternäre Operator muss das folgende Formular haben:

logischer oder Ausdruck ? Ausdruck : Zuweisungsausdruck

Wir haben also folgende Zuordnungen:

  • someValue: logisch-oder-Ausdruck
  • ++x, ++y: Ausdruck
  • ??? ist Zuweisungsausdruck --x, --y oder nur --x?

In der Tat ist es nur --x daran, dass ein Zuweisungsausdruck nicht als zwei durch Komma getrennte Ausdrücke (gemäß den Grammatikregeln von C ++) analysiert werden --x, --ykann und daher nicht als Zuweisungsausdruck behandelt werden kann .

Was dazu führt, dass der ternäre (bedingte) Ausdrucksteil so aussieht:

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

Aus Gründen der Lesbarkeit kann es hilfreich ++x,++ysein, die Berechnung in Klammern zu betrachten(++x,++y) . Alles, was zwischen ?und enthalten ist, :wird nach der Bedingung sequenziert . (Ich werde sie für den Rest des Beitrags in Klammern setzen).

und in dieser Reihenfolge ausgewertet:

  1. someValue?
  2. (++x,++y) oder --x (abhängig vom boolErgebnis von 1.)

Dieser Ausdruck wird dann als linker Unterausdruck für einen Kommaoperator behandelt, wobei der rechte Unterausdruck lautet --y wie :

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

Was bedeutet, dass die linke Seite ein Ausdruck mit verworfenem Wert ist , was bedeutet, dass er definitiv ausgewertet wird, aber dann bewerten wir die rechte Seite und geben diesen zurück.

Also, was passiert wann someValueisttrue ?

  1. (someValue?(++x,++y):--x)führt aus und erhöht xundy zu sein 11und11
  2. Der linke Ausdruck wird verworfen (obwohl die Nebenwirkungen des Inkrements bestehen bleiben)
  3. Wir werten die rechte Seite des --yKommaoperators yaus :, der dann wieder auf dekrementiert10

Zu „reparieren“ das Verhalten, können Sie Gruppe --x, --ymit Klammern in einen verwandeln primären Ausdruck , die ist ein gültiger Eintrag für eine Zuordnung Ausdruck *:

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

* Es ist eine ziemlich lustige lange Kette, die einen Zuweisungsausdruck wieder mit einem primären Ausdruck verbindet:

Zuweisungsausdruck --- (kann bestehen aus) -> bedingter Ausdruck -> logisch-oder-Ausdruck -> logisch-und-Ausdruck -> inklusive-oder-Ausdruck -> exklusiv-oder-Ausdruck - -> und-Ausdruck -> Gleichheitsausdruck -> relationaler Ausdruck -> Verschiebungsausdruck -> additiver Ausdruck -> multiplikativer Ausdruck -> pm-Ausdruck -> Besetzungsausdruck -> unärer Ausdruck -> Postfix-Ausdruck -> primärer Ausdruck

AndyG
quelle
10
Vielen Dank, dass Sie sich die Mühe gemacht haben, die Grammatikregeln zu enträtseln. Dies zeigt, dass die Grammatik von C ++ mehr enthält, als Sie in den meisten Lehrbüchern finden.
Sdenham
4
@sdenham: Wenn Leute fragen, warum "ausdrucksorientierte Sprachen" nett sind (dh wann { ... }sie als Ausdruck behandelt werden können), habe ich jetzt eine Antwort => Es soll vermieden werden, einen Kommaoperator einzuführen, der sich so knifflig verhält.
Matthieu M.
Könnten Sie mir einen Link geben, um über die assignment-expressionKette zu lesen ?
MiP
@MiP: Ich habe es aus dem Standard selbst gehoben, Sie können es unter gram.expr
AndyG
88

Wow, das ist schwierig.

Der Compiler sieht Ihren Ausdruck wie folgt:

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

Der ternäre Operator benötigt a :, er kann in diesem Zusammenhang nicht für sich allein stehen, aber danach gibt es keinen Grund, warum das Komma zum falschen Fall gehören sollte.

Jetzt ist es vielleicht sinnvoller, warum Sie diese Ausgabe erhalten. Wenn someValuewahr ist, dann ++x, ++yund --yausgeführt werden sollen , die nicht effektiv ändern tut , ysondern fügt man x.

Wenn someValuefalsch ist, werden --xund --yausgeführt, wobei beide um eins dekrementiert werden.

Rakete1111
quelle
42

Warum sollte der C ++ - Compiler Code generieren, der für den True-Branch des ternären Operators nur inkrementiert wird? x

Sie haben falsch interpretiert, was passiert ist. Der True-Branch erhöht sowohl xals auch y. Jedoch,y wird erniedrigt unmittelbar danach, bedingungslos.

Dies geschieht folgendermaßen: Da der bedingte Operator in C ++ eine höhere Priorität als der Kommaoperator hat, analysiert der Compiler den Ausdruck wie folgt:

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

Beachten Sie das "verwaiste" --ynach dem Komma. Dies führt zu einer Dekrementierung y, die ursprünglich erhöht wurde.

Ich ging sogar so weit, den wahren Zweig wie folgt in Klammern zu setzen:

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

Sie waren auf dem richtigen Weg, haben aber einen falschen Zweig in Klammern gesetzt: Sie können dies beheben, indem Sie den else-Zweig wie folgt in Klammern setzen:

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

Demo (druckt 11 11)

dasblinkenlight
quelle
5

Ihr Problem ist, dass der ternäre Ausdruck nicht wirklich eine höhere Priorität als Komma hat. Tatsächlich kann C ++ nicht einfach durch Vorrang genau beschrieben werden - und es ist genau die Interaktion zwischen dem ternären Operator und dem Komma, in der es zusammenbricht.

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

wird behandelt als:

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

(Komma verhält sich so, als hätte es eine höhere Priorität). Andererseits,

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

wird behandelt als:

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

und der ternäre Operator hat eine höhere Priorität.

Martin Bonner unterstützt Monica
quelle
Ich denke, dies hat immer noch Vorrang, da es nur eine gültige Analyse für die Mittellinie gibt, oder? Trotzdem ein hilfreiches Beispiel
sudo rm -rf Schrägstrich
2

Ein Punkt, der in den Antworten übersehen wurde (obwohl er in Kommentaren angesprochen wurde), ist, dass der bedingte Operator im realen Code immer als Verknüpfung zum Zuweisen eines von zwei Werten zu einer Variablen verwendet wird (beabsichtigt durch Design?).

Der größere Kontext wäre also:

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

Was auf den ersten Blick absurd ist, so dass die Verbrechen vielfältig sind:

  • Die Sprache erlaubt lächerliche Nebenwirkungen in einer Aufgabe.
  • Der Compiler hat Sie nicht gewarnt, dass Sie bizarre Dinge tun.
  • Das Buch scheint sich auf Trickfragen zu konzentrieren. Man kann nur hoffen, dass die Antwort im Hintergrund lautete: "Was dieser Ausdruck bewirkt, hängt von seltsamen Randfällen in einem erfundenen Beispiel ab, um Nebenwirkungen hervorzurufen, die niemand erwartet. Tun Sie dies niemals."
Taryn
quelle
1
Das Zuweisen einer von zwei Variablen ist der übliche Fall des ternären Operators, aber es gibt Fälle, in denen es nützlich ist, eine Ausdrucksform von zu haben if(z. B. den Inkrementausdruck in einer for-Schleife). Der größere Kontext könnte durchaus for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))eine Schleife sein, die modifiziert xund yunabhängig voneinander sein kann.
Martin Bonner unterstützt Monica
@ MartinBonner Ich bin nicht überzeugt, und dieses Beispiel scheint Bo-Perrsons Argument ziemlich gut zu machen, wie er Tony Hoare zitierte.
Taryn