Undefiniertes Verhalten in C++
Wenn ein Programm undefiniertes Verhalten (“undefined behavior ”, abgekürzt “UB ”) zeigt, so ist die C++ -Implementation von allen ihren Verpflichtungen bei der Ausführung (einschließlich der Übersetzung) jenes Programms entbunden. Anders gesagt, kann die Implementation und das Programm dann also „machen was es will“.
Eine C++ -Implementation ist nicht immer in der Lage und daher nicht dazu verpflichtet, undefiniertes Verhalten zu erkennen und zu melden. Vielmehr ist es die Aufgabe des C++ -Programmierers, Ausführungen mit undefiniertem Verhalten zu vermeiden.
Es ist ein Fehler des Programmierers, ein Programm zu schreiben, das unter den Umständen, unter denen es eingesetzt wird, undefiniertes Verhalten zeigt.
Ein Programm, das eine Division durch »0« oder »0.0« enthält, hat undefiniertes Verhalten.
main.cpp
#include <iostream>
#include <ostream>
#include <string>using namespace ::std::literals;
int main() { ::std::cout << 1/0 << "\n"s; }
- Protokoll
- Programmabbruch während der Ausführung
main.cpp
#include <iostream>
#include <ostream>
#include <string>using namespace ::std::literals;
int main() { ::std::cout << 1/0. << "\n"s; }
- Protokoll
inf
Was passiert tatsächlich bei der Ausführung eines Programms mit undefiniertem Verhalten? Dies läßt sich nicht vorhersagen! Alle der folgenden Möglichkeiten kommen in Frage:
- Das Programm tut genau das, was der Programmierer erwartet.
- Das Programm bricht mit einer Fehlermeldung ab.
- Das Programm tut unter einer C++ -Implementation etwas anderes als unter einer anderen.
- Die vorherige Möglichkeit schließt auch die Möglichkeit ein, daß das Programm das tut, was erwartet wird, wenn die Laufzeitoptimierung durch die Implementation ausgeschaltet ist, aber nicht mehr, nachdem sie eingeschaltet wurde.
- Das Programm bleibt stehen.
- Das Programm tut etwas Unerwartetes.
Insbesondere kann es passieren, daß ein Programm mit undefiniertem Verhalten unter einer C++ -Implementation genau das tut, was der Programmierer erwartet, aber unter einer andere C++ -Implementation ein anderes Verhalten zeigt. Daher muß man sich beim Betrachten der in dieser Lektion gezeigten Beispiele darüber im Klaren sein, daß diese die Manifestation des undefinierten Verhaltens nur unter einer bestimmten Implementation zeigen. Bei Verwendung einer anderen Implementation oder sogar bei Verwendung derselben Implementation unter anderen Umständen, kann sich das beobachtete Verhalten wieder unterscheiden. Insbesondere wird bei undefiniertem Verhalten keinesfalls immer eine Fehlermeldung ausgegeben, so daß es leider oft unbemerkt bleibt. Undefiniertes Verhalten ist eben grundsätzlich abstrakt und kann nur mit dem Verstand, aber nicht mit den Augen, gesehen werden!
Auch die Auswertung von »INT_MAX+1« hat undefiniertes Verhalten, da hier der Wertebereich des Datentyps »int« verlassen wird.
main.cpp
#include <climits> /* INT_MAX */
#include <iostream>
#include <ostream>
#include <string>using namespace ::std::literals;
int main()
{ ::std::cout << INT_MAX << "\n"s;
::std::cout << INT_MAX+1 << "\n"s; }- Protokoll
2147483647
-2147483648
(Zur Erkennung solcher „Rechenfehler“ gibt es verschiedene “safeint ”-Zusatzbibliotheken.)
Undefiniertes Verhalten ist kein Verhalten eines Programmes, sondern einer Ausführung eines Programms. Wenn ein Programm beispielsweise eine Zahl einliest und dann durch diese Zahl dividiert, dann zeigt es undefiniertes Verhalten, wenn Null eingegeben wird. Wenn keine Null eingegeben wird, dann hat dies Ausführung auch kein undefiniertes Verhalten. Ein Programm sollte so geschrieben werden, daß die zu erwartenden Ausführungen nie undefiniertes Verhalten zeigen. Wenn beispielsweise durch irgendwelche Umstände sichergestellt ist, daß nie Null eingegeben wird, dann kann man durch eine eingegebene Zahl dividieren, ohne dabei undefiniertes Verhalten befürchten zu müssen.
- Zitate *
- “If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.”, C++ 2016, 5p4
- “If the second operand of / or % is zero the behavior is undefined.”, C++ 2016, 5.6p4
- “undefined behavior” “behavior for which this International Standard imposes no requirements” C++ 2016, 1.3.25
- “However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation).” – C++ 2016, 1.9p5
Gründe für die Verwendung undefinierten Verhaltens
Undefiniertes Verhalten ist eines der zentralen Begriffe der C++ -Programmierung. Es ergibt sich unter anderem aus folgender Überlegung:
Ein Prozessor ist ein Gerät, welches ein übersetztes Programm ausführt. Eine Division im Quelltext wird in der Regel in ein Divisionskommando für den Prozessor übersetzt. Es gibt aber verschiedene Prozessoren, deren Verhalten bei einer Division sich in Details unterscheiden kann. Beispielsweise ist folgendes möglich:
- Prozessor A bricht das Programm bei einer Division durch 0 ab.
- Prozessor B liefert bei einer Division durch 0 einen speziellen Wert.
Ein C++ -Programm soll nun für beide Prozessoren übersetzt werden. Wie sollte das Verhalten der Programmiersprache für eine Division durch 0 definiert werden?
- Würde es so definiert werden, daß das Programm bei Division durch 0 abgebrochen wird, dann müßte bei der Übersetzung des Programms für den Prozessor B bei jeder Division zusätzlich auf 0 getestet werden, und gegebenenfalls ein Programmabbruch (wie bei Prozessor A) irgendwie simuliert werden. Dadurch würden alle Divisionen auf dem Prozessor B verlangsamt werden.
- Würde es so definiert werden, daß das Programm bei Division durch 0 einen speziellen Wert liefert, dann müßte bei der Übersetzung des Programms für den Prozessor A bei jeder Division zusätzlich auf 0 getestet werden, und gegebenenfalls die Lieferung eines speziellen Wertes veranlaßt werden. Dadurch würden alle Divisionen auf dem Prozessor A verlangsamt werden.
Würde also ein spezielles Verhalten für C++ festgelegt werden, so würde das Programm unter einigen Umgebungen nur verlangsamt laufen.
Portabilität ⃗
Ein Programm ist portabel, wenn es unter jeder C++ -Implementation das gewünschte Verhalten zeigt.
Unspezifiziertes Verhalten ⃗
Bei unspezifiziertem Verhalten, ist eine bestimmte Entscheidung nicht spezifiziert. Es wird oft eine kurze Liste von Möglichkeiten aufgezählt, und jede C++ -Implementation muß eine dieser Möglichkeiten umsetzen.
Die Verwendung unspezifizierten Verhaltens muß kein Fehler des Programmierers sein – etwa, wenn alle in Frage kommenden Möglichkeiten akzeptabel sind, beispielsweise, weil sie das Ergebnis einer Berechnung nicht beeinflussen.
Implementationsdefiniertes Verhalten ⃗
Implementationsdefiniertes Verhalten ist unspezifiziertes Verhalten, das durch jede Implementation in ihrer Dokumentation festgelegt werden muß.
Seine Verwendung muß nicht unbedingt ein Fehler des Programmierers sein, wenn er das Verhalten einer bestimmten Implementation kennt und wünscht. Allerdings sind Programme mit implementationsdefiniertem Verhalten im allgemeinen nicht portabel.
Warum hat die Division durch Null kein unspezifiziertes oder implementations-definiertes Verhalten? ⃗
Die Division durch Null könnte auch als „unspezifiziertes“ oder „implementations-definiertes“ Verhalten definiert werden. Dies würde bedeuten, daß das Verhalten von der Implementation abhängt und die Norm in der Regel die möglichen Verhaltensweisen auflistet.
Undefiniertes Verhalten gibt der Implementation jedoch zusätzliche Freiheiten:
- Die Implementation kann es schon beim Übersetzen als Fehler melden und die Übersetzung abbrechen.
- Die Implementation kann davon ausgehen, daß es nicht vorkommt, und so aggressiver optimieren.
- Die Implementation ist nicht verpflichtet ein bestimmtes Verhalten reproduzierbar zu zeigen.
- Insbesondere muß die Implementation das undefinierte Verhalten nicht erkennen und melden. Diese könnte manchmal sehr kompliziert, zeitaufwendig oder unmöglich sein.
Außerdem deckt das undefinierte Verhalten auch den Fall ab, daß ein Prozessor selber undefiniertes Verhalten zeigt.