Namensräume
Varianten
Aktionen

Undefiniertes Verhalten

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 
 

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.
  • fehlerhaftes Verhalten – Das (falsche) Verhalten, das die Implementierung diagnostizieren sollte.
  • Fehlerhaftes Verhalten ist immer die Folge von fehlerhaftem Programmcode.
  • Die Auswertung eines konstanten Ausdrucks führt niemals zu einem fehlerhaften Verhalten.
  • Wenn die Ausführung eine Operation enthält, die als fehlerhaftes Verhalten spezifiziert ist, darf die Implementierung eine Diagnose ausgeben und wird empfohlen, dies zu tun, 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 ein fehlerhaftes Verhalten unter einer implementierungsspezifischen Menge von Annahmen über das Programmverhalten erreichbar ist, was zu Fehlalarmen führen kann.
Beispiele für fehlerhaftes Verhalten
#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)
  • 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.
  • Laufzeit-undefiniertes Verhalten – Das Verhalten, das undefiniert ist, außer wenn es während der Auswertung eines Ausdrucks als Kern-Konstanten-Ausdruck auftritt.
(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.

#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

[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
  • 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]

[bearbeiten] Siehe auch

[[assume(ausdruck)]]
(C++23)
spezifiziert, dass der ausdruck an einer gegebenen Stelle immer zu true ausgewertet wird
(Attribut-Spezifizierer)[bearbeiten]
(C++26)
spezifiziert, dass ein Objekt einen unbestimmten Wert hat, wenn es nicht initialisiert ist.
(Attributspezifikator)[bearbeiten]
markiert einen unerreichbaren Ausführungspunkt
(funktion) [bearbeiten]
C-Dokumentation für Undefiniertes Verhalten

[bearbeiten] Externe Links

1.  The LLVM Project Blog: What Every C Programmer Should Know About Undefined Behavior #1/3
2.  The LLVM Project Blog: What Every C Programmer Should Know About Undefined Behavior #2/3
3.  The LLVM Project Blog: What Every C Programmer Should Know About Undefined Behavior #3/3
4.  Undefined behavior can result in time travel (among other things, but time travel is the funkiest)
5.  Understanding Integer Overflow in C/C++
6.  Fun with NULL pointers, part 1 (lokaler Exploit in Linux 2.6.30 durch UB aufgrund von Nullzeiger-Dereferenzierung)
7.  Undefined Behavior and Fermat’s Last Theorem
8.  C++ programmer's guide to undefined behavior