Kann Code, der niemals ausgeführt wird, undefiniertes Verhalten hervorrufen?


81

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?


7
Ich würde sagen, es ist kein "Verhalten", wenn es nie ausgeführt wird
Kevin

1
Wenn UB Laufzeit eins ist (so), würde es nicht. Aber ich bezweifle sehr, dass Standard etwas darüber sagt.
Keltar

13
Klingt nach einer Frage der Semantik, nicht der Programmierung.
Wooble

14
@Wooble Ich bin anderer Meinung. Der Ausdruck undefiniertes Verhalten hat in C / C ++ eine besondere Bedeutung. Und diese Frage bezieht sich auf einige andere Situationen, die undefiniertes Verhalten bestimmen oder nicht. Wenn Sie den C / C ++ - Standard gelesen haben, finden Sie den Ausdruck undefiniertes Verhalten überall.
Yu Hao

10
@Cornstalks: Der C-Standard verwendet nicht den Ausdruck "ruft undefiniertes Verhalten auf", sodass Sie nicht über den C-Standard nachdenken können, basierend darauf, was dieser Ausdruck bedeuten könnte. Mit ihm beschreiben C ungeeignet ist , weil es , dass „nicht definiertes Verhalten“ schon sagt, ein Ding wie ein Winden Sie in ausführen , wenn Sie von Grenzen hinausgehen. Tatsächlich ist „undefiniertes Verhalten“ ein Mangel an etwas; Es ist das Ende der Grenzen. Wenn Sie die genau definierte Stadt verlassen, die Standard C ist, befinden Sie sich auf einem offenen Feld, auf dem alles gebaut werden kann.
Eric Postpischil

Antworten:


70

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:

Verhalten
äußeres Erscheinungsbild oder Handlung

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:

undefiniertes Verhaltensverhalten
bei Verwendung eines nicht portierbaren oder fehlerhaften Programmkonstrukts oder fehlerhafter Daten, für die diese Internationale Norm keine Anforderungen stellt

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:

HINWEIS Ein mögliches undefiniertes Verhalten reicht vom vollständigen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer für die Umgebung charakteristischen dokumentierten Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit dem Ausgabe einer Diagnosemeldung).

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, wenn y == 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/0ist 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 Rahmen if (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 * zder Sequenz aufgrund des Kontexts, in dem sie erscheint, x + ykein additiver Ausdruck ist .)

Was bedeutet, dass die Fußnote in N1570 Abschnitt 6.6, die ich zitieren wollte:

Somit ist
static int i = 2 || 1 / 0;
der Ausdruck in der folgenden Initialisierung ein gültiger ganzzahliger konstanter Ausdruck mit dem Wert eins.

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:

  • Eine nicht leere Quelldatei endet nicht mit einem Zeilenumbruchzeichen, dem nicht unmittelbar ein Backslash-Zeichen vorangestellt ist, oder endet mit einem Teilvorverarbeitungstoken oder -kommentar
  • Die Token-Verkettung erzeugt eine Zeichenfolge, die der Syntax eines universellen Zeichennamens entspricht
  • Ein Zeichen, das nicht im grundlegenden Quellzeichensatz enthalten ist, wird in einer Quelldatei gefunden, außer in einem Bezeichner, einer Zeichenkonstante, einem Zeichenfolgenliteral, einem Headernamen, einem Kommentar oder einem Vorverarbeitungstoken, das niemals in ein Token konvertiert wird
  • Ein Bezeichner, Kommentar, Zeichenfolgenliteral, Zeichenkonstante oder Headername enthält ein ungültiges Multibyte-Zeichen oder beginnt und endet nicht im anfänglichen Verschiebungsstatus
  • Dieselbe Kennung hat sowohl interne als auch externe Verknüpfungen in derselben Übersetzungseinheit

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.


2
@EricPostpischil 6.6 / 4 sagt: "Jeder Konstantenausdruck muss als Konstante ausgewertet werden, die im Bereich der darstellbaren Werte für seinen Typ liegt." Würde das nicht ausschließen 1/0, ein ständiger Ausdruck zu sein?
Casey

2
@EricPostpischil: Ich denke nicht, dass das ganz richtig ist. Die Verletzung bedeutet eine Einschränkung der Regel , dass eine Kompilierung-Diagnose erforderlich ist, nicht nur , dass etwas , das sonst sein könnte foo nicht a foo . 1/0ist 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.
Keith Thompson

DR # 109 scheint darauf hinzudeuten, dass das Programm nicht UB ist, siehe die neue Antwort, die ich gerade gepostet habe.
Ouah

1
Betreff " so dass wir auf die gemeinsame englische Bedeutung zurückgreifen ", In der englischen Bedeutung verwendet das Programm ein Konstrukt, wenn es im Programm vorhanden ist. Warum geht Ihre Antwort davon aus, dass die Verwendung eines Konstrukts die Ausführung eines Konstrukts bedeutet? Ihre Schlussfolgerung folgt nicht aus Ihrer Erklärung!
Ikegami

31

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.

Die Absicht hinter dem Standard 11 scheint zu sein, dass Situationen im Allgemeinen statisch undefiniert werden, wenn es nicht einfach ist, Code für sie zu generieren. Nur wenn Code generiert werden kann, kann die Situation dynamisch undefiniert werden.

11) Private Korrespondenz mit dem Ausschussmitglied.

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.


1
Dieses Beispiel bereitet nur Schwierigkeiten, wenn Sie statisch feststellen können, ob sein Verhalten undefiniert ist. Wenn es ausgeführt wird (vorausgesetzt, das Verhalten von guard()ist nicht undefiniert), ist das Verhalten genau dann undefiniert, wenn die Anweisung 5 / 0;tatsächlich ausgeführt wird. (Beachten Sie, dass ein Compiler die Auswertung von legitimerweise durch 5 / 0einen Aufruf von abort()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, dass guard()es immer beendet wird.
Keith Thompson

@KeithThompson Um die statische / dynamische Unterscheidung im Artikel zu verdeutlichen, wird 5/0 als dynamisch angesehen, da der Compiler Code generieren kann, der durch Null geteilt wird: Generieren Sie einfach den üblichen Code, der durch z geteilt wird, nachdem Sie z auf 0 gesetzt haben. Somit ein naiver Compiler kann eine Teilungsanweisung erzeugen. Ein hoch entwickelter Compiler, der feststellt, dass er 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 ...
Pascal Cuoq

@KeithThompson… ein Compiler darf das Programm if (0) (int)(void)5;aufgrund des Rätsels, das es dem naiven Compiler stellt , ablehnen , während nicht erreichbare dynamische UB wie if (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.
Pascal Cuoq

2
(int)(void)5ist 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)5hat keinen skalaren Typ und (int)(void)5verstößt daher gegen diese Einschränkung, unabhängig davon, ob der Code, der ihn enthält, jemals ausgeführt wird.
Keith Thompson

@KeithThompson Ja, sie scheinen das falsche Beispiel ausgewählt zu haben, aber in der langen Liste in J.2 gibt es eines, das keine Einschränkungsverletzung darstellt und das sicherlich „statisch“ ist. Wie wäre es mit dem alten Klassiker „Eine nicht leere Quelldatei endet nicht mit einem Zeilenumbruch…“? Es gibt keine Vorstellung von Erreichbarkeit, die für diese gilt, aber es ist keine Einschränkungsverletzung, oder?
Pascal Cuoq

5

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).


Betrachten Sie für ein Beispiel für Fall 2, #include "//e"welches UB aufruft.
Michael Foukarakis

2

Ich würde mit dem letzten Absatz dieser Antwort fortfahren : https://stackoverflow.com/a/18384176/694576

... UB ist ein Laufzeitproblem, kein Kompilierungsproblem ...

Also, nein, es wird kein UB aufgerufen.


2
Sie sollten nicht alles glauben, was Sie im Internet lesen, insbesondere nicht aus dieser StackOverflow-Antwort.
Pascal Cuoq

1
@PascalCuoq Das erschüttert den Glauben einiger SO-Gläubiger wie mir. Wohin jetzt?
0decimal0

2

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 .


2

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):

1 undefiniertes Verhalten
  Verhalten bei Verwendung eines nicht portierbaren oder fehlerhaften Programmkonstrukts oder von
  fehlerhafte Daten, für die diese Internationale Norm keine Anforderungen stellt
2 HINWEIS Mögliches undefiniertes Verhalten reicht vom vollständigen Ignorieren der Situation aus
  mit unvorhersehbaren Ergebnissen, um sich während der Übersetzung oder Programmausführung zu verhalten
  auf dokumentierte Weise, die für die Umwelt charakteristisch ist (mit oder ohne
  Ausgabe einer Diagnosemeldung), um eine Übersetzung zu beenden oder
  Ausführung (mit der Ausgabe einer Diagnosemeldung).

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.


2

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.


0 ist immer wahr? oder sogar immer wahr? Bist du eine Art Rubinist?!
Grady Player

@Grady PlayerNein ich war nur eine Art Brain afk. Ich werde es reparieren, sorry
dhein

2

Ich denke, es ist immer noch undefiniertes Verhalten, aber ich kann im Standard keine Beweise finden, die mich unterstützen oder ablehnen.

Ich denke, das Programm ruft kein undefiniertes Verhalten auf.

Der Fehlerbericht Nr. 109 behandelt eine ähnliche Frage und lautet:

Wenn außerdem jede mögliche Ausführung eines bestimmten Programms zu einem undefinierten Verhalten führen würde, ist das angegebene Programm nicht streng konform. Eine konforme Implementierung darf ein streng konformes Programm nicht übersehen, nur weil eine mögliche Ausführung dieses Programms zu undefiniertem Verhalten führen würde. Da foo möglicherweise nie aufgerufen wird, muss das angegebene Beispiel von einer konformen Implementierung erfolgreich übersetzt werden.


-1

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.

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.