Namespaces
Variants

Undefined behavior

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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.
  • erroneous behavior - Das (fehlerhafte) Verhalten, das die Implementierung empfohlenermaßen diagnostizieren soll.
  • Erroneous behavior ist immer die Folge von fehlerhaftem Programmcode.
  • Die Auswertung eines konstanten Ausdrucks führt niemals zu erroneous behavior.
  • Wenn die Ausführung eine Operation enthält, die als erroneous behavior spezifiziert ist, ist die Implementierung berechtigt und wird empfohlen, eine Diagnose auszugeben, und darf die Ausführung zu einem nicht spezifizierten Zeitpunkt nach dieser Operation beenden.
  • Eine Implementierung kann eine Diagnose ausgeben, wenn sie feststellen kann, dass erroneous behavior unter einem implementierungsspezifischen Satz von Annahmen über das Programmverhalten erreichbar ist, was zu falsch positiven Ergebnissen führen kann.
Beispiele für erroneous behavior
#include <cassert>
#include <cstring>
void f()
{   
    int d1, d2;       // d1, d2 have erroneous values
    int e1 = d1;      // erroneous behavior
    int e2 = d1;      // erroneous behavior
    assert(e1 == e2); // holds
    assert(e1 == d1); // holds, erroneous behavior
    assert(e2 == d1); // holds, erroneous behavior
    std::memcpy(&d2, &d1, sizeof(int)); // no erroneous behavior, but
                                        // d2 has an erroneous value
    assert(e1 == d2); // holds, erroneous behavior
    assert(e2 == d2); // holds, erroneous behavior
}
unsigned char g(bool b)
{
    unsigned char c;     // c has erroneous value
    unsigned char d = c; // no erroneous behavior, but d has an erroneous value
    assert(c == d);      // holds, both integral promotions have erroneous behavior
    int e = d;           // erroneous behavior
    return b ? d : 0;    // erroneous behavior if b is true
}
(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.
  • Laufzeit-undefiniertes Verhalten - Das Verhalten, das undefiniert ist, außer wenn es während der Auswertung eines Ausdrucks als Core Constant Expression auftritt.
(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

#include <cstdio>
int main()
{
    bool p; // uninitialized local variable
    if (p)  // UB access to uninitialized scalar
        std::puts("p is true");
    if (!p) // UB access to uninitialized scalar
        std::puts("p is false");
}

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
  • C++23-Standard (ISO/IEC 14882:2024):
  • 3.25 ill-formed program [defns.ill.formed]
  • 3.26 implementation-defined behavior [defns.impl.defined]
  • 3.66 unspecified behavior [defns.unspecified]
  • 3.68 well-formed program [defns.well.formed]
  • C++20-Standard (ISO/IEC 14882:2020):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++17-Standard (ISO/IEC 14882:2017):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++14-Standard (ISO/IEC 14882:2014):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++11-Standard (ISO/IEC 14882:2011):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++98-Standard (ISO/IEC 14882:1998):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]

Siehe auch

[[ assume ( expression )]]
(C++23)
spezifiziert, dass der Ausdruck an einem bestimmten Punkt immer zu true ausgewertet wird
(Attributspezifizierer)
(C++26)
spezifiziert, dass ein Objekt einen unbestimmten Wert hat, wenn es nicht initialisiert ist
(Attributspezifizierer)
markiert einen unerreichbaren Punkt der Ausführung
(Funktion)
C-Dokumentation für Undefiniertes Verhalten

Externe Links

1. The LLVM Project Blog: Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte #1/3
2. The LLVM Project Blog: Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte #2/3
3. The LLVM Project Blog: Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte #3/3
4. Undefiniertes Verhalten kann Zeitreisen verursachen (unter anderem, aber Zeitreisen sind die ausgefallenste Folge)
5. Understanding Integer Overflow in C/C++
6. Spaß mit NULL-Zeigern, Teil 1 (lokaler Exploit in Linux 2.6.30 verursacht durch UB aufgrund von Nullzeiger-Dereferenzierung)
7. Undefiniertes Verhalten und Fermats letzter Satz
8. Leitfaden für C++-Programmierer zu undefiniertem Verhalten