Undefiniertes Verhalten
Macht das gesamte Programm bedeutungslos, wenn bestimmte Regeln der Sprache verletzt werden.
Inhalt |
[bearbeiten] Erklärung
Der C++-Standard definiert das beobachtbare Verhalten jedes C++-Programms genau, das nicht in eine der folgenden Klassen fällt:
- fehlerhaft – Das Programm weist Syntaxfehler oder diagnostizierbare semantische Fehler auf.
- Ein konformer C++-Compiler ist verpflichtet, eine Diagnose auszugeben, auch wenn er eine Spracherweiterung definiert, die solchem Code eine Bedeutung zuweist (z. B. mit variablen langen Arrays).
- Der Text des Standards verwendet die Begriffe muss, darf nicht und fehlerhaft, um diese Anforderungen zu kennzeichnen.
- fehlerhaft, keine Diagnose erforderlich – Das Programm weist semantische Fehler auf, die im Allgemeinen nicht diagnostizierbar sind (z. B. Verstöße gegen die ODR oder andere Fehler, die erst zur Linkzeit erkennbar sind).
- Das Verhalten ist undefiniert, wenn ein solches Programm ausgeführt wird.
- implementierungsdefinierte Verhalten – Das Verhalten des Programms variiert zwischen Implementierungen, und die konforme Implementierung muss die Auswirkungen jedes Verhaltens dokumentieren.
- Zum Beispiel der Typ von std::size_t oder die Anzahl der Bits in einem Byte oder der Text von std::bad_alloc::what.
- Eine Teilmenge des implementierungsdefinierten Verhaltens ist lokalisiertes Verhalten, das von der Implementierung bereitgestellten Locale abhängt.
- nicht spezifiziertes Verhalten – Das Verhalten des Programms variiert zwischen Implementierungen, und die konforme Implementierung ist nicht verpflichtet, die Auswirkungen jedes Verhaltens zu dokumentieren.
- Zum Beispiel die Auswertungsreihenfolge, ob identische String-Literale unterschiedlich sind, der Umfang des Array-Allokierungs-Overheads usw.
- Jedes nicht spezifizierte Verhalten führt zu einem von einer Reihe gültiger Ergebnisse.
|
(seit C++26) |
- undefiniertes Verhalten – Es gibt keine Einschränkungen für das Verhalten des Programms.
- Einige Beispiele für undefiniertes Verhalten sind Datenrennen, Speicherzugriffe außerhalb von Array-Grenzen, vorzeichenbehafteter Ganzzahlüberlauf, Dereferenzierung eines Nullzeigers, mehr als eine Modifikation desselben Skalarwerts in einem Ausdruck ohne einen zwischenzeitlichen Sequenzpunkt(bis C++11), der unsequenziert ist(seit C++11), Zugriff auf ein Objekt über einen Zeiger eines anderen Typs usw.
- Implementierungen müssen undefiniertes Verhalten nicht diagnostizieren (obwohl viele einfache Situationen diagnostiziert werden), und das kompilierte Programm muss nichts Sinnvolles tun.
|
(seit C++11) |
[bearbeiten] UB und Optimierung
Da korrekte C++-Programme frei von undefiniertem Verhalten sind, können Compiler unerwartete Ergebnisse liefern, wenn ein Programm, das tatsächlich UB aufweist, mit aktivierter Optimierung kompiliert wird.
Zum Beispiel,
[bearbeiten] Vorzeichenbehafteter Überlauf
int foo(int x) { return x + 1 > x; // either true or UB due to signed overflow }
kann kompiliert werden als (Demo)
foo(int): mov eax, 1 ret
[bearbeiten] Zugriff außerhalb der Grenzen
int table[4] = {}; bool exists_in_table(int v) { // return true in one of the first 4 iterations or UB due to out-of-bounds access for (int i = 0; i <= 4; i++) if (table[i] == v) return true; return false; }
kann kompiliert werden als (Demo)
exists_in_table(int): mov eax, 1 ret
[bearbeiten] Nicht initialisierte Skalarvariable
std::size_t f(int x) { std::size_t a; if (x) // either x nonzero or UB a = 42; return a; }
kann kompiliert werden als (Demo)
f(int): mov eax, 42 ret
Die angezeigte Ausgabe wurde auf einer älteren Version von GCC beobachtet.
Mögliche Ausgabe
p is true p is false
[bearbeiten] Ungültige Skalarvariable
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // reading from b is now UB return b == 0; }
kann kompiliert werden als (Demo)
f(): mov eax, 11 ret
[bearbeiten] Dereferenzierung eines Nullzeigers
Die Beispiele zeigen das Lesen aus dem Ergebnis der Dereferenzierung eines Nullzeigers.
int foo(int* p) { int x = *p; if (!p) return x; // Either UB above or this branch is never taken else return 0; } int bar() { int* p = nullptr; return *p; // Unconditional UB }
kann kompiliert werden als (Demo)
foo(int*): xor eax, eax ret bar(): ret
[bearbeiten] Zugriff auf einen an std::realloc übergebenen Zeiger
Wählen Sie clang, um die angezeigte Ausgabe zu beobachten.
#include <cstdlib> #include <iostream> int main() { int* p = (int*)std::malloc(sizeof(int)); int* q = (int*)std::realloc(p, sizeof(int)); *p = 1; // UB access to a pointer that was passed to realloc *q = 2; if (p == q) // UB access to a pointer that was passed to realloc std::cout << *p << *q << '\n'; }
Mögliche Ausgabe
12
[bearbeiten] Endlosschleife ohne Nebeneffekte
Wählen Sie clang oder das neueste GCC, um die angezeigte Ausgabe zu beobachten.
#include <iostream> bool fermat() { const int max_value = 1000; // Non-trivial infinite loop with no side effects is UB for (int a = 1, b = 1, c = 1; true; ) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return true; // disproved :() a++; if (a > max_value) { a = 1; b++; } if (b > max_value) { b = 1; c++; } if (c > max_value) c = 1; } return false; // not disproved } int main() { std::cout << "Fermat's Last Theorem "; fermat() ? std::cout << "has been disproved!\n" : std::cout << "has not been disproved.\n"; }
Mögliche Ausgabe
Fermat's Last Theorem has been disproved!
[bearbeiten] Fehlerhaft mit Diagnosemeldung
Beachten Sie, dass Compiler die Sprache auf eine Weise erweitern dürfen, die fehlerhaften Programmen eine Bedeutung verleiht. Das einzige, was der C++-Standard in solchen Fällen vorschreibt, ist eine Diagnosemeldung (Compiler-Warnung), es sei denn, das Programm war "fehlerhaft, keine Diagnose erforderlich".
Zum Beispiel kompiliert GCC das folgende Beispiel mit nur einer Warnung (es sei denn, Spracherweiterungen werden über --pedantic-errors deaktiviert), obwohl es im C++-Standard als Beispiel für einen "Fehler" aufgeführt ist (siehe auch GCC Bugzilla #55783).
#include <iostream> // Example tweak, do not use constant double a{1.0}; // C++23 standard, §9.4.5 List-initialization [dcl.init.list], Example #6: struct S { // no initializer-list constructors S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK, invoke #1 S s2{a, 2, 3}; // error: narrowing S s3{}; // OK, invoke #2 // — end example] S::S(int, double, double) {} S::S() {} int main() { std::cout << "All checks have passed.\n"; }
Mögliche Ausgabe
main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
^
static_cast<int>( )
1 error generated.[bearbeiten] Referenzen
| Erweiterter Inhalt |
|---|
|
[bearbeiten] Siehe auch
[[assume(ausdruck)]](C++23) |
spezifiziert, dass der ausdruck an einer gegebenen Stelle immer zu true ausgewertet wird (Attribut-Spezifizierer) |
[[indeterminate]](C++26) |
spezifiziert, dass ein Objekt einen unbestimmten Wert hat, wenn es nicht initialisiert ist. (Attributspezifikator) |
| (C++23) |
markiert einen unerreichbaren Ausführungspunkt (funktion) |
| C-Dokumentation für Undefiniertes Verhalten
| |