Es ist undefiniert, da es x
zwischen Sequenzpunkten zweimal geändert wird. Der Standard sagt, es ist undefiniert, daher ist es undefiniert.
Soviel weiß ich.
Aber wieso?
Meines Wissens nach können Compiler besser optimieren, wenn dies untersagt wird. Dies hätte Sinn machen können, als C erfunden wurde, aber jetzt scheint es ein schwaches Argument zu sein.
Wenn wir C heute neu erfinden würden, würden wir es dann so machen, oder könnte es besser gemacht werden?
Oder gibt es ein tieferes Problem, das es schwierig macht, konsistente Regeln für solche Ausdrücke zu definieren, also ist es am besten, sie zu verbieten?
Nehmen wir also an, wir würden C heute neu erfinden. Ich möchte einfache Regeln für Ausdrücke wie vorschlagen x=x++
, die meiner Meinung nach besser funktionieren als die vorhandenen Regeln.
Ich würde gerne Ihre Meinung zu den vorgeschlagenen Regeln im Vergleich zu den vorhandenen oder zu anderen Vorschlägen einholen.
Vorgeschlagene Regeln:
- Zwischen den Sequenzpunkten ist die Reihenfolge der Auswertung nicht angegeben.
- Nebenwirkungen treten sofort auf.
Es ist kein undefiniertes Verhalten beteiligt. Ausdrücke werden auf diesen oder jenen Wert ausgewertet, formatieren Ihre Festplatte jedoch nicht (seltsamerweise habe ich noch nie eine Implementierung gesehen, bei der x=x++
die Festplatte formatiert wird).
Beispielausdrücke
x=x++
- Gut definiert, ändert sich nichtx
.
Zuerstx
wird inkrementiert (sofort, wennx++
ausgewertet wird), dann wird der alte Wert in gespeichertx
.x++ + ++x
- Inkrementiertx
zweimal, ergibt2*x+2
.
Obwohl jede Seite zuerst bewertet werden kann, ist das Ergebnis entwederx + (x+2)
(linke Seite zuerst) oder(x+1) + (x+1)
(rechte Seite zuerst).x = x + (x=3)
- Nicht angegeben,x
auf entwederx+3
oder eingestellt6
.
Wenn die rechte Seite zuerst ausgewertet wird, ist esx+3
. Es ist auch möglich, dassx=3
zuerst ausgewertet wird, also ist es3+3
. In beiden Fällen erfolgt diex=3
Zuweisung sofortx=3
nach der Auswertung, sodass der gespeicherte Wert durch die andere Zuweisung überschrieben wird.x+=(x=3)
- Gut definiert, setztx
auf 6.
Sie könnten argumentieren, dass dies nur eine Abkürzung für den obigen Ausdruck ist.
Aber ich würde sagen, das+=
muss nachher ausgeführtx=3
werden und nicht in zwei Teilen (lesenx
, auswertenx=3
, addieren und neuen Wert speichern).
Was ist der vorteil
Einige Kommentare haben diesen guten Punkt angesprochen.
Ich denke nicht, dass Ausdrücke, wie x=x++
sie in einem normalen Code verwendet werden sollten.
Eigentlich bin ich viel strenger als das - ich halte die einzig gute Verwendung für rein x++
wie x++;
alleine.
Ich denke jedoch, dass die Sprachregeln so einfach wie möglich sein müssen. Sonst verstehen sie die Programmierer einfach nicht. Die Regel, die das zweimalige Ändern einer Variablen zwischen Sequenzpunkten verbietet, ist sicherlich eine Regel, die die meisten Programmierer nicht verstehen.
Eine sehr grundlegende Regel lautet:
Wenn A gültig ist und B gültig ist und sie auf gültige Weise kombiniert werden, ist das Ergebnis gültig.
x
ist ein gültiger L-Wert, x++
ein gültiger Ausdruck und =
eine gültige Möglichkeit, einen L-Wert und einen Ausdruck zu kombinieren. Warum x=x++
ist das nicht zulässig?
Der C-Standard macht hier eine Ausnahme, und diese Ausnahme kompliziert die Regeln. Sie können stackoverflow.com durchsuchen und sehen, wie sehr diese Ausnahme die Menschen verwirrt.
Also sage ich - befreie dich von dieser Verwirrung.
=== Zusammenfassung der Antworten ===
Warum das tun?
Ich habe versucht, dies im obigen Abschnitt zu erklären - ich möchte, dass C-Regeln einfach sind.Optimierungspotential:
Dies nimmt dem Compiler zwar etwas Freiheit, aber ich habe nichts gesehen, was mich davon überzeugt hätte, dass es von Bedeutung sein könnte.
Die meisten Optimierungen können noch durchgeführt werden. Beispielsweisea=3;b=5;
kann nachbestellt werden, obwohl der Standard die Reihenfolge festlegt. Ausdrücke wiea=b[i++]
können noch ähnlich optimiert werden.Sie können den vorhandenen Standard nicht ändern.
Ich gebe zu, ich kann nicht. Ich hätte nie gedacht, dass ich tatsächlich Standards und Compiler ändern kann. Ich wollte nur darüber nachdenken, ob die Dinge anders hätten gemacht werden können.
quelle
x
sich selbst zuzuweisen , und wenn Sie inkrementieren möchten,x
können Sie einfach sagenx++;
- die Zuweisung ist nicht erforderlich. Ich würde sagen, es sollte nicht definiert werden, nur weil es schwierig ist, sich daran zu erinnern, was passieren soll.Antworten:
Vielleicht solltest du zuerst die Frage beantworten, warum es definiert werden soll? Gibt es einen Vorteil in Bezug auf Programmierstil, Lesbarkeit, Wartbarkeit oder Leistung, wenn solche Ausdrücke mit zusätzlichen Nebenwirkungen kombiniert werden? Ist
lesbarer als
Angesichts dessen, dass eine solche Änderung äußerst grundlegend ist und die bestehende Codebasis verletzt.
quelle
Das Argument, dass dieses undefinierte Verhalten eine bessere Optimierung ermöglicht, ist heute nicht schwach. Tatsächlich ist es heute viel stärker als damals, als C neu war.
Als C neu war, waren Maschinen, die dies für eine bessere Optimierung nutzen konnten, meist theoretische Modelle. Es wurde über die Möglichkeit gesprochen, CPUs zu bauen, bei denen der Compiler die CPU anweist, welche Anweisungen parallel zu anderen Anweisungen ausgeführt werden können / sollen. Sie wiesen auf die Tatsache hin, dass das Zulassen eines undefinierten Verhaltens dazu führte, dass Sie auf einer solchen CPU, falls es jemals wirklich existierte, den "Inkrement" -Teil des Befehls so einplanen konnten, dass er parallel zum Rest des Befehlsstroms ausgeführt wird. Während sie in Bezug auf die Theorie recht hatten, gab es zu der Zeit wenig Hardware, die diese Möglichkeit wirklich ausnutzen konnte.
Das ist nicht mehr nur theoretisch. Jetzt gibt es Hardware in der Produktion und in großem Umfang im Einsatz (z. B. Itanium-, VLIW-DSPs), die diese Vorteile wirklich nutzen können. Sie wirklich tun , kann der Compiler einen Befehlsstrom zu erzeugen, der angibt , daß die Befehle X, Y und Z können alle parallel ausgeführt werden. Dies ist kein theoretisches Modell mehr - es ist echte Hardware, die im realen Einsatz echte Arbeit leistet.
IMO, dieses definierte Verhalten zu erzeugen, ist nahezu die schlechteste "Lösung" für das Problem. Sie sollten solche Ausdrücke eindeutig nicht verwenden. Für die überwiegende Mehrheit des Codes wäre es das ideale Verhalten, wenn der Compiler solche Ausdrücke einfach vollständig ablehnt. Zu dieser Zeit haben C-Compiler nicht die Flussanalyse durchgeführt, die erforderlich war, um dies zuverlässig zu erkennen. Selbst zum Zeitpunkt der ursprünglichen C-Norm war dies noch gar nicht üblich.
Ich bin mir auch heute nicht sicher, ob dies für die Community akzeptabel ist - während viele Compiler diese Art der Flussanalyse durchführen können, tun sie dies in der Regel nur, wenn Sie eine Optimierung anfordern. Ich bezweifle, dass die meisten Programmierer die Idee, "Debug" -Builds zu verlangsamen, nur um Code ablehnen zu können, den sie (weil sie vernünftig sind) niemals schreiben würden.
Was C getan hat, ist eine halbwegs vernünftige zweitbeste Wahl: Sagen Sie den Leuten, dass sie das nicht tun sollen, und erlauben Sie dem Compiler, den Code abzulehnen (aber nicht zu verlangen). Dies verhindert (noch weiter), dass die Kompilierung für Benutzer verlangsamt wird, die sie nie verwenden würden, ermöglicht es jedoch jemandem, einen Compiler zu schreiben, der solchen Code ablehnt, wenn er möchte (und / oder Flags hat, die ihn ablehnen, die Benutzer verwenden können) oder nicht, wie sie es für richtig halten).
Zumindest bei IMO wäre es (zumindest in der Nähe) die schlechteste Entscheidung , dieses definierte Verhalten zu treffen. Bei Hardware im VLIW-Stil sollten Sie langsameren Code für die sinnvolle Verwendung der Inkrement-Operatoren generieren, nur um sie zu missbrauchen, oder immer eine ausführliche Ablaufanalyse durchführen, um zu beweisen, dass Sie sich nicht damit befassen beschissener Code, so dass Sie den langsamen (serialisierten) Code nur dann produzieren können, wenn es wirklich notwendig ist.
Fazit: Wenn Sie dieses Problem beheben möchten, sollten Sie in die entgegengesetzte Richtung denken. Anstatt zu definieren, was ein solcher Code tut, sollten Sie die Sprache definieren, damit solche Ausdrücke überhaupt nicht erlaubt sind (und mit der Tatsache leben, dass sich die meisten Programmierer wahrscheinlich für eine schnellere Kompilierung entscheiden, anstatt diese Anforderung durchzusetzen).
quelle
a=b[i++];
(zum Beispiel) ist in Ordnung und das Optimieren ist eine gute Sache. Ich sehe jedoch keinen Sinn darin, vernünftigen Code so zu verletzen, nur damit so etwas++i++
eine definierte Bedeutung hat.++i++
ist genau, dass es im Allgemeinen schwierig ist, sie von gültigen Ausdrücken mit Nebenwirkungen (wiea=b[i++]
) zu unterscheiden. Es mag für uns einfach genug erscheinen, aber wenn ich mich richtig an das Drachenbuch erinnere , ist es tatsächlich ein NP-hartes Problem. Aus diesem Grund ist dieses Verhalten nicht verboten, sondern UB.Eric Lippert, Chefdesigner im C # -Compiler-Team, veröffentlichte in seinem Blog einen Artikel über eine Reihe von Überlegungen, die dazu führen, dass ein Feature auf der Ebene der Sprachspezifikationen nicht definiert wird. Offensichtlich ist C # eine andere Sprache, wobei verschiedene Faktoren in das Sprachdesign einfließen, aber die Punkte, die er hervorhebt, sind dennoch relevant.
Insbesondere weist er auf die Frage hin, ob es Compiler für eine Sprache gibt, die bereits implementiert sind und Vertreter in einem Ausschuss haben. Ich bin mir nicht sicher, ob dies der Fall ist, bin mir aber für die meisten C- und C ++ -bezogenen Spezifikationsdiskussionen relevant.
Bemerkenswert ist auch, wie Sie sagten, das Leistungspotential für die Compileroptimierung. Zwar ist die Leistung von CPUs heutzutage um ein Vielfaches höher als zu Zeiten, als C noch jung war, doch wird ein Großteil der C-Programmierung heutzutage aufgrund des möglichen Leistungsgewinns und des Potenzials für eine (hypothetische) Zukunft durchgeführt ) CPU-Befehlsoptimierungen und Multicore-Verarbeitungsoptimierungen wären wegen eines übermäßig restriktiven Regelsatzes für den Umgang mit Nebenwirkungen und Sequenzpunkten dumm auszuschließen.
quelle
Schauen wir uns zunächst die Definition von undefiniertem Verhalten an:
Mit anderen Worten, undefiniertes Verhalten bedeutet einfach, dass der Compiler frei ist, die Situation so zu handhaben, wie er möchte, und eine solche Aktion wird als "korrekt" betrachtet.
Die Wurzel des zur Diskussion stehenden Problems ist die folgende Klausel:
Betonung hinzugefügt.
Gegeben ein Ausdruck wie
die Unterausdrücke
a++
,--b
,c
, und++d
kann ausgewertet werden , in beliebiger Reihenfolge . Darüber hinaus können die Nebenwirkungen vona++
,--b
und++d
vor dem nächsten Sequenzpunkt an einer beliebigen Stelle angewendet werden (IOW, auch wenna++
vor ausgewertet wird--b
, ist es nicht garantiert , dassa
wird aktualisiert , bevor--b
ausgewertet). Wie andere bereits gesagt haben, ist es der Grund für dieses Verhalten, der Implementierung die Freiheit zu geben, Operationen auf optimale Weise neu zu ordnen.Aus diesem Grund jedoch Ausdrücke wie
usw. ergeben unterschiedliche Ergebnisse für unterschiedliche Implementierungen (oder für dieselbe Implementierung mit unterschiedlichen Optimierungseinstellungen oder basierend auf dem umgebenden Code usw.).
Das Verhalten bleibt undefiniert, so dass der Compiler nicht verpflichtet ist, "das Richtige zu tun", wie auch immer dies sein mag. Die oben genannten Fälle sind leicht zu fassen, aber es gibt eine nicht unbedeutende Anzahl von Fällen, die zum Zeitpunkt der Kompilierung schwer bis unmöglich zu fassen wären.
Offensichtlich Sie können eine Sprache gestalten, dass Reihenfolge der Auswertung und der Reihenfolge , in der Nebenwirkungen angewendet werden , sind streng definiert und beide Java und C # tun so, weitgehend die Probleme zu vermeiden , dass die C- und C ++ Definitionen führen.
Warum wurde diese Änderung nach drei Standardrevisionen nicht in C vorgenommen? Zuallererst gibt es C-Code, der 40 Jahre alt ist, und es kann nicht garantiert werden, dass durch eine solche Änderung dieser Code nicht beschädigt wird. Dies stellt eine gewisse Belastung für Compiler-Autoren dar, da durch eine solche Änderung alle vorhandenen Compiler sofort nicht mehr konform wären. Jeder musste bedeutende Änderungen vornehmen. Und selbst auf schnellen, modernen CPUs ist es immer noch möglich, echte Leistungssteigerungen durch Optimierung der Auswertungsreihenfolge zu erzielen.
quelle
Zuerst muss man verstehen, dass nicht nur x = x ++ undefiniert ist. X = x ++ interessiert niemanden, da es keinen Sinn macht, egal wie Sie es definieren würden. Was undefiniert ist, ist eher wie "a = b ++ wo a und b gleich sind" - dh
Je nachdem, was für die Prozessorarchitektur (und für die umgebenden Anweisungen, falls dies eine komplexere Funktion als das Beispiel ist) am effizientesten ist, kann die Funktion auf verschiedene Arten implementiert werden. Zum Beispiel zwei offensichtliche:
oder
Beachten Sie, dass die erste oben aufgeführte Anweisung, die mehr Anweisungen und mehr Register verwendet, in allen Fällen verwendet werden muss, in denen sich a und b nicht nachweisen lassen.
quelle
b
vorher zu speicherna
.Erbe
Die Annahme, dass C heute neu erfunden werden könnte, kann nicht gelten. Es gibt so viele Zeilen mit C-Codes, die produziert und täglich verwendet werden, dass es einfach falsch ist, die Spielregeln mitten im Spiel zu ändern.
Natürlich können Sie mit Ihren Regeln eine neue Sprache erfinden, beispielsweise C + = . Aber das wird nicht C sein.
quelle
Wenn Sie festlegen, dass etwas definiert ist, werden die vorhandenen Compiler nicht dahingehend geändert, dass Ihre Definition eingehalten wird. Dies gilt insbesondere für eine Annahme, auf die an vielen Stellen explizit oder implizit zurückgegriffen werden kann.
Das Hauptproblem bei der Annahme liegt nicht bei
x = x++;
(Compiler können es leicht überprüfen und sollten warnen), sondern bei*p1 = (*p2)++
und gleichwertig (p1[i] = p2[j]++;
wenn p1 und p2 Parameter für eine Funktion sind), wenn der Compiler nicht leicht weiß, obp1 == p2
(in C99)restrict
wurde hinzugefügt, um die Möglichkeit der Annahme von p1! = p2 zwischen Sequenzpunkten zu verteilen, weshalb die Optimierungsmöglichkeiten für wichtig erachtet wurden.quelle
p1[i]=p2[j]++
. Wenn der Compiler kein Aliasing annehmen kann, gibt es kein Problem. Wenn dies nicht möglich ist, muss es nach dem Buch sortiert werden -p2[j]
zuerst inkrementieren ,p1[i]
später speichern . Abgesehen von den Optimierungsmöglichkeiten, die nicht signifikant erscheinen, sehe ich kein Problem.x = x++;
es nicht geschrieben worden,t = x; x++; x = t;
oderx=x; x++;
oder was auch immer Sie als semantisch wollen (aber was ist mit der Diagnose?). Für eine neue Sprache lassen Sie einfach die Nebenwirkungen fallen.x++
als Sequenzpunkt behandeln , als ob es ein Funktionsaufrufinc_and_return_old(&x)
wäre , würde den Trick tun.In manchen Fällen ist diese Art von Code wurde in der neuen C ++ definiert 11 Standard.
quelle
x = ++x
ist jetzt gut definiert (aber nichtx = x++
)