Was ist die richtige Antwort für cout << a ++ << a;?

98

Kürzlich gab es in einem Interview eine folgende objektive Frage.

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

Antworten:

ein. 10
b. 01
c. undefiniertes Verhalten

Ich antwortete auf Wahl b, dh die Ausgabe wäre "01".

Zu meiner Überraschung wurde mir später von einem Interviewer gesagt, dass die richtige Antwort Option c: undefiniert ist.

Jetzt kenne ich das Konzept der Sequenzpunkte in C ++. Das Verhalten ist für die folgende Anweisung undefiniert:

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

aber nach meinem für die Aussage zu verstehen cout << a++ << a, das ostream.operator<<()würde zweimal aufgerufen werden, zuerst mit ostream.operator<<(a++)und später ostream.operator<<(a).

Ich habe auch das Ergebnis auf dem VS2010-Compiler überprüft und seine Ausgabe ist ebenfalls '01'.

Pravs
quelle
30
Hast du um eine Erklärung gebeten? Ich interviewe oft potenzielle Kandidaten und bin sehr daran interessiert, Fragen zu erhalten, es zeigt Interesse.
Brady
3
@jrok Es ist undefiniertes Verhalten. Alles, was die Implementierung tut (einschließlich des Versendens einer beleidigenden E-Mail in Ihrem Namen an Ihren Chef), ist konform.
James Kanze
2
Diese Frage verlangt nach einer Antwort auf C ++ 11 (die aktuelle Version von C ++), in der keine Sequenzpunkte erwähnt werden. Leider kenne ich mich nicht genug mit dem Ersetzen von Sequenzpunkten in C ++ 11 aus.
CB Bailey
3
Wenn es nicht undefiniert wäre, könnte es definitiv nicht sein 10, es wäre entweder 01oder 00. ( c++Wertet immer auf den Wert chatte , bevor erhöht wird). Und selbst wenn es nicht undefiniert wäre, wäre es immer noch schrecklich verwirrend.
links um
2
Weißt du, als ich den Titel "cout << c ++ << c" las, dachte ich momentan an eine Aussage über die Beziehung zwischen den Sprachen C und C ++ und an eine andere mit dem Namen "cout". Sie wissen, als hätte jemand gesagt, dass er dachte, dass "cout" C ++ viel unterlegen sei und dass C ++ C viel unterlegen sei - und wahrscheinlich aufgrund der Transitivität, dass "cout" C sehr, sehr viel unterlegen sei :) :)
tchrist

Antworten:

145

Sie können sich vorstellen:

cout << a++ << a;

Wie:

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

C ++ garantiert, dass alle Nebenwirkungen früherer Bewertungen an Sequenzpunkten durchgeführt wurden . Es gibt keine Sequenzpunkte zwischen der Auswertung von Funktionsargumenten, was bedeutet, dass das Argument avor std::operator<<(std::cout, a++)oder nach dem Argument ausgewertet werden kann . Das Ergebnis des oben Gesagten ist also undefiniert.


C ++ 17 Update

In C ++ 17 wurden die Regeln aktualisiert. Bestimmtes:

In einem Shift-Operator-Ausdruck E1<<E2und E1>>E2wird jede Wertberechnung und Nebenwirkung von E1vor jeder Wertberechnung und Nebenwirkung von sequenziert E2.

Dies bedeutet, dass der Code ein Ergebnis erzeugen muss b, das ausgegeben wird 01.

Weitere Informationen finden Sie unter P0145R3 Verfeinern der Ausdrucksauswertungsreihenfolge für idiomatisches C ++ .

Maxim Egorushkin
quelle
@ Maxim: Danke für die Erweiterung. Bei den von Ihnen erläuterten Anrufen wäre dies ein undefiniertes Verhalten. Aber jetzt habe ich noch eine Frage (vielleicht eine düstere, und mir fehlt etwas Grundlegendes und lautes Denken). Wie haben Sie daraus geschlossen, dass die globale Version von std :: operator << () anstelle von ostream :: operator <aufgerufen wird? <() Mitgliedsversion. Beim Debuggen lande ich in einer Mitgliedsversion von ostream :: operator << () und nicht in einer globalen Version. Aus diesem Grund dachte ich anfangs, dass die Antwort 01 sein würde
pravs
@ Maxim Nicht, dass es einen anderen macht, aber da cTyp hat int, sind die operator<<hier Mitgliedsfunktionen.
James Kanze
2
@pravs: Ob operator<<es sich um eine Mitgliedsfunktion oder eine freistehende Funktion handelt, wirkt sich nicht auf Sequenzpunkte aus.
Maxim Egorushkin
11
Der 'Sequenzpunkt' wird im C ++ - Standard nicht mehr verwendet. Es war ungenau und wurde durch die Beziehung "Vorher / Nachher sequenziert" ersetzt.
Rafał Dowgird
2
So the result of the above is undefined.Ihre Erklärung ist nur für nicht spezifizierte , nicht für undefinierte gut . JamesKanze erklärte, wie verdammt undefiniert es in seiner Antwort war .
Deduplikator
68

Technisch gesehen ist dies insgesamt undefiniertes Verhalten .

Die Antwort hat jedoch zwei wichtige Aspekte.

Die Code-Anweisung:

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

wird bewertet als:

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

Der Standard definiert nicht die Reihenfolge der Bewertung von Argumenten für eine Funktion.
Also entweder:

  • std::operator<<(std::cout, a++) wird zuerst ausgewertet oder
  • awird zuerst ausgewertet oder
  • Es kann sich um eine beliebig implementierungsdefinierte Reihenfolge handeln.

Diese Bestellung ist gemäß Standard nicht spezifiziert [Ref. 1] .

[Ref 1] C ++ 03 5.2.2 Funktionsaufruf
Abs. 8

Die Reihenfolge der Bewertung der Argumente ist nicht angegeben . Alle Nebenwirkungen von Argumentausdrucksauswertungen werden wirksam, bevor die Funktion eingegeben wird. Die Reihenfolge der Auswertung des Postfix-Ausdrucks und der Liste der Argumentausdrücke ist nicht angegeben.

Ferner gibt es keinen Sequenzpunkt zwischen der Auswertung von Argumenten für eine Funktion, aber ein Sequenzpunkt existiert erst nach Auswertung aller Argumente [Ref. 2] .

[Ref 2] C ++ 03 1.9 Programmausführung [intro.execution]:
Abs. 17:

Beim Aufrufen einer Funktion (unabhängig davon, ob die Funktion inline ist oder nicht) gibt es nach der Auswertung aller Funktionsargumente (falls vorhanden) einen Sequenzpunkt, der vor der Ausführung von Ausdrücken oder Anweisungen im Funktionskörper erfolgt.

Beachten Sie, dass hier cmehrmals auf den Wert von zugegriffen wird, ohne dass ein dazwischenliegender Sequenzpunkt vorhanden ist. Diesbezüglich lautet der Standard:

[Ref 3] C ++ 03 5 Ausdrücke [Ausdruck]:
Abs. 4:

....
Zwischen dem vorherigen und dem nächsten Sequenzpunkt soll der gespeicherte Wert eines skalaren Objekts höchstens einmal durch Auswertung eines Ausdrucks geändert werden. Darüber hinaus darf nur auf den vorherigen Wert zugegriffen werden, um den zu speichernden Wert zu bestimmen . Die Anforderungen dieses Absatzes müssen für jede zulässige Reihenfolge der Unterausdrücke eines vollständigen Ausdrucks erfüllt sein. Andernfalls ist das Verhalten undefiniert .

Der Code wird cmehrmals geändert, ohne dass ein Sequenzpunkt dazwischen liegt, und es wird nicht darauf zugegriffen, um den Wert des gespeicherten Objekts zu bestimmen. Dies ist ein klarer Verstoß gegen die obige Klausel, und daher ist das vom Standard vorgeschriebene Ergebnis undefiniertes Verhalten [Ref. 3] .

Alok Speichern
quelle
1
Technisch gesehen ist das Verhalten undefiniert, da ein Objekt geändert wird und an anderer Stelle ohne dazwischenliegenden Sequenzpunkt darauf zugegriffen wird. Undefiniert ist nicht nicht spezifiziert; Dadurch bleibt der Implementierung noch mehr Spielraum.
James Kanze
1
@ Als Ja. Ich hatte Ihre Änderungen nicht gesehen (obwohl ich auf die Aussage von jrok reagiert habe, dass das Programm nichts Seltsames tun kann - es kann). Ihre bearbeitete Version ist soweit gut, aber in meinen Augen ist das Schlüsselwort Teilreihenfolge ; Sequenzpunkte führen nur eine teilweise Reihenfolge ein.
James Kanze
1
@Als danke für eine ausführliche Beschreibung, wirklich sehr hilfreich !!
Pravs
4
Der neue C ++ 0x-Standard sagt im Wesentlichen dasselbe, jedoch in unterschiedlichen Abschnitten und in unterschiedlichen Formulierungen :) Zitat: (1.9 Programmausführung [intro.execution], Par 15): "Wenn eine Nebenwirkung auf ein skalares Objekt relativ zu nicht skaliert wird entweder ein weiterer Nebeneffekt auf dasselbe Skalarobjekt oder eine Wertberechnung unter Verwendung des Werts desselben Skalarobjekts, das Verhalten ist nicht definiert. "
Rafał Dowgird
2
Ich glaube, diese Antwort enthält einen Fehler. "std :: cout << c ++ << c;" kann nicht in "std :: operator << (std :: operator << (std :: cout, c ++), c)" übersetzt werden, da std :: operator << (std :: ostream &, int) nicht existiert. Stattdessen wird "std :: cout.operator << (c ++). Operator (c);" übersetzt, der tatsächlich einen Sequenzpunkt zwischen der Auswertung von "c ++" und "c" hat (ein überladener Operator wird als a betrachtet Funktionsaufruf und daher gibt es einen Sequenzpunkt, an dem der Funktionsaufruf zurückkehrt). Folglich ist das Verhalten und die Ausführungsreihenfolge wird angegeben.
Christopher Smith
20

Sequenzpunkte nur definieren Teilbestellung. In Ihrem Fall haben Sie (sobald die Überlastungsauflösung abgeschlossen ist):

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

Es gibt einen Sequenzpunkt zwischen dem a++und dem ersten Aufruf von std::ostream::operator<<und einen Sequenzpunkt zwischen dem zweiten aund dem zweiten Aufruf von std::ostream::operator<<, aber es gibt keinen Sequenzpunkt zwischen a++und a; Die einzigen Ordnungsbeschränkungen bestehen darin, dass a++sie vor dem ersten Aufruf von vollständig ausgewertet werden (einschließlich Nebenwirkungen) operator<<und dass die zweite avor dem zweiten Aufruf von vollständig ausgewertet wird operator<<. (Es gibt auch kausale Ordnungsbeschränkungen: Der zweite Aufruf von operator<<kann dem ersten nicht vorangehen, da er die Ergebnisse des ersten als Argument erfordert.) §5 / 4 (C ++ 03) besagt:

Sofern nicht anders angegeben, ist die Reihenfolge der Auswertung von Operanden einzelner Operatoren und Unterausdrücke einzelner Ausdrücke sowie die Reihenfolge, in der Nebenwirkungen auftreten, nicht angegeben. Zwischen dem vorherigen und dem nächsten Sequenzpunkt soll der gespeicherte Wert eines Skalarobjekts höchstens einmal durch Auswertung eines Ausdrucks geändert werden. Darüber hinaus darf nur auf den vorherigen Wert zugegriffen werden, um den zu speichernden Wert zu bestimmen. Die Anforderungen dieses Absatzes müssen für jede zulässige Reihenfolge der Unterausdrücke eines vollständigen Ausdrucks erfüllt sein. Andernfalls ist das Verhalten undefiniert.

Einer der zulässigen Ordnungen des Ausdrucks ist a++, azunächst Anruf operator<<, auf den zweiten Anruf operator<<; Dies ändert den gespeicherten Wert von a( a++) und greift darauf zu, außer um den neuen Wert (den zweiten a) zu bestimmen. Das Verhalten ist undefiniert.

James Kanze
quelle
Ein Haken aus Ihrem Zitat des Standards. Das "außer wo angegeben", IIRC, enthält eine Ausnahme beim Umgang mit einem überladenen Operator, der den Operator als Funktion behandelt und daher einen Sequenzpunkt zwischen dem ersten und zweiten Aufruf von std :: ostream :: operator << (int ). Bitte korrigieren Sie mich, wenn ich falsch liege.
Christopher Smith
@ChristopherSmith Ein überladener Operator verhält sich wie ein Funktionsaufruf. Wenn cwaren ein Benutzertyp mit einem Benutzer definiert ++, statt int, wären die Ergebnisse nicht spezifizieren, aber es gäbe kein undefiniertes Verhalten sein.
James Kanze
1
@ChristopherSmith Wo sehen Sie einen Sequenzpunkt zwischen den beiden cin foo(foo(bar(c)), c)? Es gibt einen Sequenzpunkt, an dem Funktionen aufgerufen werden und wenn sie zurückkehren, aber zwischen den Auswertungen der beiden ist kein Funktionsaufruf erforderlich c.
James Kanze
1
@ChristopherSmith Wenn ces sich um eine UDT handeln würde , wären die überladenen Operatoren Funktionsaufrufe und würden einen Sequenzpunkt einführen, sodass das Verhalten nicht undefiniert wäre. Es ist jedoch immer noch nicht angegeben, ob der Unterausdruck cvorher oder nachher ausgewertet c++wurde. Ob Sie also die inkrementierte Version erhalten haben oder nicht, wird nicht angegeben (und muss theoretisch nicht jedes Mal gleich sein).
James Kanze
1
@ChristopherSmith Alles vor dem Sequenzpunkt passiert vor allem nach dem Sequenzpunkt. Sequenzpunkte definieren jedoch nur eine Teilreihenfolge. In dem fraglichen Ausdruck gibt es beispielsweise keinen Sequenzpunkt zwischen den Unterausdrücken cund c++, so dass die beiden in beliebiger Reihenfolge auftreten können. Semikolons ... Sie verursachen nur dann einen Sequenzpunkt, wenn sie vollständige Ausdrücke sind. Weitere wichtige Sequenzpunkte sind der Funktionsaufruf: f(c++)wird der erhöhte sehen cin f, und der Komma - Operator, &&, ||und ?:auch Ursache Sequenzpunkte.
James Kanze
4

Die richtige Antwort ist, die Frage zu stellen. Die Aussage ist inakzeptabel, da ein Leser keine klare Antwort sehen kann. Eine andere Sichtweise ist, dass wir Nebenwirkungen (c ++) eingeführt haben, die die Interpretation der Aussage erheblich erschweren. Prägnanter Code ist großartig, vorausgesetzt, seine Bedeutung ist klar.

Paul Marrington
quelle
4
Die Frage kann eine schlechte Programmierpraxis aufweisen (und sogar ungültiges C ++). Aber eine Antwort soll die Frage beantworten , was falsch ist und warum es falsch ist. Ein Kommentar zu der Frage ist keine Antwort, auch wenn sie vollkommen gültig sind. Im besten Fall kann dies ein Kommentar sein, keine Antwort.
PP