Undefiniertes Verhalten und Sequenzpunkte

987

Was sind "Sequenzpunkte"?

Welche Beziehung besteht zwischen undefiniertem Verhalten und Sequenzpunkten?

Ich benutze oft lustige und verschlungene Ausdrücke wie a[++i] = i;, um mich besser zu fühlen. Warum sollte ich sie nicht mehr benutzen?

Wenn Sie dies gelesen haben, lesen Sie unbedingt die Folgefrage Undefiniertes Verhalten und neu geladene Sequenzpunkte .

(Hinweis: Dies ist als Eintrag in die C ++ - FAQ von Stack Overflow gedacht . Wenn Sie die Idee kritisieren möchten, eine FAQ in dieser Form bereitzustellen, ist die Veröffentlichung auf Meta, mit der all dies begonnen hat , der richtige Ort dafür. Antworten auf Diese Frage wird im C ++ - Chatroom überwacht, in dem die FAQ-Idee ursprünglich begann, sodass Ihre Antwort sehr wahrscheinlich von denjenigen gelesen wird, die auf die Idee gekommen sind.)

unbekannt
quelle

Antworten:

683

C ++ 98 und C ++ 03

Diese Antwort gilt für ältere Versionen des C ++ - Standards. Die Versionen C ++ 11 und C ++ 14 des Standards enthalten formal keine 'Sequenzpunkte'. Operationen werden stattdessen "vorher sequenziert" oder "nicht sequenziert" oder "unbestimmt sequenziert". Der Nettoeffekt ist im Wesentlichen der gleiche, aber die Terminologie ist unterschiedlich.


Haftungsausschluss : Okay. Diese Antwort ist etwas lang. Also haben Sie Geduld beim Lesen. Wenn Sie diese Dinge bereits kennen, macht Sie das erneute Lesen nicht verrückt.

Voraussetzungen : Grundkenntnisse in C ++ Standard


Was sind Sequenzpunkte?

Der Standard sagt

An bestimmten festgelegten Punkten in der Ausführungssequenz, die als Sequenzpunkte bezeichnet werden , müssen alle Nebenwirkungen früherer Bewertungen vollständig sein und es dürfen keine Nebenwirkungen nachfolgender Bewertungen aufgetreten sein. (§1.9 / 7)

Nebenwirkungen? Was sind Nebenwirkungen?

Die Auswertung eines Ausdrucks erzeugt etwas, und wenn sich zusätzlich der Zustand der Ausführungsumgebung ändert, wird gesagt, dass der Ausdruck (seine Auswertung) einige Nebenwirkungen hat.

Zum Beispiel:

int x = y++; //where y is also an int

Zusätzlich zur Initialisierungsoperation wird der Wert von yaufgrund der Nebenwirkung des ++Bedieners geändert .

So weit, ist es gut. Weiter zu Sequenzpunkten. Eine vom Autor von comp.lang.c angegebene alternative Definition von Seq-Punkten Steve Summit:

Der Sequenzpunkt ist ein Zeitpunkt, zu dem sich der Staub abgesetzt hat und alle bisher beobachteten Nebenwirkungen garantiert vollständig sind.


Was sind die im C ++ Standard aufgeführten allgemeinen Sequenzpunkte?

Jene sind:

  • am Ende der Auswertung des vollständigen Ausdrucks ( §1.9/16) (Ein vollständiger Ausdruck ist ein Ausdruck, der kein Unterausdruck eines anderen Ausdrucks ist.) 1

    Beispiel:

    int a = 5; // ; is a sequence point here
  • bei der Auswertung jedes der folgenden Ausdrücke nach der Auswertung des ersten Ausdrucks ( §1.9/18) 2

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18)(hier ist a, b ein Kommaoperator; in func(a,a++) ,ist kein Kommaoperator, es ist lediglich ein Trennzeichen zwischen den Argumenten aund a++. Daher ist das Verhalten in diesem Fall undefiniert (wenn aes als primitiver Typ betrachtet wird))
  • bei einem Funktionsaufruf (unabhängig davon, ob die Funktion inline ist oder nicht) nach der Auswertung aller Funktionsargumente (falls vorhanden), die vor der Ausführung von Ausdrücken oder Anweisungen im Funktionskörper ( §1.9/17) erfolgt.

1: Hinweis: Die Auswertung eines vollständigen Ausdrucks kann die Auswertung von Unterausdrücken umfassen, die nicht lexikalisch Teil des vollständigen Ausdrucks sind. Beispielsweise wird davon ausgegangen, dass Unterausdrücke, die an der Auswertung von Standardargumentausdrücken (8.3.6) beteiligt sind, in dem Ausdruck erstellt werden, der die Funktion aufruft, und nicht in dem Ausdruck, der das Standardargument definiert

2: Die angegebenen Operatoren sind die integrierten Operatoren, wie in Abschnitt 5 beschrieben. Wenn einer dieser Operatoren in einem gültigen Kontext überladen ist (Abschnitt 13) und somit eine benutzerdefinierte Operatorfunktion bezeichnet, bezeichnet der Ausdruck einen Funktionsaufruf und Die Operanden bilden eine Argumentliste ohne einen impliziten Sequenzpunkt zwischen ihnen.


Was ist undefiniertes Verhalten?

Der Standard definiert undefiniertes Verhalten in Abschnitt §1.3.12als

Verhalten, wie es bei Verwendung eines fehlerhaften Programmkonstrukts oder fehlerhafter Daten auftreten kann, für die diese Internationale Norm keine Anforderungen stellt 3 .

Undefiniertes Verhalten kann auch erwartet werden, wenn in dieser Internationalen Norm die Beschreibung einer expliziten Definition des Verhaltens weggelassen wird.

3: Das zulässige undefinierte Verhalten reicht vom vollständigen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer dokumentierten, für die Umgebung charakteristischen Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe einer Diagnosemeldung).

Kurz gesagt, undefiniertes Verhalten bedeutet, dass alles passieren kann, von Dämonen, die aus Ihrer Nase fliegen, bis zu Ihrer schwangeren Freundin.


Welche Beziehung besteht zwischen undefiniertem Verhalten und Sequenzpunkten?

Bevor ich darauf eingehe, müssen Sie den Unterschied zwischen undefiniertem Verhalten, nicht spezifiziertem Verhalten und implementierungsdefiniertem Verhalten kennen .

Das müssen Sie auch wissen the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.

Zum Beispiel:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

Ein weiteres Beispiel hier .


Jetzt §5/4sagt der Standard in

  • 1) Zwischen dem vorherigen und dem nächsten Sequenzpunkt muss der gespeicherte Wert eines Skalarobjekts höchstens einmal durch Auswertung eines Ausdrucks geändert werden.

Was bedeutet das?

Informell bedeutet dies, dass zwischen zwei Sequenzpunkten eine Variable nicht mehr als einmal geändert werden darf. In einer Ausdrucksanweisung steht das next sequence pointnormalerweise am abschließenden Semikolon und das previous sequence pointam Ende der vorherigen Anweisung. Ein Ausdruck kann auch ein Zwischenprodukt enthalten sequence points.

Aus dem obigen Satz rufen die folgenden Ausdrücke undefiniertes Verhalten hervor:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)

Aber die folgenden Ausdrücke sind in Ordnung:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

  • 2) Auf den vorherigen Wert darf nur zugegriffen werden, um den zu speichernden Wert zu bestimmen.

Was bedeutet das? Wenn ein Objekt innerhalb eines vollständigen Ausdrucks beschrieben wird, müssen alle Zugriffe innerhalb desselben Ausdrucks direkt an der Berechnung des zu schreibenden Werts beteiligt sein .

Zum Beispiel sind bei i = i + 1allen Zugriffen von i(in LHS und in RHS) direkt an der Berechnung des zu schreibenden Wertes beteiligt. Also ist es gut.

Diese Regel beschränkt rechtliche Ausdrücke effektiv auf diejenigen, bei denen die Zugriffe nachweislich der Änderung vorausgehen.

Beispiel 1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

Beispiel 2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

wird nicht zugelassen, weil einer der Zugriffe von i(der in a[i]) nichts mit dem Wert zu tun hat, der in i gespeichert wird (was in in geschieht i++), und daher gibt es keine gute Möglichkeit zu definieren - weder für unser Verständnis noch für das Compiler - Gibt an, ob der Zugriff vor oder nach dem Speichern des inkrementierten Werts erfolgen soll. Das Verhalten ist also undefiniert.

Beispiel 3:

int x = i + i++ ;// Similar to above

Folgen Sie der Antwort für C ++ 11 hier .

Prasoon Saurav
quelle
45
*p++ = 4 ist nicht undefiniertes Verhalten. *p++wird interpretiert als *(p++). p++gibt p(eine Kopie) zurück und der Wert wird an der vorherigen Adresse gespeichert. Warum sollte das UB aufrufen? Es ist vollkommen in Ordnung.
Prasoon Saurav
6
@ Mike: AFAIK, es gibt keine (legalen) Kopien des C ++ - Standards, auf die Sie verlinken könnten.
sbi
11
Dann könnten Sie einen Link zur entsprechenden Bestellseite der ISO haben. Wenn man darüber nachdenkt, scheint der Ausdruck "Grundkenntnisse in C ++ Standard" ein gewisser Widerspruch zu sein, denn wenn man den Standard liest, ist man über die Grundstufe hinaus. Vielleicht könnten wir auflisten, welche Dinge in der Sprache Sie grundlegend verstehen müssen, wie Ausdruckssyntax, Reihenfolge der Operationen und möglicherweise das Überladen von Operatoren?
Mike DeSimone
41
Ich bin nicht sicher, ob das Zitieren des Standards der beste Weg ist, um Neulingen beizubringen
Inverse
6
@Adrian Der erste Ausdruck ruft eine UB auf, da zwischen dem letzten ++iund der Zuweisung zu kein Sequenzpunkt liegt i. Der zweite Ausdruck ruft UB nicht auf, da der Ausdruck iden Wert von nicht ändert i. Im zweiten Beispiel i++folgt auf einen Sequenzpunkt ( ,), bevor der Zuweisungsoperator aufgerufen wird.
Kolyunya
276

Dies ist eine Fortsetzung meiner vorherigen Antwort und enthält C ++ 11-bezogenes Material. .


Voraussetzungen : Grundkenntnisse in Beziehungen (Mathematik).


Stimmt es, dass es in C ++ 11 keine Sequenzpunkte gibt?

Ja! Das ist sehr wahr.

Sequenzpunkte wurden in C ++ 11 durch die Beziehungen Sequenced Before und Sequenced After (und Unsequenced and Indeterminately Sequenced ) ersetzt .


Was genau ist dieses "Sequenced before" -Ding?

Sequenced Before (§1.9 / 13) ist eine Beziehung, die ist:

zwischen Auswertungen, die von einem einzelnen Thread ausgeführt werden und eine strikte Teilreihenfolge induzieren 1

Formal bedeutet es zwei beliebige Auswertungen gegeben (siehe unten) A und B, wenn Asie vor sequenziert B , dann der Ausführung A soll vorangehen , die Ausführung B. Wenn Avorher nicht sequenziert wurde Bund vorher Bnicht sequenziert wurde A, dann Aund Bsind nicht sequenziert 2 .

Auswertungen Aund Bsind unbestimmt sequenziert, wenn entweder Avorher Boder Bvorher sequenziert wird A, aber es ist nicht spezifiziert, welche 3 .

[Anmerkungen]
1: Eine strenge Teilordnung ist eine binäre Relation "<" über eine Menge , Pdas ist asymmetric, und transitive, das heißt, für alle a, bund cin P, haben wir , dass:
........ (i). wenn a <b dann ¬ (b <a) ( asymmetry);
........ (ii). wenn a <b und b <c, dann a <c ( transitivity).
2: Die Ausführung nicht sequenzierter Auswertungen kann sich überschneiden .
3: Unbestimmt sequenzierte Auswertungen können sich nicht überlappen , können jedoch zuerst ausgeführt werden.


Was bedeutet das Wort "Evaluierung" im Kontext von C ++ 11?

In C ++ 11 umfasst die Bewertung eines Ausdrucks (oder eines Unterausdrucks) im Allgemeinen:

  • Wertberechnungen (einschließlich Bestimmen der Identität eines Objekts für die Gl- Wert- Bewertung und Abrufen eines Werts, der zuvor einem Objekt für die Wert-Bewertung zugewiesen wurde ) und

  • Einleitung von Nebenwirkungen .

Jetzt (§1.9 / 14) heißt es:

Jede mit einem vollständigen Ausdruck verbundene Wertberechnung und Nebenwirkung wird vor jeder mit dem nächsten zu bewertenden vollständigen Ausdruck verbundenen Wertberechnung und Nebenwirkung sequenziert .

  • Triviales Beispiel:

    int x; x = 10; ++x;

    Die damit verbundene ++xWertberechnung und Nebenwirkung wird nach der Wertberechnung und Nebenwirkung von sequenziertx = 10;


Es muss also eine Beziehung zwischen undefiniertem Verhalten und den oben genannten Dingen geben, oder?

Ja! Recht.

In (§1.9 / 15) wurde erwähnt, dass

Sofern nicht anders angegeben, werden Auswertungen von Operanden einzelner Operatoren und von Unterausdrücken einzelner Ausdrücke nicht sequenziert 4 .

Beispielsweise :

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  1. Die Auswertung der Operanden des +Operators ist relativ zueinander nicht sequenziert.
  2. Die Auswertung von Operanden <<und >>Operatoren ist relativ zueinander nicht sequenziert.

4: In einem Ausdruck, der während der Ausführung eines Programms mehr als einmal ausgewertet wird, müssen nicht sequenzierte und unbestimmt sequenzierte Auswertungen seiner Unterausdrücke nicht konsistent in verschiedenen Auswertungen durchgeführt werden.

(§1.9 / 15) Die Wertberechnungen der Operanden eines Operators werden vor der Wertberechnung des Ergebnisses des Operators sequenziert.

Das heißt bei x + yder Wertberechnung von xund ywerden vor der Wertberechnung von sequenziert (x + y).

Wichtiger

(§1.9 / 15) Wenn eine Nebenwirkung auf ein skalares Objekt relativ zu beiden nicht sequenziert wird

(a) eine weitere Nebenwirkung auf dasselbe skalare Objekt

oder

(b) eine Wertberechnung unter Verwendung des Wertes desselben skalaren Objekts.

Das Verhalten ist undefiniert .

Beispiele:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

Beim Aufrufen einer Funktion (unabhängig davon, ob die Funktion inline ist oder nicht) wird jede Wertberechnung und Nebenwirkung, die einem Argumentausdruck oder dem Postfix-Ausdruck zugeordnet ist, der die aufgerufene Funktion bezeichnet, vor der Ausführung jedes Ausdrucks oder jeder Anweisung im Hauptteil des sequenziert Funktion genannt. [ Hinweis: Wertberechnungen und Nebenwirkungen, die mit verschiedenen Argumentausdrücken verbunden sind, werden nicht sequenziert . - Endnote ]

Ausdrücke (5), (7)und (8)nicht undefiniert Verhalten aufrufen. In den folgenden Antworten finden Sie eine ausführlichere Erklärung.


Schlussbemerkung :

Wenn Sie einen Fehler in der Post finden, hinterlassen Sie bitte einen Kommentar. Power-User (mit rep> 20000) zögern Sie bitte nicht, den Beitrag zu bearbeiten, um Tippfehler und andere Fehler zu korrigieren.

Prasoon Saurav
quelle
3
Anstelle von "asymmetrisch" werden vorher / nachher "antisymmetrische" Beziehungen sequenziert. Dies sollte im Text geändert werden, um der später angegebenen Definition einer Teilreihenfolge zu entsprechen (was auch mit Wikipedia übereinstimmt).
TemplateRex
1
Warum ist 7) ​​Element im letzten Beispiel ein UB? Vielleicht sollte es sein f(i = -1, i = 1)?
Mikhail
1
Ich habe die Beschreibung der Beziehung "vorher sequenziert" korrigiert. Es ist eine strenge Teilordnung . Offensichtlich kann ein Ausdruck nicht vor sich selbst sequenziert werden, daher kann die Beziehung nicht reflexiv sein. Daher ist es asymmetrisch und nicht antisymmetrisch.
ThomasMcLeod
1
5) gut befallen zu sein hat mich umgehauen. Die Erklärung von Johannes Schaub war nicht ganz einfach zu bekommen. Vor allem, weil ich der Meinung war, dass der Standard selbst in ++i(der Wert wird vor dem +Bediener ausgewertet ) nicht sagt, dass seine Nebenwirkung beendet werden muss. Aber in der Tat, weil es einen Verweis auf a zurückgibt, lvalueder iselbst ist, MUSS es den Nebeneffekt beendet haben, da die Bewertung abgeschlossen sein muss, daher muss der Wert auf dem neuesten Stand sein. Dies war der verrückte Teil, um tatsächlich zu bekommen.
v.oddou
"Die Mitglieder des ISO C ++ - Komitees waren der Meinung, dass Sequence Points-Dinge ziemlich schwer zu verstehen sind. Deshalb haben sie beschlossen, sie durch die oben genannten Beziehungen zu ersetzen, nur um eine klarere Formulierung und eine genauere Darstellung zu erhalten." - Haben Sie eine Referenz für diese Behauptung? Es scheint mir, dass die neuen Beziehungen schwerer zu verstehen sind.
MM
30

C ++ 17 ( N4659) enthält einen Vorschlag zur Verfeinerung der Ausdrucksbewertungsreihenfolge für idiomatisches C ++, der eine strengere Reihenfolge der Ausdrucksbewertung definiert.

Insbesondere der folgende Satz

8.18 Zuweisungs- und zusammengesetzte Zuweisungsoperatoren :
....

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.

zusammen mit der folgenden Klarstellung

Ein Ausdruck X wird als vor einem Ausdruck Y sequenziert bezeichnet, wenn jede Wertberechnung und jede mit dem Ausdruck X verbundene Nebenwirkung vor jeder Wertberechnung und jeder mit dem Ausdruck Y verbundenen Nebenwirkung sequenziert wird .

Machen Sie mehrere Fälle von zuvor undefiniertem Verhalten gültig, einschließlich des fraglichen:

a[++i] = i;

Einige andere ähnliche Fälle führen jedoch immer noch zu undefiniertem Verhalten.

In N4140:

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

Aber in N4659

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

Die Verwendung eines C ++ 17-kompatiblen Compilers bedeutet natürlich nicht unbedingt, dass man mit dem Schreiben solcher Ausdrücke beginnen sollte.

AlexD
quelle
warum i = i++ + 1;definierte Verhalten in C ++ 17, glaube ich , auch wenn „Der rechte Operand vor dem linken Operanden sequenziert wird“, aber die Modifikation für „i ++“ und die Nebenwirkung für die Zuordnung ist unsequenced, bitte weitere Informationen geben , um diese zu interpretieren
Jack X
@jackX Ich habe die Antwort erweitert :).
AlexD
Ja, ich denke, das Detail der Interpretation des Satzes "Der rechte Operand wird vor dem linken Operanden sequenziert" ist nützlicher. "Der rechte Operand wird vor dem linken Operanden sequenziert" bedeutet, dass die mit dem rechten Operanden verbundene Wertberechnung und Nebenwirkung sind vor dem linken Operanden sequenziert. wie du es getan hast :-)
jack X
11

Ich vermute, es gibt einen fundamentalen Grund für die Änderung, es ist nicht nur kosmetisch, die alte Interpretation klarer zu machen: Dieser Grund ist Parallelität. Eine nicht spezifizierte Reihenfolge der Ausarbeitung ist lediglich die Auswahl einer von mehreren möglichen Serienreihenfolgen. Dies unterscheidet sich erheblich von der Reihenfolge vor und nach der Bestellung, da eine gleichzeitige Auswertung möglich ist, wenn keine spezifizierte Reihenfolge vorliegt: Nicht so bei den alten Regeln. Zum Beispiel in:

f (a,b)

vorher entweder a dann b oder b dann a. Jetzt können a und b mit verschachtelten Anweisungen oder sogar auf verschiedenen Kernen ausgewertet werden.

Yttrill
quelle
5
Ich glaube jedoch, dass, wenn entweder 'a' oder 'b' einen Funktionsaufruf enthält, diese eher unbestimmt als nicht sequenziert sequenziert werden, was bedeutet, dass alle Nebenwirkungen von einem auftreten müssen, bevor Nebenwirkungen von dem auftreten andere, obwohl der Compiler nicht konsistent sein muss, welcher zuerst geht. Wenn dies nicht mehr der Fall ist, wird eine Menge Code zerstört, der darauf beruht, dass sich die Operationen nicht überlappen (z. B. wenn 'a' und 'b' jeweils einen gemeinsamen statischen Zustand einrichten, verwenden und entfernen).
Supercat
2

In C99(ISO/IEC 9899:TC3)dieser Diskussion scheinen bisher die folgenden Aussagen zur Reihenfolge der Bewertung getroffen worden zu sein.

[...] Die Reihenfolge der Bewertung von Subexpressionen und die Reihenfolge, in der Nebenwirkungen auftreten, sind beide nicht spezifiziert. (Abschnitt 6.5, S. 67)

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 (Abschnitt 6.5.16, S. 91).

awiebe
quelle
2
Die Frage ist mit C ++ und nicht mit C gekennzeichnet, was gut ist, da sich das Verhalten in C ++ 17 stark vom Verhalten in älteren Versionen unterscheidet - und in keiner Beziehung zum Verhalten in C11, C99, C90 usw. steht. Oder nur sehr wenig Beziehung dazu. Im Großen und Ganzen würde ich vorschlagen, dies zu entfernen. Noch wichtiger ist, dass wir die entsprechenden Fragen und Antworten für C finden und sicherstellen müssen, dass sie in Ordnung sind (und dass C ++ 17 insbesondere die Regeln ändert - das Verhalten in C ++ 11 und früher war mehr oder weniger das gleiche wie in C11, obwohl das Wort, das es in C beschreibt, immer noch 'Sequenzpunkte' verwendet, während C ++ 11 und höher dies nicht
Jonathan Leffler