655 Stimmen

Unbestimmtes, nicht spezifiziertes und implementierungsdefiniertes Verhalten

Was ist undefiniertes Verhalten (UB) in C und C++? Was ist mit nicht spezifiziertes Verhalten y Implementierung definiert Verhalten? Worin besteht der Unterschied zwischen ihnen?

1 Stimmen

Ich war mir ziemlich sicher, dass wir das schon einmal gemacht haben, aber ich kann es nicht finden. Siehe auch: stackoverflow.com/questions/2301372/

1 Stimmen

491voto

fredoverflow Punkte 245881

Undefiniertes Verhalten ist einer der Aspekte der Sprachen C und C++, die für Programmierer, die aus anderen Sprachen kommen, überraschend sein können (andere Sprachen versuchen, dies besser zu verbergen). Grundsätzlich ist es möglich, C++-Programme zu schreiben, die sich nicht vorhersehbar verhalten, auch wenn viele C++-Compiler keine Fehler im Programm melden!

Schauen wir uns ein klassisches Beispiel an:

#include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}

Die Variable p zeigt auf das String-Literal "hello!\n" und die beiden folgenden Zuweisungen versuchen, dieses Zeichenfolgenliteral zu ändern. Was macht dieses Programm? Gemäß Abschnitt 2.14.5 Absatz 11 des C++-Standards ruft es auf undefiniertes Verhalten :

Die Auswirkung des Versuchs, ein String-Literal zu ändern, ist undefiniert.

Ich höre schon die Leute schreien: "Aber warte mal, ich kann das doch problemlos kompilieren und bekomme die Ausgabe yellow " oder "Was meinen Sie mit undefiniert, String-Literale werden im Nur-Lese-Speicher gespeichert, so dass der erste Zuweisungsversuch zu einem Core-Dump führt". Genau das ist das Problem mit dem undefinierten Verhalten. Im Grunde erlaubt der Standard alles, was passieren kann, sobald man undefiniertes Verhalten aufruft (sogar nasale Dämonen). Wenn es ein "richtiges" Verhalten gemäß Ihrem mentalen Modell der Sprache gibt, ist dieses Modell einfach falsch; der C++-Standard hat die einzige Stimme, Punkt.

Andere Beispiele für undefiniertes Verhalten sind der Zugriff auf ein Array außerhalb seiner Grenzen, Dereferenzierung des Null-Zeigers , Zugriff auf Objekte nach Ende ihrer Lebensdauer oder Schreiben vermeintlich kluge Ausdrücke wie i++ + ++i .

In Abschnitt 1.9 des C++-Standards werden auch die beiden weniger gefährlichen Brüder des undefinierten Verhaltens erwähnt, nicht spezifiziertes Verhalten y implementierungsdefiniertes Verhalten :

Die semantischen Beschreibungen in dieser Internationalen Norm definieren eine parametrisierte, nicht-deterministische abstrakte Maschine.

Bestimmte Aspekte und Funktionen der abstrakten Maschine werden in dieser Internationalen Norm wie folgt beschrieben Implementierung definiert (zum Beispiel, sizeof(int) ). Diese bilden die Parameter der abstrakten Maschine. Jede Implementierung muss eine Dokumentation enthalten, die ihre Eigenschaften und ihr Verhalten in dieser Hinsicht beschreibt.

Bestimmte andere Aspekte und Operationen der abstrakten Maschine werden in dieser Internationalen Norm beschrieben als nicht spezifiziert (z. B. die Reihenfolge der Auswertung der Argumente einer Funktion). Soweit möglich, definiert diese Internationale Norm eine Reihe von zulässigen Verhaltensweisen. Diese definieren die nicht-deterministischen Aspekte der abstrakten Maschine.

Bestimmte andere Tätigkeiten werden in diesem Internationalen Standard beschrieben als undefiniert (z. B. die Auswirkung der Dereferenzierung des Null-Zeigers). [ Hinweis : Diese Internationale Norm stellt keine Anforderungen an das Verhalten von Programmen, die undefiniertes Verhalten enthalten. - Endnote ]

Konkret heißt es in Abschnitt 1.3.24:

Zulässiges undefiniertes Verhalten reicht von völliges Ignorieren der Situation mit unvorhersehbaren Ergebnissen zu einem für die Umgebung charakteristischen, dokumentierten Verhalten während der Übersetzung oder Programmausführung (mit oder ohne Ausgabe einer Diagnosemeldung), zum Abbruch einer Übersetzung oder Ausführung (mit Ausgabe einer Diagnosemeldung).

Was können Sie tun, um nicht auf undefiniertes Verhalten zu stoßen? Grundsätzlich müssen Sie Folgendes lesen gute C++ Bücher von Autoren, die wissen, wovon sie sprechen. Vermeiden Sie Internet-Tutorials. Vermeiden Sie Bullschildt.

11 Stimmen

Es ist eine seltsame Tatsache, die sich aus der Zusammenlegung ergibt, dass diese Antwort nur C++ abdeckt, die Tags dieser Frage aber C einschließen. C hat einen anderen Begriff von "undefiniertem Verhalten": Die Implementierung muss auch dann Diagnosemeldungen ausgeben, wenn das Verhalten bei bestimmten Regelverletzungen (Constraint-Verletzungen) als undefiniert eingestuft wird.

0 Stimmen

@Johannes: Das ist in der Tat schlecht. Warum nicht mehrere Antworten aus der Frage verlinken?

1 Stimmen

Handelt es sich in diesem Fall (Änderung eines String-Litterals) um ein undefiniertes Verhalten, weil das String-Litteral möglicherweise einem schreibgeschützten Textsegment zugewiesen wurde?

121voto

AnT Punkte 300728

Nun, dies ist im Grunde eine direkte Kopie aus dem Standard

3.4.1 1 implementierungsdefiniertes Verhalten nicht spezifiziertes Verhalten, wenn jede Implementierung dokumentiert, wie die Wahl getroffen wird

2 BEISPIEL Ein Beispiel für implementierungsdefiniertes Verhalten ist die Weitergabe des Bits höherer Ordnung, wenn eine ganze Zahl mit Vorzeichen nach rechts verschoben wird.

3.4.3 1 undefiniertes Verhalten Verhalten, bei Verwendung eines nicht portablen oder fehlerhaften Programmkonstrukts oder von fehlerhaften Daten, für die diese Internationale Norm keine Anforderungen auferlegt

2 HINWEIS Mögliches undefiniertes Verhalten reicht vom Ignorieren der Situation mit unvorhersehbaren Ergebnissen, bis hin zu einem Verhalten während der Übersetzung oder Programmausführung in einer dokumentierten Art und Weise, die für die Umgebung (mit oder ohne die Ausgabe einer Diagnosemeldung), zum Abbruch einer Übersetzung oder Ausführung (mit Ausgabe einer Diagnosemeldung) Nachricht).

3 BEISPIEL Ein Beispiel für undefiniertes Verhalten ist das Verhalten bei Integer-Überlauf.

3.4.4 1 nicht spezifiziertes Verhalten Verwendung eines nicht spezifizierten Wertes oder anderes Verhalten wo diese Internationale Norm zwei oder mehr Möglichkeiten vorsieht und keine weiteren Anforderungen stellt an welches in jedem Fall gewählt wird

2 BEISPIEL Ein Beispiel für nicht spezifiziertes Verhalten ist die Reihenfolge, in der die Argumente einer Funktion ausgewertet werden.

5 Stimmen

Was ist der Unterschied zwischen implementierungsdefiniertem und nicht spezifiziertem Verhalten?

32 Stimmen

@Zolomon: Genau wie es da steht: im Grunde das Gleiche, nur dass im Falle von "implementation-defined" die Implementierung verpflichtet ist zu dokumentieren (zu garantieren), was genau passieren wird, während im Falle von "unspecified" die Implementierung nicht verpflichtet ist, irgendetwas zu dokumentieren oder zu garantieren.

1 Stimmen

@Zolomon: Das spiegelt sich in dem Unterschied zwischen 3.4.1 und 2.4.4 wider.

73voto

Khaled Alshaya Punkte 90854

Vielleicht sind einfache Formulierungen leichter zu verstehen als die strenge Definition der Normen.

implementierungsdefiniertes Verhalten
Die Sprache sagt, dass wir Datentypen haben. Die Compiler-Hersteller geben an, welche Größen sie verwenden sollen, und dokumentieren, was sie getan haben.

undefiniertes Verhalten
Sie machen etwas falsch. Sie haben zum Beispiel einen sehr großen Wert in einer int das nicht hineinpasst char . Wie setzen Sie diesen Wert in char ? eigentlich gibt es keine Möglichkeit! Es könnte alles Mögliche passieren, aber das Vernünftigste wäre, das erste Byte dieses int zu nehmen und es in char . Es ist einfach falsch, dies zu tun, um das erste Byte zuzuweisen, aber das ist, was unter der Haube passiert.

nicht spezifiziertes Verhalten
Welche dieser beiden Funktionen wird zuerst ausgeführt?

void fun(int n, int m);

int fun1() {
    std::cout << "fun1";
    return 1;
}
int fun2() {
    std::cout << "fun2";
    return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?

Die Sprache gibt nicht vor, ob die Auswertung von links nach rechts oder von rechts nach links erfolgt! Ein nicht spezifiziertes Verhalten kann also zu einem undefinierten Verhalten führen oder auch nicht, aber Ihr Programm sollte auf keinen Fall ein nicht spezifiziertes Verhalten erzeugen.


@eSKay Ich denke, Ihre Frage ist es wert, die Antwort zu bearbeiten, um mehr zu klären :)

für fun(fun1(), fun2()); ist das Verhalten nicht "implementierungsdefiniert"? Der Compiler muss doch den einen oder anderen Weg wählen?

Der Unterschied zwischen "implementation-defined" und "unspecified" besteht darin, dass der Compiler im ersten Fall ein Verhalten auswählen soll, im zweiten Fall aber nicht muss. Zum Beispiel muss eine Implementierung eine und nur eine Definition von sizeof(int) . Es kann also nicht heißen, dass sizeof(int) beträgt 4 für einige Teile des Programms und 8 für andere. Im Gegensatz zu nicht spezifiziertem Verhalten, bei dem der Compiler sagen kann: OK, ich werde diese Argumente von links nach rechts auswerten und die Argumente der nächsten Funktion werden von rechts nach links ausgewertet. Das kann im selben Programm passieren, deshalb heißt es ja auch nicht spezifiziert . In der Tat hätte C++ einfacher sein können, wenn einige der nicht spezifizierten Verhaltensweisen spezifiziert worden wären. Werfen Sie einen Blick auf Die Antwort von Dr. Stroustrup auf diese Frage :

Es wird behauptet, dass der Unterschied zwischen dem, was mit dieser Freiheit des Compilers erzeugt werden kann, und dem, was eine "gewöhnliche Auswertung von links nach rechts" erfordert, erheblich sein kann. Ich bin nicht überzeugt, aber da es unzählige Compiler "da draußen" gibt, die diese Freiheit ausnutzen, und einige Leute diese Freiheit leidenschaftlich verteidigen, wäre eine Änderung schwierig und könnte Jahrzehnte dauern, bis sie in die entlegenen Winkel der C- und C++-Welt vordringt. Ich bin enttäuscht, dass nicht alle Compiler vor Code wie dem folgenden warnen ++i+i++ . Auch die Reihenfolge der Auswertung der Argumente ist nicht festgelegt.

IMO bleiben viel zu viele "Dinge" undefiniert, unspezifiziert, das ist leicht zu sagen und sogar mit Beispielen zu belegen, aber schwer zu beheben. Es sollte auch angemerkt werden, dass es gar nicht so schwierig ist, die meisten Probleme zu vermeiden und portablen Code zu produzieren.

3 Stimmen

Für fun(fun1(), fun2()); ist nicht das Verhalten "implementation defined" ? Der Compiler muss sich doch für den einen oder anderen Weg entscheiden?

2 Stimmen

@AraK: Danke für die Erklärung. Jetzt verstehe ich es. Btw, "I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left" Ich verstehe dies can passieren. Ist das bei den Compilern, die wir heutzutage verwenden, wirklich der Fall?

1 Stimmen

@eSKay Das musst du einen Guru fragen, der sich mit vielen Compilern die Hände schmutzig gemacht hat :) AFAIK wertet VC Argumente immer von rechts nach links aus.

32voto

Aus dem offiziellen C Rationale Dokument

Die Begriffe nicht spezifiziert Verhalten, undefiniert Verhalten, und Implementierung definiert Verhalten werden verwendet, um das Ergebnis des Schreibens von Programmen zu kategorisieren, deren Eigenschaften der Standard nicht oder nicht vollständig beschreiben kann. Das Ziel dieser Kategorisierung ist es, eine gewisse Vielfalt unter den Implementierungen zu ermöglichen, die es erlaubt, dass die Qualität der Implementierung eine aktive Kraft auf dem Markt ist, sowie bestimmte populäre Erweiterungen zuzulassen, ohne das Gütesiegel der Konformität mit der Norm zu verlieren. In Anhang F der Norm sind die Verhaltensweisen aufgeführt, die in eine dieser drei Kategorien fallen.

Nicht spezifiziertes Verhalten gibt dem Implementierer einen gewissen Spielraum bei der Übersetzung von Programmen. Dieser Spielraum reicht jedoch nicht so weit, dass das Programm nicht übersetzt wird.

Undefiniertes Verhalten gibt dem Implementierer die Möglichkeit, bestimmte Programmfehler, die schwer zu diagnostizieren sind, nicht zu erkennen. Es werden auch Bereiche identifiziert, in denen eine konforme Spracherweiterung möglich ist: Der Implementierer kann die Sprache erweitern, indem er eine Definition für das offiziell nicht definierte Verhalten bereitstellt.

Implementierung definiert Verhalten lässt dem Implementierer die Freiheit, den geeigneten Ansatz zu wählen, erfordert aber, dass diese Wahl dem Benutzer erklärt wird. Verhaltensweisen, die als implementierungsdefiniert bezeichnet werden, sind im Allgemeinen solche, bei denen ein Benutzer auf der Grundlage der Implementierungsdefinition sinnvolle Kodierungsentscheidungen treffen könnte. Implementierer sollten dieses Kriterium berücksichtigen, wenn sie entscheiden, wie umfangreich eine Implementierungsdefinition sein sollte. Wie bei nicht spezifiziertem Verhalten ist es keine angemessene Reaktion, den Quelltext, der das implementierungsdefinierte Verhalten enthält, einfach nicht zu übersetzen.

4 Stimmen

Hyper-moderne Compiler-Autoren betrachten "undefiniertes Verhalten" auch als Freibrief für Compiler-Autoren, davon auszugehen, dass Programme niemals Eingaben erhalten, die zu undefiniertem Verhalten führen würden, und alle Aspekte des Verhaltens der Programme willkürlich zu ändern, wenn sie solche Eingaben erhalten.

2 Stimmen

Ein weiterer Punkt, der mir gerade aufgefallen ist: In C89 wurde der Begriff "Erweiterung" nicht verwendet, um Funktionen zu beschreiben, die bei einigen Implementierungen garantiert waren, bei anderen jedoch nicht. Die Autoren von C89 erkannten, dass die Mehrheit der damals aktuellen Implementierungen vorzeichenbehaftete und vorzeichenlose Arithmetik identisch behandelt, außer wenn die Ergebnisse auf bestimmte Weise verwendet werden, und dass eine solche Behandlung auch im Falle eines vorzeichenbehafteten Überlaufs gilt; sie führten dies jedoch nicht als gemeinsame Erweiterung in Anhang J2 auf, was für mich darauf hindeutet, dass sie dies als einen natürlichen Zustand und nicht als eine Erweiterung betrachteten.

15voto

Anders Abel Punkte 65873

Undefiniertes Verhalten vs. nicht spezifiziertes Verhalten enthält eine kurze Beschreibung des Projekts.

Ihre abschließende Zusammenfassung:

Zusammenfassend lässt sich sagen, dass ein nicht spezifiziertes Verhalten in der Regel etwas ist, das man nicht Sorgen machen, es sei denn, Ihre Software muss portabel sein. Umgekehrt ist undefiniertes Verhalten immer unerwünscht und sollte niemals auftreten.

1 Stimmen

Es gibt zwei Arten von Compilern: solche, die, sofern nicht explizit anders dokumentiert, die meisten Formen von unbestimmtem Verhalten des Standards so interpretieren, dass sie auf charakteristische Verhaltensweisen zurückgreifen, die von der zugrundeliegenden Umgebung dokumentiert werden, und solche, die standardmäßig nur Verhaltensweisen sinnvoll freilegen, die der Standard als implementierungsdefiniert charakterisiert. Bei Verwendung von Compilern des ersten Typs können viele Dinge des ersten Typs effizient und sicher mit UB erledigt werden. Compiler des zweiten Typs sind für solche Aufgaben nur dann geeignet, wenn sie Optionen bieten, die das Verhalten in solchen Fällen garantieren.

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