Warum ist f (i = -1, i = -1) undefiniertes Verhalten?


267

Ich habe über Verstöße gegen die Reihenfolge der Bewertungen gelesen und sie geben ein Beispiel, das mich verwirrt.

1) Wenn eine Nebenwirkung auf ein Skalarobjekt relativ zu einer anderen Nebenwirkung auf dasselbe Skalarobjekt nicht sequenziert wird, ist das Verhalten undefiniert.

// snip
f(i = -1, i = -1); // undefined behavior

In diesem Zusammenhang ihandelt es sich um ein skalares Objekt , was anscheinend bedeutet

Arithmetische Typen (3.9.1), Aufzählungstypen, Zeigertypen, Zeiger auf Elementtypen (3.9.2), std :: nullptr_t und cv-qualifizierte Versionen dieser Typen (3.9.3) werden gemeinsam als Skalartypen bezeichnet.

Ich sehe nicht, wie mehrdeutig die Aussage in diesem Fall ist. Es scheint mir, dass unabhängig davon, ob das erste oder zweite Argument zuerst ausgewertet wird, als iendet -1und beide Argumente auch sind -1.

Kann jemand bitte klarstellen?


AKTUALISIEREN

Ich schätze die ganze Diskussion sehr. Bisher gefällt mir die Antwort von @ harmic sehr gut, da sie die Fallstricke und Feinheiten der Definition dieser Aussage aufdeckt, obwohl sie auf den ersten Blick so einfach aussieht. @ acheong87 weist auf einige Probleme hin, die bei der Verwendung von Referenzen auftreten, aber ich denke, dass dies orthogonal zum Aspekt der nicht sequenzierten Nebenwirkungen dieser Frage ist.


ZUSAMMENFASSUNG

Da diese Frage eine Menge Aufmerksamkeit erhielt, werde ich die wichtigsten Punkte / Antworten zusammenfassen. Lassen Sie mich zunächst einen kleinen Exkurs machen, um darauf hinzuweisen, dass "warum" eng verwandte, aber subtil unterschiedliche Bedeutungen haben kann, nämlich "aus welchem Grund ", "aus welchem Grund " und "zu welchem Zweck ". Ich werde die Antworten gruppieren, nach welcher dieser Bedeutungen des "Warum" sie angesprochen haben.

aus welchem ​​Grund

Die Hauptantwort hier kommt von Paul Draper , wobei Martin J eine ähnliche, aber nicht so umfassende Antwort beisteuert. Paul Drapers Antwort läuft darauf hinaus

Es ist ein undefiniertes Verhalten, da nicht definiert ist, wie das Verhalten ist.

Die Antwort ist insgesamt sehr gut in Bezug auf die Erklärung, was der C ++ - Standard sagt. Es werden auch einige verwandte Fälle von UB wie f(++i, ++i);und angesprochen f(i=1, i=-1);. Im ersten der verwandten Fälle ist nicht klar, ob das erste Argument sein sollte i+1und das zweite i+2oder umgekehrt; im zweiten ist nicht klar, ob inach dem Funktionsaufruf 1 oder -1 sein soll. Beide Fälle sind UB, da sie unter die folgende Regel fallen:

Wenn eine Nebenwirkung auf ein Skalarobjekt relativ zu einer anderen Nebenwirkung auf dasselbe Skalarobjekt nicht sequenziert wird, ist das Verhalten undefiniert.

Daher f(i=-1, i=-1)ist auch UB, da es unter die gleiche Regel fällt, obwohl die Absicht des Programmierers (IMHO) offensichtlich und eindeutig ist.

Paul Draper macht dies auch in seiner Schlussfolgerung deutlich

Könnte es ein definiertes Verhalten gewesen sein? Ja. Wurde es definiert? Nein.

was uns zu der Frage bringt: "Aus welchem ​​Grund / Zweck wurde f(i=-1, i=-1)als undefiniertes Verhalten zurückgelassen?"

aus welchem ​​Grund / Zweck

Obwohl der C ++ - Standard einige (möglicherweise nachlässige) Versehen enthält, sind viele Auslassungen gut begründet und dienen einem bestimmten Zweck. Obwohl mir bewusst ist, dass der Zweck oft entweder "die Arbeit des Compiler-Writers erleichtern" oder "schnellerer Code" ist, war ich hauptsächlich daran interessiert zu wissen, ob es einen guten Grund gibt, f(i=-1, i=-1) als UB zu gehen.

Harmic und Supercat liefern die Hauptantworten, die einen Grund für die UB liefern . Harmic weist darauf hin, dass ein optimierender Compiler die scheinbar atomaren Zuweisungsoperationen in mehrere Maschinenbefehle aufteilen und diese Befehle für eine optimale Geschwindigkeit weiter verschachteln könnte. Dies könnte zu einigen sehr überraschenden Ergebnissen führen: iendet in seinem Szenario als -2! Harmic zeigt also, wie das mehrmalige Zuweisen desselben Werts zu einer Variablen negative Auswirkungen haben kann, wenn die Operationen nicht sequenziert werden.

supercat bietet eine verwandte Darstellung der Fallstricke des Versuchs, das f(i=-1, i=-1)zu tun, wie es aussieht. Er weist darauf hin, dass es bei einigen Architekturen strenge Einschränkungen für mehrere gleichzeitige Schreibvorgänge auf dieselbe Speicheradresse gibt. Ein Compiler könnte es schwer haben, dies zu verstehen, wenn wir uns mit etwas weniger Trivialem befassen als f(i=-1, i=-1).

Davidf bietet auch ein Beispiel für Interleaving-Anweisungen, die denen von Harmic sehr ähnlich sind.

Obwohl jedes der Beispiele von Harmic, Supercat und Davidf etwas erfunden ist, liefern sie zusammengenommen immer noch einen konkreten Grund, warum f(i=-1, i=-1)undefiniertes Verhalten sein sollte.

Ich habe die Antwort von harmic akzeptiert, weil sie alle Bedeutungen des Warum am besten angesprochen hat, obwohl Paul Drapers Antwort den Teil "Aus welchem ​​Grund" besser angesprochen hat.

Andere Antwort

JohnB weist darauf hin, dass wir auch Probleme bekommen können , wenn wir überladene Zuweisungsoperatoren (anstelle von einfachen Skalaren) berücksichtigen.


1
Ein Skalarobjekt ist ein Objekt vom Skalartyp. Siehe 3.9 / 9: „Arithmetic Typen (3.9.1), Aufzählungstypen, Zeigertypen, Zeiger auf Elementtypen (3.9.2), std::nullptr_tund cv-qualifizierte Versionen dieser Arten (3.9.3) werden zusammenfassend als skalare Typen . ""
Rob Kennedy

1
Vielleicht gibt es einen Fehler auf der Seite, und sie bedeuteten tatsächlich f(i-1, i = -1)oder etwas Ähnliches.
Herr Lister

Schauen Sie sich diese Frage an: stackoverflow.com/a/4177063/71074
Robert S. Barnes

@ RobKennedy Danke. Enthält "arithmetische Typen" Bool?
Nicu Stiurca

1
SchighSchagh Ihr Update sollte im Antwortbereich sein.
Grijesh Chauhan

Antworten:


343

Da die Operationen nicht sequenziert sind, gibt es nichts zu sagen, dass die Anweisungen, die die Zuweisung ausführen, nicht verschachtelt werden können. Abhängig von der CPU-Architektur ist dies möglicherweise optimal. Auf der Seite, auf die verwiesen wird, heißt es:

Wenn A nicht vor B sequenziert wird und B nicht vor A sequenziert wird, gibt es zwei Möglichkeiten:

  • Auswertungen von A und B sind nicht sequenziert: Sie können in beliebiger Reihenfolge durchgeführt werden und sich überschneiden (innerhalb eines einzelnen Ausführungsthreads kann der Compiler die CPU-Anweisungen, aus denen A und B bestehen, verschachteln).

  • Die Bewertungen von A und B sind unbestimmt sequenziert: Sie können in beliebiger Reihenfolge durchgeführt werden, dürfen sich jedoch nicht überlappen: Entweder ist A vor B vollständig oder B ist vor A abgeschlossen. Die Reihenfolge kann beim nächsten Mal des gleichen Ausdrucks umgekehrt sein wird ausgewertet.

Das allein scheint kein Problem zu verursachen - vorausgesetzt, die ausgeführte Operation speichert den Wert -1 in einem Speicherort. Es gibt aber auch nichts zu sagen, dass der Compiler dies nicht in einen separaten Befehlssatz optimieren kann, der den gleichen Effekt hat, der jedoch fehlschlagen könnte, wenn der Vorgang mit einem anderen Vorgang am selben Speicherort verschachtelt würde.

Stellen Sie sich zum Beispiel vor, es wäre effizienter, den Speicher auf Null zu setzen und dann zu dekrementieren, als den Wert -1 in zu laden. Dann:

f(i=-1, i=-1)

könnte werden:

clear i
clear i
decr i
decr i

Jetzt bin ich -2.

Es ist wahrscheinlich ein falsches Beispiel, aber es ist möglich.


59
Sehr schönes Beispiel dafür, wie der Ausdruck tatsächlich etwas Unerwartetes bewirken kann, während er den Sequenzierungsregeln entspricht. Ja, ein bisschen erfunden, aber auch der Code, nach dem ich frage, ist abgeschnitten. :)
Nicu Stiurca

10
Und selbst wenn die Zuweisung als atomare Operation ausgeführt wird, ist es möglich, sich eine superskalare Architektur vorzustellen, bei der beide Zuweisungen gleichzeitig vorgenommen werden und einen Speicherzugriffskonflikt verursachen, der zu einem Fehler führt. Die Sprache ist so konzipiert, dass Compiler-Autoren die Vorteile des Zielcomputers so frei wie möglich nutzen können.
Ach

11
Ich mag Ihr Beispiel wirklich, wie selbst das Zuweisen des gleichen Werts zu derselben Variablen in beiden Parametern zu einem unerwarteten Ergebnis führen kann, da die beiden Zuweisungen nicht sequenziert werden
Martin J.

1
+ 1e + 6 (ok, +1) für den Punkt, dass kompilierter Code nicht immer das ist, was Sie erwarten würden. Optimierer sind wirklich gut darin, diese Art von Kurven auf dich zu werfen, wenn du die Regeln nicht befolgst: P
Corey

3
Auf dem Arm-Prozessor kann eine 32-Bit-Last bis zu 4 Anweisungen aufnehmen: load 8bit immediate and shiftBis zu 4-mal. Normalerweise führt der Compiler eine indirekte Adressierung durch, um eine Zahl aus einer Tabelle abzurufen und dies zu vermeiden. (-1 kann in 1 Anweisung ausgeführt werden, es kann jedoch auch ein anderes Beispiel ausgewählt werden).
Strg-Alt-Delor

208

Erstens : „skalare Objekt“ ist eine Art wie ein int, floatoder ein Zeiger (siehe Was ist ein Skalar Objekt in C ++? ).


Zweitens mag es offensichtlicher erscheinen, dass

f(++i, ++i);

hätte undefiniertes Verhalten. Aber

f(i = -1, i = -1);

ist weniger offensichtlich.

Ein etwas anderes Beispiel:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

Welche Aufgabe geschah "zuletzt" i = 1, oder i = -1? Es ist nicht im Standard definiert. Wirklich, das ikönnte sein 5(siehe die Antwort von harmic für eine völlig plausible Erklärung, wie dies der Fall sein könnte). Oder Sie programmieren Segfault. Oder formatieren Sie Ihre Festplatte neu.

Aber jetzt fragen Sie: "Was ist mit meinem Beispiel? Ich habe -1für beide Aufgaben den gleichen Wert ( ) verwendet. Was könnte daran möglicherweise unklar sein?"

Sie haben Recht ... außer in der Weise, wie das C ++ - Standardkomitee dies beschrieben hat.

Wenn eine Nebenwirkung auf ein Skalarobjekt relativ zu einer anderen Nebenwirkung auf dasselbe Skalarobjekt nicht sequenziert wird, ist das Verhalten undefiniert.

Sie konnten haben eine besondere Ausnahme für Ihren speziellen Fall gemacht, aber sie tat es nicht. (Und warum sollten sie? Welchen Nutzen hätte das jemals möglicherweise?) Also ikönnte es immer noch sein 5. Oder Ihre Festplatte könnte leer sein. Die Antwort auf Ihre Frage lautet also:

Es ist ein undefiniertes Verhalten, da nicht definiert ist, wie das Verhalten ist.

(Dies verdient Hervorhebung, da viele Programmierer denken, dass "undefiniert" "zufällig" oder "unvorhersehbar" bedeutet. Dies ist nicht der Fall. Es bedeutet, dass es nicht durch den Standard definiert ist. Das Verhalten könnte 100% konsistent sein und dennoch undefiniert.)

Könnte es ein definiertes Verhalten gewesen sein? Ja. Wurde es definiert? Nein, daher ist es "undefiniert".

"Undefiniert" bedeutet jedoch nicht, dass ein Compiler Ihre Festplatte formatiert. Es bedeutet, dass dies ein standardkonformer Compiler sein könnte und immer noch wäre. Realistisch bin ich sicher, dass g ++, Clang und MSVC alle das tun werden, was Sie erwartet haben. Sie würden einfach nicht "müssen".


Eine andere Frage könnte sein, warum sich das C ++ - Standardkomitee dafür entschieden hat, diesen Nebeneffekt nicht zu berücksichtigen. . Diese Antwort wird die Geschichte und die Meinungen des Ausschusses beinhalten. Oder Was nützt es, wenn diese Nebenwirkung in C ++ nicht sequenziert wird? Dies lässt jede Rechtfertigung zu, unabhängig davon, ob es sich um die eigentliche Begründung des Normungsausschusses handelte oder nicht. Sie können diese Fragen hier oder unter programmers.stackexchange.com stellen.


9
@hvd, ja, tatsächlich weiß ich, dass wenn Sie -Wsequence-pointfür g ++ aktivieren , es Sie warnen wird.
Paul Draper

47
"Ich bin sicher, dass g ++, Clang und MSVC alle das tun werden, was Sie erwartet haben." Ich würde einem modernen Compiler nicht vertrauen. Sie sind böse. Beispielsweise können sie erkennen, dass dies ein undefiniertes Verhalten ist, und annehmen, dass dieser Code nicht erreichbar ist. Wenn sie dies heute nicht tun, könnten sie es morgen tun. Jede UB ist eine tickende Zeitbombe.
CodesInChaos

8
@BlacklightShining "Ihre Antwort ist schlecht, weil sie nicht gut ist" ist kein sehr nützliches Feedback, oder?
Vincent van der Weele

13
@BobJarvis Die Kompilierung ist absolut nicht verpflichtet, bei undefiniertem Verhalten auch nur entfernt korrekten Code zu generieren. Es kann sogar davon ausgegangen werden, dass dieser Code niemals aufgerufen wird, und somit das Ganze durch ein NOP ersetzen (Beachten Sie, dass Compiler solche Annahmen tatsächlich gegenüber UB treffen). Daher würde ich sagen, dass die richtige Reaktion auf einen solchen Fehlerbericht nur "geschlossen werden kann, funktioniert wie beabsichtigt"
Grizzly

7
@SchighSchagh Manchmal brauchen die Leute eine Neuformulierung der Begriffe (die nur oberflächlich betrachtet eine tautologische Antwort zu sein scheint). Die meisten Leute, die mit technischen Spezifikationen noch nicht vertraut undefined behaviorsind something random will happen, denken Mittel , was die meiste Zeit nicht der Fall ist.
Izkata

27

Ein praktischer Grund, keine Ausnahme von den Regeln zu machen, nur weil die beiden Werte gleich sind:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

Betrachten Sie den Fall, in dem dies erlaubt war.

Jetzt, einige Monate später, besteht das Bedürfnis, sich zu ändern

 #define VALUEB 2

Scheinbar harmlos, nicht wahr? Und doch würde prog.cpp plötzlich nicht mehr kompilieren. Wir sind jedoch der Meinung, dass die Zusammenstellung nicht vom Wert eines Literals abhängen sollte.

Fazit: Es gibt keine Ausnahme von der Regel, da dadurch eine erfolgreiche Kompilierung vom Wert (eher vom Typ) einer Konstanten abhängt.

BEARBEITEN

@HeartWare wies darauf hin, dass konstante Ausdrücke des Formulars A DIV Bin einigen Sprachen nicht zulässig sind, wenn B0 ist, und dass die Kompilierung fehlschlägt. Daher kann das Ändern einer Konstante an einer anderen Stelle zu Kompilierungsfehlern führen. Was meiner Meinung nach unglücklich ist. Aber es ist sicherlich gut, solche Dinge auf das Unvermeidliche zu beschränken.


Sicher, aber das Beispiel macht Gebrauch Ganzzahlliterale. Ihr f(i = VALUEA, i = VALUEB);hat definitiv das Potenzial für undefiniertes Verhalten. Ich hoffe, Sie codieren nicht wirklich gegen Werte hinter Bezeichnern.
Wolf

3
@Wold Der Compiler sieht jedoch keine Präprozessor-Makros. Und selbst wenn dies nicht so wäre, ist es schwierig, ein Beispiel in einer Programmiersprache zu finden, in der ein Quellcode kompiliert wird, bis eine int-Konstante von 1 auf 2 geändert wird. Dies ist einfach inakzeptabel und unerklärlich, während Sie hier sehr gute Erklärungen sehen warum dieser Code auch mit den gleichen Werten kaputt ist.
Ingo

Ja, die Kompilierungen sehen keine Makros. Aber war das die Frage?
Wolf

1
Ihrer Antwort fehlt der Punkt, lesen Sie die Antwort von Harmic und den Kommentar des OP dazu.
Wolf

1
Es könnte reichen SomeProcedure(A, B, B DIV (2-A)). Wenn die Sprache besagt, dass CONST zum Zeitpunkt der Kompilierung vollständig evaluiert werden muss, ist meine Behauptung für diesen Fall natürlich nicht gültig. Da es irgendwie die Unterscheidung zwischen Kompilierungszeit und Laufzeit verwischt. Würde es auch auffallen, wenn wir schreiben CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ? Oder sind Funktionen nicht erlaubt?
Ingo

12

Die Verwirrung besteht darin, dass das Speichern eines konstanten Werts in einer lokalen Variablen nicht eine atomare Anweisung für jede Architektur ist, auf der das C ausgeführt werden soll. Der Prozessor, auf dem der Code ausgeführt wird, ist in diesem Fall wichtiger als der Compiler. Zum Beispiel benötigt auf ARM, wo jeder Befehl keine vollständige 32-Bit-Konstante tragen kann, das Speichern eines int in einer Variablen mehr als einen Befehl. Beispiel mit diesem Pseudocode, bei dem Sie jeweils nur 8 Bit speichern können und in einem 32-Bit-Register arbeiten müssen, ist i ein int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

Sie können sich vorstellen, dass der Compiler, wenn er ihn optimieren möchte, dieselbe Sequenz zweimal verschachteln kann und Sie nicht wissen, welcher Wert in i geschrieben wird. und sagen wir mal, dass er nicht sehr schlau ist:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

In meinen Tests ist gcc jedoch so freundlich zu erkennen, dass der gleiche Wert zweimal verwendet und einmal generiert wird und nichts Seltsames tut. Ich bekomme -1, -1 Aber mein Beispiel ist immer noch gültig, da es wichtig ist zu berücksichtigen, dass selbst eine Konstante möglicherweise nicht so offensichtlich ist, wie es scheint.


Ich nehme an, dass der Compiler auf ARM nur die Konstante aus einer Tabelle lädt. Was Sie beschreiben, scheint eher MIPS zu sein.
Ach

1
@AndreyChernyakhovskiy Yep, aber in einem Fall, in dem es nicht einfach ist -1(dass der Compiler irgendwo gespeichert hat), sondern es ist eher 3^81 mod 2^32, aber konstant, dann könnte der Compiler genau das tun, was hier getan wird, und in einem Hebel der Omtimierung verschachtele ich die Aufrufsequenzen zu vermeiden, zu warten.
Yo

@tohecz, ja, ich habe es schon überprüft. In der Tat ist der Compiler zu intelligent, um jede Konstante aus einer Tabelle zu laden. Auf jeden Fall würde es niemals dasselbe Register verwenden, um die beiden Konstanten zu berechnen. Dies würde das definierte Verhalten genauso sicher "undefinieren".
Ach

@AndreyChernyakhovskiy Aber Sie sind wahrscheinlich nicht "jeder C ++ - Compiler-Programmierer der Welt". Denken Sie daran, dass Maschinen mit 3 kurzen Registern nur für Berechnungen zur Verfügung stehen.
yo

@tohecz, betrachten Sie das Beispiel, f(i = A, j = B)in dem iund jsind zwei separate Objekte. Dieses Beispiel hat keine UB. Eine Maschine mit 3 kurzen Registern ist keine Entschuldigung für den Compiler, die beiden Werte von Aund Bin demselben Register zu mischen (wie in der Antwort von @ davidf gezeigt), da dies die Programmsemantik brechen würde.
Ach

11

Verhalten wird üblicherweise als undefiniert angegeben, wenn es einen denkbaren Grund gibt, warum ein Compiler, der versucht hat, "hilfreich" zu sein, etwas tun könnte, das zu völlig unerwartetem Verhalten führen würde.

In dem Fall, in dem eine Variable mehrmals geschrieben wird, ohne dass sichergestellt werden muss, dass die Schreibvorgänge zu unterschiedlichen Zeiten ausgeführt werden, können bei einigen Hardwaretypen mehrere "Speicher" -Operationen gleichzeitig an verschiedenen Adressen unter Verwendung eines Dual-Port-Speichers ausgeführt werden. Einige Dual-Port-Speicher verbieten jedoch ausdrücklich das Szenario, in dem zwei Speicher gleichzeitig dieselbe Adresse erreichen, unabhängig davon, ob die geschriebenen Werte übereinstimmen oder nicht. Wenn ein Compiler für einen solchen Computer zwei nicht sequenzierte Versuche bemerkt, dieselbe Variable zu schreiben, kann er entweder die Kompilierung verweigern oder sicherstellen, dass die beiden Schreibvorgänge nicht gleichzeitig geplant werden können. Wenn jedoch einer oder beide Zugriffe über einen Zeiger oder eine Referenz erfolgen, kann der Compiler möglicherweise nicht immer feststellen, ob beide Schreibvorgänge möglicherweise denselben Speicherort erreichen. In diesem Fall werden die Schreibvorgänge möglicherweise gleichzeitig geplant, wodurch beim Zugriffsversuch ein Hardware-Trap verursacht wird.

Die Tatsache, dass jemand einen C-Compiler auf einer solchen Plattform implementieren könnte, bedeutet natürlich nicht, dass ein solches Verhalten nicht auf Hardwareplattformen definiert werden sollte, wenn Speicher von Typen verwendet werden, die klein genug sind, um atomar verarbeitet zu werden. Der Versuch, zwei verschiedene Werte in nicht sequenzierter Weise zu speichern, kann zu Verrücktheit führen, wenn ein Compiler sich dessen nicht bewusst ist. zum Beispiel gegeben:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

Wenn der Compiler den Aufruf von "moo" leitet und feststellt, dass er "v" nicht ändert, speichert er möglicherweise eine 5 bis v, dann eine 6 bis * p, übergibt dann 5 an "zoo" und dann Übergebe den Inhalt von v an "zoo". Wenn "zoo" "v" nicht ändert, sollten die beiden Aufrufe auf keinen Fall unterschiedliche Werte erhalten, aber das könnte trotzdem leicht passieren. Andererseits könnte in Fällen, in denen beide Geschäfte den gleichen Wert schreiben würden, eine solche Verrücktheit nicht auftreten, und auf den meisten Plattformen gäbe es keinen vernünftigen Grund für eine Implementierung, etwas Seltsames zu tun. Leider brauchen einige Compiler-Autoren keine Entschuldigung für alberne Verhaltensweisen, die über "weil der Standard es erlaubt" hinausgehen, so dass selbst diese Fälle nicht sicher sind.


9

Die Tatsache, dass das Ergebnis in diesem Fall in den meisten Implementierungen dasselbe wäre, ist zufällig. Die Reihenfolge der Auswertung ist noch undefiniert. Bedenken Sie f(i = -1, i = -2): Hier kommt es auf die Reihenfolge an. Der einzige Grund, warum es in Ihrem Beispiel keine Rolle spielt, ist der Unfall, dass beide Werte sind -1.

Da der Ausdruck als Ausdruck mit einem undefinierten Verhalten angegeben wird, zeigt ein böswillig kompatibler Compiler möglicherweise ein unangemessenes Bild an, wenn Sie f(i = -1, i = -1)die Ausführung auswerten und abbrechen - und wird dennoch als vollständig korrekt angesehen. Zum Glück sind mir keine Compiler bekannt.


8

Es scheint mir die einzige Regel zu sein, die sich auf die Sequenzierung des Funktionsargumentausdrucks bezieht:

3) Beim Aufrufen einer Funktion (unabhängig davon, ob die Funktion inline ist oder nicht und ob die explizite Funktionsaufrufsyntax verwendet wird oder nicht) wird jede Wertberechnung und jeder Nebeneffekt, der einem Argumentausdruck oder dem Postfix-Ausdruck zugeordnet ist, der die aufgerufene Funktion bezeichnet, angegeben vor der Ausführung jedes Ausdrucks oder jeder Anweisung im Hauptteil der aufgerufenen Funktion sequenziert.

Dies definiert keine Sequenzierung zwischen Argumentausdrücken, daher enden wir in diesem Fall:

1) Wenn eine Nebenwirkung auf ein Skalarobjekt relativ zu einer anderen Nebenwirkung auf dasselbe Skalarobjekt nicht sequenziert wird, ist das Verhalten undefiniert.

In der Praxis läuft das von Ihnen angegebene Beispiel auf den meisten Compilern einwandfrei (im Gegensatz zum "Löschen Ihrer Festplatte" und anderen theoretischen undefinierten Verhaltensfolgen).
Dies ist jedoch eine Haftung, da es vom spezifischen Compilerverhalten abhängt, auch wenn die beiden zugewiesenen Werte gleich sind. Wenn Sie versuchen, andere Werte zuzuweisen, sind die Ergebnisse natürlich "wirklich" undefiniert:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}

8

C ++ 17 definiert strengere Bewertungsregeln. Insbesondere werden Funktionsargumente sequenziert (obwohl in nicht spezifizierter Reihenfolge).

N5659 §4.6:15
Die Bewertungen A und B werden unbestimmt sequenziert, wenn entweder A vor B sequenziert wird oder B vor A sequenziert wird , aber es ist nicht spezifiziert, welche. [ Hinweis : Unbestimmt sequenzierte Auswertungen können sich nicht überschneiden, können jedoch zuerst ausgeführt werden. - Endnote ]

N5659 § 8.2.2:5
Die Initialisierung eines Parameters, einschließlich aller zugehörigen Wertberechnungen und Nebenwirkungen, wird in Bezug auf die eines anderen Parameters unbestimmt sequenziert.

Es erlaubt einige Fälle, die vorher UB wären:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one

2
Vielen Dank, dass Sie dieses Update für C ++ 17 hinzugefügt haben , sodass ich es nicht musste. ;)
Yakk - Adam Nevraumont

Super, vielen Dank für diese Antwort. Leichte Nachverfolgung: Wenn fdie Signatur wäre f(int a, int b), garantiert C ++ 17 dies a == -1und b == -2wenn es wie im zweiten Fall aufgerufen wird?
Nicu Stiurca

Ja. Wenn wir Parameter haben aund bdann entweder i-then- aauf -1 initialisiert werden, danach i-then- bauf -2 initialisiert werden oder umgekehrt. In beiden Fällen haben wir a == -1und b == -2. Zumindest habe ich so gelesen: " Die Initialisierung eines Parameters, einschließlich aller zugehörigen Wertberechnungen und Nebenwirkungen, ist in Bezug auf die eines anderen Parameters unbestimmt sequenziert ."
AlexD

Ich denke, dass es in C seit Ewigkeiten dasselbe ist.
Fuz

5

Der Zuweisungsoperator könnte überlastet sein. In diesem Fall könnte die Reihenfolge von Bedeutung sein:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true

1
Richtig, aber die Frage betraf Skalartypen , auf die andere hingewiesen haben, bedeutet im Wesentlichen int family, float family und pointers.
Nicu Stiurca

Das eigentliche Problem in diesem Fall ist, dass der Zuweisungsoperator statusbehaftet ist, sodass selbst eine regelmäßige Manipulation der Variablen für solche Probleme anfällig ist.
AJMansfield

2

Dies ist nur die Antwort auf die Frage "Ich bin nicht sicher, was" skalares Objekt "neben so etwas wie einem Int oder einem Float bedeuten könnte".

Ich würde das "Skalarobjekt" als Abkürzung für "Skalartypobjekt" oder einfach als "Skalartypvariable" interpretieren. Dann pointer, enum(konstant) von skalaren Typ.

Dies ist ein MSDN-Artikel von Scalar Types .


Dies liest sich ein bisschen wie eine "Nur-Link-Antwort". Können Sie die relevanten Bits von diesem Link in diese Antwort kopieren (in einem Blockzitat)?
Cole Johnson

1
@ColeJohnson Dies ist keine Link-Antwort. Der Link dient nur zur weiteren Erläuterung. Meine Antwort lautet "Zeiger", "Aufzählung".
Peng Zhang

Ich habe nicht gesagt , Ihre Antwort war ein Link nur beantworten. Ich sagte, es "liest sich wie [eins]" . Ich schlage vor, Sie lesen im Hilfebereich nach, warum wir nicht nur Antworten mit Links wünschen. Der Grund dafür ist, dass dieser Link unterbrochen wird, wenn Microsoft seine URLs auf seiner Website aktualisiert.
Cole Johnson

1

Tatsächlich gibt es einen Grund, nicht davon abhängig zu sein, dass der Compiler prüft, ob iderselbe Wert zweimal zugewiesen wurde, sodass er durch eine einzelne Zuweisung ersetzt werden kann. Was ist, wenn wir einige Ausdrücke haben?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}

1
Sie müssen den Satz von Fermat nicht beweisen: Weisen Sie ihn einfach 1zu i. Entweder weisen beide Argumente zu 1und dies bewirkt das "Richtige", oder die Argumente weisen unterschiedliche Werte zu und es ist ein undefiniertes Verhalten, sodass unsere Auswahl weiterhin zulässig ist.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.