Der Code, der undefiniertes Verhalten aufruft (in diesem Beispiel Division durch Null), wird niemals ausgeführt. Ist das Programm immer noch undefiniertes Verhalten?
int main(void)
{
int i;
if(0)
{
i = 1/0;
}
return 0;
}
Ich denke, es ist immer noch undefiniertes Verhalten, aber ich kann im Standard keine Beweise finden, die mich unterstützen oder ablehnen.
Also irgendwelche Ideen?
c
language-lawyer
Yu Hao
quelle
quelle
Antworten:
Schauen wir uns an, wie der C-Standard die Begriffe "Verhalten" und "undefiniertes Verhalten" definiert.
Verweise beziehen sich auf den Entwurf N1570 der Norm ISO C 2011; Mir sind keine relevanten Unterschiede in den drei veröffentlichten ISO C-Standards (1990, 1999 und 2011) bekannt.
Abschnitt 3.4:
Ok, das ist ein bisschen vage, aber ich würde argumentieren, dass eine bestimmte Aussage kein "Aussehen" und sicherlich keine "Aktion" hat, es sei denn, sie wird tatsächlich ausgeführt.
Abschnitt 3.4.3:
Es heißt " bei Verwendung " eines solchen Konstrukts. Das Wort "Verwendung" ist nicht durch den Standard definiert, daher greifen wir auf die übliche englische Bedeutung zurück. Ein Konstrukt wird nicht "verwendet", wenn es nie ausgeführt wird.
Es gibt einen Hinweis unter dieser Definition:
Ein Compiler kann Ihr Programm also zur Kompilierungszeit ablehnen , wenn sein Verhalten nicht definiert ist. Meine Interpretation davon ist jedoch, dass dies nur möglich ist, wenn nachgewiesen werden kann, dass bei jeder Ausführung des Programms ein undefiniertes Verhalten auftritt. Was impliziert, denke ich, dass dies:
if (rand() % 2 == 0) { i = i / 0; }
das sicherlich kann nicht definiertes Verhalten hat, kann nicht bei der Kompilierung abgelehnt werden.
In der Praxis müssen Programme in der Lage sein, Laufzeitprüfungen durchzuführen, um zu verhindern, dass undefiniertes Verhalten aufgerufen wird, und der Standard muss dies zulassen.
Ihr Beispiel war:
if (0) { i = 1/0; }
Die Division wird niemals durch 0 ausgeführt. Eine sehr verbreitete Redewendung ist:
int x, y; /* set values for x and y */ if (y != 0) { x = x / y; }
Die Division hat sicherlich undefiniertes Verhalten, wenn
y == 0
, aber es wird nie ausgeführt, wenny == 0
. Das Verhalten ist gut definiert und aus demselben Grund, aus dem Ihr Beispiel gut definiert ist: weil das potenzielle undefinierte Verhalten niemals tatsächlich auftreten kann.(Es sei denn
INT_MIN < -INT_MAX && x == INT_MIN && y == -1
(ja, die Ganzzahldivision kann überlaufen), aber das ist ein separates Problem.)In einem Kommentar (seitdem gelöscht) hat jemand darauf hingewiesen, dass der Compiler zur Kompilierungszeit konstante Ausdrücke auswerten kann. Was wahr ist, aber in diesem Fall nicht relevant, weil im Kontext von
i = 1/0;
1/0
ist kein konstanter Ausdruck .Ein konstanter Ausdruck ist eine syntaktische Kategorie, die sich auf einen bedingten Ausdruck reduziert (der Zuweisungen und Kommaausdrücke ausschließt). Die Herstellung konstanter Ausdruck erscheint in der Grammatik nur in Zusammenhängen , die tatsächlich einen konstanten Ausdruck, wie Fall Etiketten erfordern. Also, wenn Sie schreiben:
switch (...) { case 1/0: ... }
dann
1/0
ist ein konstanter Ausdruck - und einer, der die Einschränkung in 6.6p4 verletzt: "Jeder konstante Ausdruck muss als Konstante ausgewertet werden, die im Bereich der darstellbaren Werte für seinen Typ liegt." Daher ist eine Diagnose erforderlich. Die rechte Seite einer Zuweisung erfordert jedoch keinen konstanten Ausdruck , sondern lediglich einen bedingten Ausdruck , sodass die Einschränkungen für konstante Ausdrücke nicht gelten. Ein Compiler kann jeden Ausdruck auswerten , dass es bei der Kompilierung in der Lage ist, aber nur , wenn das Verhalten das gleiche ist , wie wenn es während der Ausführung ausgewertet wurde (oder, im Rahmenif (0)
, nicht während der Ausführung ausgewertet ().(Etwas, das genau wie ein konstanter Ausdruck aussieht, ist nicht unbedingt ein konstanter Ausdruck , genauso wie in
x + y * z
der Sequenz aufgrund des Kontexts, in dem sie erscheint,x + y
kein additiver Ausdruck ist .)Was bedeutet, dass die Fußnote in N1570 Abschnitt 6.6, die ich zitieren wollte:
ist für diese Frage eigentlich nicht relevant.
Schließlich gibt es einige Dinge, die definiert sind, um undefiniertes Verhalten zu verursachen, bei denen es nicht darum geht, was während der Ausführung passiert. Anhang J, Abschnitt 2 der C-Norm (siehe auch den Entwurf von N1570 ) listet Dinge auf, die undefiniertes Verhalten verursachen, wie aus dem Rest der Norm hervorgeht. Einige Beispiele (ich behaupte nicht, dass dies eine vollständige Liste ist) sind:
Diese speziellen Fälle sind Dinge , die ein Compiler könnte erkennen. Ich denke, ihr Verhalten ist undefiniert, weil das Komitee nicht allen Implementierungen dasselbe Verhalten auferlegen wollte oder konnte, und die Definition einer Reihe zulässiger Verhaltensweisen war die Mühe einfach nicht wert. Sie fallen nicht wirklich in die Kategorie "Code, der niemals ausgeführt wird", aber ich erwähne sie hier der Vollständigkeit halber.
quelle
1/0
, ein ständiger Ausdruck zu sein?1/0
ist kein konstanter Ausdruck im Kontext der Frage, da er nicht als konstanter Ausdruck analysiert wird , sondern lediglich als bedingter Ausdruck , der Teil eines Zuweisungsausdrucks ist .case 1/0:
würde die Einschränkung verletzen und eine Diagnose erfordern.Dieser Artikel beschreibt diese Frage in Abschnitt 2.6:
int main(void){ guard(); 5 / 0; }
Die Autoren sind der Ansicht, dass das Programm definiert wird, wenn
guard()
es nicht beendet wird. Sie unterscheiden auch die Begriffe „statisch undefiniert“ und „dynamisch undefiniert“, z.Ich würde empfehlen, den gesamten Artikel zu lesen. Zusammen ergibt sich ein einheitliches Bild.
Die Tatsache, dass die Autoren des Artikels die Frage mit einem Ausschussmitglied diskutieren mussten, bestätigt, dass der Standard bei der Beantwortung Ihrer Frage derzeit unscharf ist.
quelle
guard()
ist nicht undefiniert), ist das Verhalten genau dann undefiniert, wenn die Anweisung5 / 0;
tatsächlich ausgeführt wird. (Beachten Sie, dass ein Compiler die Auswertung von legitimerweise durch5 / 0
einen Aufruf vonabort()
oder etwas Ähnliches ersetzen könnte . Das Programm würde dann genau dann abbrechen, wenn die Ausführung diesen Punkt erreicht.) Ein Compiler kann dieses Programm nur ablehnen , wenn er feststellen kann, dassguard()
es immer beendet wird.guard()
nicht beendet wird, muss für 5/0 überhaupt keinen Code generieren. Im Gegensatz dazu gibt es keine Möglichkeit, Code für zu generieren(int)(void)5
, man kann nicht einfach den Code für generieren(int)(void)z
, da dies auch nicht korrekt ist. Also die Autoren denken, dass ...if (0) (int)(void)5;
aufgrund des Rätsels, das es dem naiven Compiler stellt , ablehnen , während nicht erreichbare dynamische UB wieif (0) 5 / 0;
harmlos sind. Dies ergab sich aus ihrer Diskussion mit einem Ausschussmitglied, und ich habe ein ähnliches Argument an anderer Stelle gesehen (aber vielleicht aus derselben Quelle, zumal ich mich nicht erinnere, wo es war). Ich gehe im Moment die C99-Begründung durch. Wenn ich eine Erwähnung davon sehe, werde ich zurückkommen und darauf hinweisen.(int)(void)5
ist eine Einschränkungsverletzung. N1570 6.5.4, in dem der Umwandlungsoperator beschrieben wird: "Einschränkungen: Sofern der Typname keinen ungültigen Typ angibt, muss der Typname einen atomaren, qualifizierten oder nicht qualifizierten Skalartyp angeben, und der Operand muss einen Skalartyp haben."(void)5
hat keinen skalaren Typ und(int)(void)5
verstößt daher gegen diese Einschränkung, unabhängig davon, ob der Code, der ihn enthält, jemals ausgeführt wird.In diesem Fall ist das undefinierte Verhalten das Ergebnis der Ausführung des Codes. Wenn der Code nicht ausgeführt wird, gibt es kein undefiniertes Verhalten.
Nicht ausgeführter Code kann undefiniertes Verhalten aufrufen, wenn das undefinierte Verhalten ausschließlich auf die Deklaration des Codes zurückzuführen ist (z. B. wenn ein Fall von variabler Abschattung undefiniert war).
quelle
#include "//e"
welches UB aufruft.Ich würde mit dem letzten Absatz dieser Antwort fortfahren : https://stackoverflow.com/a/18384176/694576
Also, nein, es wird kein UB aufgerufen.
quelle
Nur wenn der Standard Änderungen vornimmt und Ihr Code plötzlich nicht mehr "wird nie ausgeführt". Aber ich sehe keine logische Art und Weise, wie dies zu "undefiniertem Verhalten" führen kann. Es verursacht nichts .
quelle
Beim Thema undefiniertes Verhalten ist es oft schwierig, die formalen Aspekte von den praktischen zu trennen. Dies ist die Definition von undefiniertem Verhalten im Standard von 1989 (ich habe keine neuere Version zur Hand, aber ich erwarte nicht, dass sich dies wesentlich geändert hat):
Aus formaler Sicht würde ich sagen, dass Ihr Programm undefiniertes Verhalten hervorruft, was bedeutet, dass der Standard keinerlei Anforderungen an die Ausführung stellt, nur weil es eine Division durch Null enthält.
Auf der anderen Seite wäre ich aus praktischer Sicht überrascht, einen Compiler zu finden, der sich nicht so verhält, wie Sie es intuitiv erwarten.
quelle
Der Standard besagt, wie ich mich recht erinnere, dass es ab dem Moment, in dem eine Regel gebrochen wurde, alles tun darf. Vielleicht gibt es einige besondere Ereignisse mit globalem Flair (aber ich habe so etwas noch nie gehört oder gelesen) ... Also würde ich sagen: Nein, das kann nicht UB sein, denn solange das Verhalten gut definiert ist, ist 0 immer false, sodass die Regel zur Laufzeit nicht verletzt werden kann.
quelle
Ich denke, das Programm ruft kein undefiniertes Verhalten auf.
Der Fehlerbericht Nr. 109 behandelt eine ähnliche Frage und lautet:
quelle
Dies hängt davon ab, wie der Ausdruck "undefiniertes Verhalten" definiert ist und ob "undefiniertes Verhalten" einer Anweisung mit "undefiniertes Verhalten" für ein Programm identisch ist.
Dieses Programm sieht aus wie C, daher ist eine eingehendere Analyse des vom Compiler verwendeten C-Standards (wie einige Antworten) angemessen.
In Ermangelung eines festgelegten Standards lautet die richtige Antwort "es kommt darauf an". In einigen Sprachen versuchen Compiler nach dem ersten Fehler zu erraten, was der Programmierer bedeuten könnte, und generieren dennoch Code, wie der Compiler vermutet. In anderen, reineren Sprachen breitet sich die Undefiniertheit auf das gesamte Programm aus, sobald etwas undefiniert ist.
Andere Sprachen haben ein Konzept von "begrenzten Fehlern". Für einige begrenzte Arten von Fehlern definieren diese Sprachen, wie viel Schaden ein Fehler verursachen kann. Insbesondere Sprachen mit impliziter Speicherbereinigung machen häufig einen Unterschied, ob ein Fehler das Typisierungssystem ungültig macht oder nicht.
quelle