54 Stimmen

Ist in C99 f()+g() undefiniert oder nur unspezifiziert?

Ich dachte immer, dass in C99, auch wenn die Nebeneffekte von Funktionen f y g eingemischt, und obwohl der Ausdruck f() + g() keinen Sequenzpunkt enthält, f y g enthalten würde, so dass das Verhalten nicht spezifiziert wäre: entweder würde f() vor g() oder g() vor f() aufgerufen werden.

Ich bin mir da nicht mehr so sicher. Was ist, wenn der Compiler die Funktionen einbindet (was der Compiler auch dann tun kann, wenn die Funktionen nicht deklariert sind inline ) und bestellt dann die Anweisungen neu? Kann man ein anderes Ergebnis als die beiden oben genannten erhalten? Mit anderen Worten, ist dies ein undefiniertes Verhalten?

Das liegt nicht daran, dass ich vorhabe, so etwas zu schreiben, sondern daran, dass ich die beste Bezeichnung für eine solche Anweisung in einem statischen Analysator auswählen möchte.

25voto

Jonathan Leffler Punkte 694013

Der Ausdruck f() + g() enthält mindestens 4 Sequenzpunkte; einen vor dem Aufruf von f() (nachdem alle Null-Argumente ausgewertet wurden); eine vor dem Aufruf von g() (nachdem alle Null-Argumente ausgewertet wurden); eine als Aufruf von f() zurück; und eine als Aufruf von g() zurück. Außerdem sind die beiden Sequenzpunkte, die mit f() entweder beide vor oder beide nach den beiden Sequenzpunkten auftreten, die mit g() . Sie können jedoch nicht sagen, in welcher Reihenfolge die Sequenzpunkte auftreten werden - ob die f-Punkte vor den g-Punkten auftreten oder umgekehrt.

Selbst wenn der Compiler den Code inlined, muss er die "als ob"-Regel befolgen - der Code muss sich genauso verhalten, als ob die Funktionen nicht verschachtelt wären. Das schränkt den Spielraum für Schäden ein (vorausgesetzt, der Compiler ist fehlerfrei).

Also die Reihenfolge, in der f() y g() ausgewertet werden, ist nicht spezifiziert. Aber alles andere ist ziemlich sauber.


In einem Kommentar, Superkatze fragt:

Ich würde erwarten, dass Funktionsaufrufe im Quellcode als Sequenzpunkte erhalten bleiben, auch wenn ein Compiler von sich aus beschließt, sie zu inlinen. Gilt das auch für Funktionen, die als "inline" deklariert sind, oder erhält der Compiler zusätzlichen Spielraum?

Ich glaube, dass die "als ob"-Regel gilt und der Compiler keinen zusätzlichen Spielraum erhält, um Sequenzpunkte auszulassen, weil er eine explizite inline Funktion. Der Hauptgrund für diese Überlegung (ich bin zu faul, den genauen Wortlaut in der Norm zu suchen) ist, dass der Compiler eine Funktion nach seinen Regeln inline oder nicht inline einbinden darf, aber das Verhalten des Programms sollte sich nicht ändern (außer für die Leistung).

Was lässt sich außerdem über die Abfolge der (a(),b()) + (c(),d()) ? Ist es möglich, dass c() または d() zur Ausführung zwischen a() y b() oder für a() o b() zur Ausführung zwischen c() y d() ?

  • Ich glaube, dass es möglich ist, dass c und d zwischen a und b ausgeführt werden, obwohl es ziemlich unwahrscheinlich ist, dass der Compiler den Code so generiert; ebenso könnten a und b zwischen c und d ausgeführt werden. Und obwohl ich "und" in "c und d" verwendet habe, könnte das ein "oder" sein - das heißt, jede dieser Operationsfolgen erfüllt die Einschränkungen:

    • Auf jeden Fall erlaubt
    • abcd
    • cdab
    • Möglicherweise zulässig (behält die Reihenfolge a b, c d bei)
    • acbd
    • acdb
    • cadb
    • cabd

     
    Ich glaube, das deckt alle möglichen Sequenzen ab. Siehe auch die Chat zwischen Jonathan Leffler und AnArrayOfFunctions - das Wesentliche ist, dass AnArrayOfFunctions glaubt nicht, dass die "möglicherweise erlaubten" Sequenzen überhaupt erlaubt sind.

Wenn so etwas möglich wäre, würde das einen wesentlichen Unterschied zwischen Inline-Funktionen und Makros bedeuten.

Es gibt erhebliche Unterschiede zwischen Inline-Funktionen und Makros, aber ich glaube nicht, dass die Reihenfolge im Ausdruck einer davon ist. Das heißt, jede der Funktionen a, b, c oder d könnte durch ein Makro ersetzt werden, und die gleiche Reihenfolge der Makrokörper könnte auftreten. Der Hauptunterschied besteht meines Erachtens darin, dass es bei den Inline-Funktionen garantierte Reihenfolgepunkte bei den Funktionsaufrufen - wie in der Hauptantwort beschrieben - sowie bei den Komma-Operatoren gibt. Bei Makros gehen die funktionsbezogenen Sequenzpunkte verloren. (Vielleicht ist das also ein wichtiger Unterschied...) In vielerlei Hinsicht ist das Problem jedoch ähnlich wie die Frage, wie viele Engel auf einem Stecknadelkopf tanzen können - in der Praxis ist es nicht sehr wichtig. Wenn mich jemand mit dem Ausdruck (a(),b()) + (c(),d()) Bei einer Codeüberprüfung würde ich ihnen sagen, dass sie den Code umschreiben sollen, damit er klarer wird:

a();
c();
x = b() + d();

Und das setzt voraus, dass es keine kritischen Anforderungen an die Sequenzierung gibt b() gegen d() .

14voto

Siehe Anhang C für eine Liste der Sequenzpunkte. Funktionsaufrufe (der Zeitpunkt zwischen der Auswertung aller Argumente und der Übergabe der Ausführung an die Funktion) sind Sequenzpunkte. Wie Sie schon sagten, ist nicht festgelegt, welche Funktion zuerst aufgerufen wird, aber jede der beiden Funktionen wird entweder alle Nebenwirkungen der anderen sehen oder gar keine.

1voto

Pascal Cuoq Punkte 77147

@dmckee

Nun, das passt nicht in einen Kommentar, aber hier ist die Sache:

Zuerst schreiben Sie einen korrekten statischen Analysator. "Korrekt" bedeutet in diesem Zusammenhang, dass er nicht schweigen wird, wenn es irgendetwas Fragwürdiges im analysierten Code gibt, so dass Sie in dieser Phase fröhlich undefiniertes und unspezifiziertes Verhalten vermischen. Beide sind in kritischem Code schlecht und inakzeptabel, und Sie warnen zu Recht vor beiden.

Aber Sie wollen nur einmal vor einem möglichen Fehler warnen, und Sie wissen auch, dass Ihr Analysator in Benchmarks in Bezug auf "Präzision" und "Recall" im Vergleich zu anderen, möglicherweise nicht korrekten, Analysatoren beurteilt wird, also dürfen Sie nicht zweimal vor demselben Problem warnen... Sei es ein echter oder ein falscher Alarm (Sie wissen nicht, welcher. Sie wissen nie, welcher, sonst wäre es zu einfach).

Sie wollen also eine einzige Warnung ausgeben für

*p = x;
y = *p;

Denn sobald p bei der ersten Anweisung ein gültiger Zeiger ist, kann davon ausgegangen werden, dass er auch bei der zweiten Anweisung ein gültiger Zeiger ist. Wenn Sie dies nicht ableiten, verringert sich Ihre Punktzahl bei der Präzisionsmetrik.

Sie bringen Ihrem Analysator also bei, dass er davon ausgeht, dass p ein gültiger Zeiger ist, sobald Sie im obigen Code das erste Mal davor gewarnt haben, so dass Sie beim zweiten Mal nicht mehr davor warnen müssen. Allgemeiner ausgedrückt: Sie lernen, Werte (und Ausführungspfade) zu ignorieren, die etwas entsprechen, vor dem Sie bereits gewarnt haben.

Dann stellen Sie fest, dass nicht viele Leute kritischen Code schreiben, also machen Sie andere, leichtere Analysen für den Rest von ihnen, basierend auf den Ergebnissen der ersten, korrekten Analyse. Sagen wir, ein C-Programm-Slicer.

Und du sagst es "ihnen": Sie müssen sich nicht um all die (möglicherweise oft falschen) Alarme kümmern, die bei der ersten Analyse ausgelöst werden. Das zerschnittene Programm verhält sich genauso wie das ursprüngliche Programm, solange keiner von ihnen ausgelöst wird. Der Slicer erzeugt Programme, die für das Slicing-Kriterium für "definierte" Ausführungspfade äquivalent sind.

Und die Benutzer ignorieren fröhlich die Alarme und benutzen die Schneidemaschine.

Und dann merkt man, dass es sich vielleicht um ein Missverständnis handelt. Zum Beispiel, die meisten Implementierungen von memmove (Sie wissen schon, derjenige, der überlappende Blöcke behandelt) rufen tatsächlich ein nicht spezifiziertes Verhalten auf, wenn sie mit Zeigern aufgerufen werden, die nicht auf denselben Block zeigen (indem sie Adressen vergleichen, die nicht auf denselben Block zeigen). Und Ihr Analysator ignoriert beide Ausführungspfade, weil beide nicht spezifiziert sind, aber in Wirklichkeit sind beide Ausführungspfade gleichwertig und alles ist gut.

Es sollte also keine Missverständnisse über die Bedeutung von Alarmen geben, und wenn man beabsichtigt, sie zu ignorieren, sollten nur unmissverständliche und undefinierte Verhaltensweisen ausgeschlossen werden.

Und so kommt es, dass man ein starkes Interesse daran hat, zwischen nicht spezifiziertem Verhalten und nicht definiertem Verhalten zu unterscheiden. Niemand kann Ihnen vorwerfen, dass Sie letzteres ignorieren. Aber Programmierer werden ersteres schreiben, ohne überhaupt darüber nachzudenken, und wenn Sie sagen, dass Ihr Slicer "falsches Verhalten" des Programms ausschließt, werden sie sich nicht betroffen fühlen.

Und dies ist das Ende einer Geschichte, die definitiv nicht in einen Kommentar gepasst hat. Ich entschuldige mich bei allen, die bis hierher gelesen haben.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X