Undefined behavior
Macht das gesamte Programm bedeutungslos, wenn bestimmte Regeln der Sprache verletzt werden.
Inhaltsverzeichnis |
Erklärung
Der C++-Standard definiert präzise das beobachtbare Verhalten jedes C++-Programms, das nicht in eine der folgenden Kategorien fällt:
- ill-formed - Das Programm enthält Syntaxfehler oder diagnostizierbare semantische Fehler.
-
- Ein konformer C++-Compiler ist verpflichtet, eine Diagnose auszugeben, selbst wenn er eine Spracherweiterung definiert, die solchem Code eine Bedeutung zuweist (wie etwa bei Arrays variabler Länge).
- Der Text des Standards verwendet shall , shall not , und ill-formed , um diese Anforderungen zu kennzeichnen.
- ill-formed, no diagnostic required - Das Programm weist semantische Fehler auf, die im Allgemeinen nicht diagnostizierbar sein müssen (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.
- implementation-defined behavior - Das Verhalten des Programms variiert zwischen Implementierungen, und die konforme Implementierung muss die Auswirkungen jedes Verhaltens dokumentieren.
-
- Beispielsweise 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 localespezifisches Verhalten , das von der implementierungsabhängigen Locale abhängt.
- unspecified behavior - Das Verhalten des Programms variiert zwischen Implementierungen, und eine konforme Implementierung ist nicht verpflichtet, die Auswirkungen jedes Verhaltens zu dokumentieren.
-
- Zum Beispiel, Auswertungsreihenfolge , ob identische String-Literale unterschiedlich sind, der Umfang des Array-Allokations-Overheads, usw.
- Jedes nicht spezifizierte Verhalten führt zu einem von mehreren gültigen Ergebnissen.
|
(seit C++26) |
- undefined behavior - Dem Programmverhalten sind keine Einschränkungen auferlegt.
-
- Einige Beispiele für undefiniertes Verhalten sind Datenrennen, Speicherzugriffe außerhalb von Array-Grenzen, Überlauf vorzeichenbehafteter Ganzzahlen, Dereferenzierung von Nullzeigern, mehrfache Modifikationen desselben Skalars in einem Ausdruck ohne zwischenliegenden Sequenzpunkt (bis C++11) die unsequenziert sind (seit C++11) , Zugriff auf ein Objekt durch einen Zeiger eines anderen Typs , usw.
- Implementierungen sind nicht verpflichtet, undefiniertes Verhalten zu diagnostizieren (obwohl viele einfache Situationen diagnostiziert werden), und das kompilierte Programm ist nicht verpflichtet, etwas Sinnvolles zu tun.
|
(seit C++11) |
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 aktivierten Optimierungen kompiliert wird:
Zum Beispiel,
Vorzeichenbehafteter Überlauf
int foo(int x) { return x + 1 > x; // entweder wahr oder UB aufgrund von vorzeichenbehaftetem Überlauf }
kann kompiliert werden als ( Demo )
foo(int): mov eax, 1 ret
Zugriff außerhalb der Grenzen
int table[4] = {}; bool exists_in_table(int v) { // gib in einer der ersten 4 Iterationen true zurück oder UB aufgrund von Zugriff außerhalb der Grenzen 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
Nicht initialisierte Skalarvariable
std::size_t f(int x) { std::size_t a; if (x) // entweder x ungleich Null oder UB a = 42; return a; }
Kann kompiliert werden als ( Demo )
f(int): mov eax, 42 ret
Die gezeigte Ausgabe wurde auf einer älteren Version von gcc beobachtet
Mögliche Ausgabe:
p is true p is false
Ungültiger Skalar
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // Das Lesen von b ist jetzt UB return b == 0; }
Kann kompiliert werden als ( Demo )
f(): mov eax, 11 ret
Nullzeiger-Dereferenzierung
Die Beispiele demonstrieren das Lesen aus dem Ergebnis der Dereferenzierung eines Nullzeigers.
int foo(int* p) { int x = *p; if (!p) return x; // Entweder UB oben oder dieser Zweig wird nie ausgeführt else return 0; } int bar() { int* p = nullptr; return *p; // Unbedingtes UB }
kann kompiliert werden als ( Demo )
foo(int*): xor eax, eax ret bar(): ret
Zugriff auf Zeiger übergeben an std::realloc
Wählen Sie clang aus, 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
Endlosschleife ohne Nebeneffekte
Wählen Sie clang oder den neuesten gcc, um die gezeigte 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!
Fehlerhaft mit Diagnosemeldung
Beachten Sie, dass Compiler die Sprache in einer Weise erweitern dürfen, die fehlerhaften Programmen Bedeutung verleiht. Das Einzige, was der C++-Standard in solchen Fällen verlangt, ist eine Diagnosemeldung (Compiler-Warnung), es sei denn, das Programm war "ill-formed no diagnostic required".
Beispielsweise wird GCC, sofern Spracherweiterungen nicht durch
--pedantic-errors
deaktiviert wurden, das folgende Beispiel
nur mit einer Warnung
kompilieren, obwohl es
im C++-Standard
als Beispiel eines "Fehlers" erscheint (siehe auch
GCC Bugzilla #55783
)
#include <iostream> // Beispielanpassung, keine Konstante verwenden double a{1.0}; // C++23 Standard, §9.4.5 Listeninitialisierung [dcl.init.list], Beispiel #6: struct S { // keine Initializer-List-Konstruktoren S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK, ruft #1 auf S s2{a, 2, 3}; // Fehler: Einschränkung S s3{}; // OK, ruft #2 auf // — Beispielende] 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.
Referenzen
| Erweiterter Inhalt |
|---|
|
Siehe auch
[[
assume
(
expression
)]]
(C++23)
|
spezifiziert, dass der
Ausdruck
an einem bestimmten Punkt immer zu
true
ausgewertet wird
(Attributspezifizierer) |
[[
indeterminate
]]
(C++26)
|
spezifiziert, dass ein Objekt einen unbestimmten Wert hat, wenn es nicht initialisiert ist
(Attributspezifizierer) |
|
(C++23)
|
markiert einen unerreichbaren Punkt der Ausführung
(Funktion) |
|
C-Dokumentation
für
Undefiniertes Verhalten
|
|