Was machte i = i ++ + 1; legal in C ++ 17?

186

Bevor Sie anfangen, undefiniertes Verhalten zu schreien, wird dies in N4659 (C ++ 17) explizit aufgeführt.

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

Noch in N3337 (C ++ 11)

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

Was hat sich geändert?

Soweit ich das beurteilen kann, aus [N4659 basic.exec]

Sofern nicht anders angegeben, werden Auswertungen von Operanden einzelner Operatoren und von Unterausdrücken einzelner Ausdrücke nicht sequenziert. [...] Die Wertberechnungen der Operanden eines Operators werden vor der Wertberechnung des Ergebnisses des Operators sequenziert. Wenn ein Nebeneffekt an einem Speicherort nicht in Bezug auf einen anderen Nebeneffekt an demselben Speicherort oder eine Wertberechnung unter Verwendung des Werts eines Objekts an demselben Speicherort sequenziert wird und diese möglicherweise nicht gleichzeitig auftreten, ist das Verhalten undefiniert.

Wobei der Wert unter [N4659 basic.type] definiert ist

Bei trivial kopierbaren Typen ist die Wertedarstellung eine Menge von Bits in der Objektdarstellung, die einen Wert bestimmt , der ein diskretes Element einer implementierungsdefinierten Menge von Werten ist

Aus [N3337 basic.exec]

Sofern nicht anders angegeben, werden Auswertungen von Operanden einzelner Operatoren und von Unterausdrücken einzelner Ausdrücke nicht sequenziert. [...] Die Wertberechnungen der Operanden eines Operators werden vor der Wertberechnung des Ergebnisses des Operators sequenziert. Wenn eine Nebenwirkung auf ein Skalarobjekt in Bezug auf eine andere Nebenwirkung auf dasselbe Skalarobjekt oder eine Wertberechnung unter Verwendung des Werts desselben Skalarobjekts nicht sequenziert wird, ist das Verhalten undefiniert.

Ebenso ist der Wert unter [N3337 basic.type] definiert.

Bei trivial kopierbaren Typen ist die Wertdarstellung eine Menge von Bits in der Objektdarstellung, die einen Wert bestimmt , der ein diskretes Element eines implementierungsdefinierten Satzes von Werten ist.

Sie sind identisch, mit Ausnahme der Erwähnung der Parallelität, die keine Rolle spielt, und bei Verwendung des Speicherorts anstelle des skalaren Objekts , wo

Arithmetische Typen, Aufzählungstypen, Zeigertypen, Zeiger auf Elementtypen std::nullptr_tund lebenslaufqualifizierte Versionen dieser Typen werden gemeinsam als Skalartypen bezeichnet.

Was das Beispiel nicht beeinflusst.

Aus [N4659 expr.ass]

Der Zuweisungsoperator (=) und die zusammengesetzten Zuweisungsoperatoren gruppieren sich alle von rechts nach links. Alle benötigen einen modifizierbaren l-Wert als linken Operanden und geben einen l-Wert zurück, der sich auf den linken Operanden bezieht. Das Ergebnis ist in allen Fällen ein Bitfeld, wenn der linke Operand ein Bitfeld ist. In allen Fällen wird die Zuweisung nach der Wertberechnung des rechten und linken Operanden und vor der Wertberechnung des Zuweisungsausdrucks sequenziert. Der rechte Operand wird vor dem linken Operanden sequenziert.

Aus [N3337 expr.ass]

Der Zuweisungsoperator (=) und die zusammengesetzten Zuweisungsoperatoren gruppieren sich alle von rechts nach links. Alle benötigen einen modifizierbaren l-Wert als linken Operanden und geben einen l-Wert zurück, der sich auf den linken Operanden bezieht. Das Ergebnis ist in allen Fällen ein Bitfeld, wenn der linke Operand ein Bitfeld ist. In allen Fällen wird die Zuweisung nach der Wertberechnung des rechten und linken Operanden und vor der Wertberechnung des Zuweisungsausdrucks sequenziert.

Der einzige Unterschied besteht darin, dass der letzte Satz in N3337 fehlt.

Der letzte Satz sollte jedoch keine Bedeutung haben, da der linke Operand iweder "ein weiterer Nebeneffekt" noch "den Wert desselben skalaren Objekts verwenden" ist, da der id-Ausdruck ein l-Wert ist.

Passant von
quelle
23
Sie haben den Grund dafür identifiziert: In C ++ 17 wird der rechte Operand vor dem linken Operanden sequenziert. In C ++ 11 gab es keine solche Sequenzierung. Was genau ist Ihre Frage?
Robᵩ
4
@ Robᵩ Siehe letzten Satz.
Passant Bis zum
7
Hat jemand einen Link zur Motivation für diese Änderung? Ich möchte, dass ein statischer Analysator sagen kann, dass Sie das nicht wollen, wenn er mit Code wie konfrontiert wird i = i++ + 1;.
7
@NeilButterworth, es ist aus dem Artikel p0145r3.pdf : "Verfeinerung der Ausdrucksbewertungsreihenfolge für idiomatisches C ++".
Xaizek
9
@NeilButterworth, Abschnitt 2 besagt, dass dies nicht intuitiv ist und selbst Experten nicht in allen Fällen das Richtige tun. Das ist so ziemlich ihre ganze Motivation.
Xaizek

Antworten:

144

In C ++ 11 wird der Vorgang der "Zuweisung", dh der Nebeneffekt der Änderung der LHS, nach der Wertberechnung des richtigen Operanden sequenziert . Beachten Sie, dass dies eine relativ "schwache" Garantie ist: Sie erzeugt eine Sequenzierung nur in Bezug auf die Wertberechnung der RHS. Es sagt nichts über die Nebenwirkungen aus , die in der RHS vorhanden sein könnten, da das Auftreten von Nebenwirkungen nicht Teil der Wertberechnung ist . Die Anforderungen von C ++ 11 legen keine relative Reihenfolge zwischen dem Zuweisungsvorgang und den Nebenwirkungen der RHS fest. Dies schafft das Potenzial für UB.

Die einzige Hoffnung in diesem Fall sind zusätzliche Garantien, die von bestimmten in RHS verwendeten Betreibern gegeben werden. Wenn die RHS ein Präfix verwendet hätte ++, hätten Sequenzierungseigenschaften, die für die Präfixform von spezifisch sind, ++in diesem Beispiel den Tag gerettet. Aber Postfix ++ist eine andere Geschichte: Es gibt keine solchen Garantien. In C ++ 11 bleiben die Nebenwirkungen von =und Postfix ++in diesem Beispiel in Bezug zueinander unsequenziert. Und das ist UB.

In C ++ 17 wird der Spezifikation des Zuweisungsoperators ein zusätzlicher Satz hinzugefügt:

Der rechte Operand wird vor dem linken Operanden sequenziert.

In Kombination mit dem oben Gesagten ergibt sich eine sehr starke Garantie. Es sequenziert alles , was in der RHS passiert (einschließlich aller Nebenwirkungen), vor allem , was in der LHS passiert. Da die eigentliche Zuordnung nach LHS (und RHS) sequenziert wird, isoliert diese zusätzliche Sequenzierung den Zuweisungsvorgang vollständig von allen in RHS vorhandenen Nebenwirkungen. Diese stärkere Sequenzierung eliminiert die obige UB.

(Aktualisiert, um die Kommentare von @John Bollinger zu berücksichtigen.)

Ameise
quelle
3
Ist es wirklich richtig, "den tatsächlichen Zuweisungsakt" in die Effekte aufzunehmen, die von "dem linken Operanden" in diesem Auszug abgedeckt werden? Der Standard hat eine separate Sprache für die Reihenfolge der tatsächlichen Zuordnung. Ich gehe davon aus, dass der von Ihnen vorgestellte Auszug in seinem Umfang auf die Sequenzierung der linken und rechten Unterausdrücke beschränkt ist, was in Kombination mit dem Rest dieses Abschnitts nicht ausreicht, um eine gute Unterstützung zu bieten. Definiertheit der OP-Erklärung.
John Bollinger
11
Korrektur: Die tatsächliche Zuordnung wird nach der Wertberechnung des linken Operanden noch sequenziert, und die Auswertung des linken Operanden wird nach der (vollständigen) Auswertung des rechten Operanden sequenziert. Ja, diese Änderung reicht aus, um die Genauigkeit des OP zu unterstützen fragte nach. Ich streite also nur die Details, aber diese sind wichtig, da sie unterschiedliche Auswirkungen auf unterschiedlichen Code haben können.
John Bollinger
3
@JohnBollinger: Ich finde es merkwürdig, dass die Autoren des Standards eine Änderung vornehmen würden, die die Effizienz selbst einer einfachen Codegenerierung beeinträchtigt und historisch nicht notwendig war, und dennoch andere Verhaltensweisen nicht definieren, deren Abwesenheit ein viel größeres Problem darstellt und die würde selten ein bedeutendes Hindernis für die Effizienz darstellen.
Supercat
1
@Kaz: Für Verbindung Zuweisungen nach der rechten Seite der linken Wertauswertung durchführen kann so etwas wie x -= y;so verarbeitet werden , mov eax,[y] / sub [x],eaxals vielmehr mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Daran sehe ich nichts Idiodisches. Wenn man eine Reihenfolge angeben müsste, wäre die effizienteste Reihenfolge wahrscheinlich, alle Berechnungen durchzuführen, die erforderlich sind, um zuerst das Objekt auf der linken Seite zu identifizieren , dann den rechten Operanden und dann den Wert des linken Objekts auszuwerten , aber dafür wäre ein Term erforderlich für den Vorgang des Auflösens der Identität des linken Objekts.
Supercat
1
@Kaz: Wenn xund ywaren volatile, wäre das Nebenwirkungen haben. Ferner würde gelten die gleichen Überlegungen x += f();, wo f()modifiziert x.
Supercat
33

Sie haben den neuen Satz identifiziert

Der rechte Operand wird vor dem linken Operanden sequenziert.

und Sie haben richtig festgestellt, dass die Bewertung des linken Operanden als Wert irrelevant ist. Die zuvor sequenzierte Beziehung wird jedoch als transitive Beziehung angegeben. Der vollständige rechte Operand (einschließlich des Nachinkrements) wird daher auch vor der Zuweisung sequenziert. In C ++ 11 wurde vor der Zuweisung nur die Wertberechnung des richtigen Operanden sequenziert.


quelle
7

In älteren C ++ - Standards und in C11 endet die Definition des Zuweisungsoperatortextes mit dem Text:

Die Auswertungen der Operanden sind nicht sequenziert.

Dies bedeutet, dass Nebenwirkungen in den Operanden nicht sequenziert werden und daher definitiv undefiniertes Verhalten, wenn sie dieselbe Variable verwenden.

Dieser Text wurde in C ++ 11 einfach entfernt, sodass er etwas mehrdeutig war. Ist es UB oder nicht? Dies wurde in C ++ 17 klargestellt, wo Folgendes hinzugefügt wurde:

Der rechte Operand wird vor dem linken Operanden sequenziert.


Als Randnotiz wurde dies in noch älteren Standards sehr deutlich gemacht, Beispiel aus C99:

Die Reihenfolge der Auswertung der Operanden ist nicht spezifiziert. Wenn versucht wird, das Ergebnis eines Zuweisungsoperators zu ändern oder nach dem nächsten Sequenzpunkt darauf zuzugreifen, ist das Verhalten undefiniert.

Grundsätzlich haben sie in C11 / C ++ 11 Fehler gemacht, als sie diesen Text entfernt haben.

Lundin
quelle
1

Dies sind weitere Informationen zu den anderen Antworten und ich poste sie, da häufig auch nach dem folgenden Code gefragt wird .

Die Erklärung in den anderen Antworten ist korrekt und gilt auch für den folgenden Code, der jetzt genau definiert ist (und den gespeicherten Wert von nicht ändert i):

i = i++;

Das + 1ist ein roter Hering und es ist nicht wirklich klar, warum der Standard ihn in ihren Beispielen verwendet hat, obwohl ich mich an Leute erinnere, die vor C ++ 11 auf Mailinglisten argumentierten, dass dies möglicherweise + 1einen Unterschied gemacht hat, weil die frühzeitige Wertumwandlung auf der rechten Seite erzwungen wurde. Handseite. Sicherlich gilt nichts davon in C ++ 17 (und wahrscheinlich nie in einer Version von C ++).

MM
quelle