Suche…


Einführung

Was ist undefiniertes Verhalten (UB)? Gemäß dem ISO-C ++ - Standard (§ 1.3.24, N4296) ist es "Verhalten, für das diese Internationale Norm keine Anforderungen aufstellt"

Das heißt, wenn ein Programm auf UB trifft, darf es tun, was es will. Dies bedeutet oft einen Absturz, aber es kann einfach nichts tun, Dämonen aus der Nase fliegen lassen oder sogar richtig erscheinen zu arbeiten!

Es ist unnötig zu erwähnen, dass Sie es vermeiden sollten, Code zu schreiben, der UB aufruft.

Bemerkungen

Wenn ein Programm ein undefiniertes Verhalten enthält, setzt der C ++ - Standard keine Einschränkungen für sein Verhalten.

  • Es scheint zu funktionieren, als beabsichtigte der Entwickler, aber es kann auch abstürzen oder ungewöhnliche Ergebnisse produzieren.
  • Das Verhalten kann zwischen den Ausführungen desselben Programms variieren.
  • Jeder Teil des Programms kann eine Fehlfunktion aufweisen, einschließlich Zeilen, die vor der Zeile stehen, die undefiniertes Verhalten enthält.
  • Die Implementierung ist nicht erforderlich, um das Ergebnis undefinierten Verhaltens zu dokumentieren.

Eine Implementierung kann das Ergebnis einer Operation dokumentieren, die undefiniertes Verhalten gemäß dem Standard erzeugt, aber ein Programm, das von einem solchen dokumentierten Verhalten abhängt, ist nicht portierbar.

Warum undefiniertes Verhalten existiert

Intuitives, undefiniertes Verhalten wird als schlecht angesehen, da solche Fehler nicht etwa durch Ausnahmebehandler gnädig behandelt werden können.

Aber ein gewisses undefiniertes Verhalten ist eigentlich ein wesentlicher Bestandteil des Versprechens von C ++ "Sie zahlen nicht für das, was Sie nicht verwenden". Durch ein nicht definiertes Verhalten kann ein Compiler davon ausgehen, dass der Entwickler weiß, was er tut, und nicht Code einführen, um die in den obigen Beispielen hervorgehobenen Fehler zu überprüfen.

Undefiniertes Verhalten finden und vermeiden

Einige Tools können verwendet werden, um undefiniertes Verhalten während der Entwicklung zu erkennen:

  • Die meisten Compiler verfügen über Warnflags, die auf einige Fälle undefinierten Verhaltens beim Kompilieren hinweisen.
  • Neuere Versionen von gcc und clang enthalten ein sogenanntes "Undefined Behavior Sanitizer" -Flag ( -fsanitize=undefined ), das zur Laufzeit nach undefiniertem Verhalten zu Leistungskosten prüft.
  • lint -ähnlichen Werkzeuge können gründlichere undefiniertes Verhalten Analyse.

Nicht definiertes, nicht spezifiziertes und implementierungsdefiniertes Verhalten

Vom Standard C ++ 14 (ISO / IEC 14882: 2014), Abschnitt 1.9 (Programmausführung):

  1. Die semantischen Beschreibungen in dieser Internationalen Norm definieren eine parametrisierte nichtdeterministische abstrakte Maschine. [SCHNITT]

  2. Bestimmte Aspekte und Operationen der abstrakten Maschine werden in dieser Internationalen Norm als implementierungsdefiniert beschrieben (z. B. sizeof(int) ). Diese bilden die Parameter der abstrakten Maschine . Jede Implementierung muss eine Dokumentation enthalten, in der ihre Merkmale und ihr Verhalten in dieser Hinsicht beschrieben werden. [SCHNITT]

  3. Bestimmte andere Aspekte und Operationen der abstrakten Maschine werden in dieser Internationalen Norm als nicht spezifiziert beschrieben (z. B. Auswertung von Ausdrücken in einem Neuinitialisierer, wenn die Zuweisungsfunktion keinen Speicher belegt). Wo es möglich ist, definiert diese Internationale Norm eine Reihe zulässiger Verhaltensweisen. Diese definieren die nichtdeterministischen Aspekte der abstrakten Maschine. Eine Instanz der abstrakten Maschine kann somit mehr als eine mögliche Ausführung für ein gegebenes Programm und eine gegebene Eingabe haben.

  4. Bestimmte andere Operationen werden in dieser Internationalen Norm als undefiniert beschrieben (oder die Auswirkungen eines Änderungsversuches eines const Objekts). [ Anmerkung : Diese Internationale Norm stellt keine Anforderungen an das Verhalten von Programmen, die undefiniertes Verhalten enthalten. - Endnote ]

Lesen oder Schreiben durch einen Nullzeiger

int *ptr = nullptr;
*ptr = 1; // Undefined behavior

Dies ist ein undefiniertes Verhalten , da ein Nullzeiger nicht auf ein gültiges Objekt zeigt. Daher gibt es kein Objekt bei *ptr in das geschrieben werden kann.

Dies verursacht zwar meistens einen Segmentierungsfehler, ist jedoch undefiniert und alles kann passieren.

Keine Rückgabeanweisung für eine Funktion mit einem nicht ungültigen Rückgabetyp

Das Auslassen der return Anweisung in einer Funktion, die einen nicht void gültigen return hat void ist undefiniertes Verhalten .

int function() {  
    // Missing return statement
} 

int main() {
    function(); //Undefined Behavior
}

Die meisten modernen Compiler geben zur Kompilierzeit eine Warnung für dieses undefinierte Verhalten aus.


Hinweis: main ist die einzige Ausnahme von der Regel. Wenn main keine return Anweisung hat, fügt der Compiler automatisch return 0; für Sie, so kann es sicher weggelassen werden.

Ändern eines String-Literal

C ++ 11
char *str = "hello world";
str[0] = 'H';

"hello world" ist ein Zeichenkettenliteral, dessen Modifizieren also undefiniertes Verhalten ergibt.

Die Initialisierung von str im obigen Beispiel wurde in C ++ 03 formal veraltet (zur Entfernung aus einer zukünftigen Version des Standards geplant). Einige Compiler vor 2003 könnten eine Warnung ausgeben (z. B. eine verdächtige Konvertierung). Nach 2003 warnen Compiler normalerweise vor einer veralteten Konvertierung.

C ++ 11

Das obige Beispiel ist ungültig und führt zu einer Compilerdiagnose in C ++ 11 und höher. Ein ähnliches Beispiel kann konstruiert werden, um undefiniertes Verhalten zu zeigen, indem die Typumwandlung explizit zugelassen wird, z.

char *str = const_cast<char *>("hello world");
str[0] = 'H'; 

Zugriff auf einen Out-of-Bounds-Index

Es ist nicht definiert , auf einen Index zuzugreifen, der für ein Array (oder einen Standard-Bibliothekscontainer, da diese alle mithilfe eines Raw- Arrays implementiert werden) außerhalb der Grenzen liegt:

 int array[] = {1, 2, 3, 4, 5};
 array[5] = 0;  // Undefined behavior

Es darf ein Zeiger auf das Ende des Arrays (in diesem Fall array + 5 ) verweisen. Sie können ihn nur dereferenzieren, da er kein gültiges Element ist.

 const int *end = array + 5;  // Pointer to one past the last index
 for (int *p = array; p != end; ++p)
   // Do something with `p`

Im Allgemeinen dürfen Sie keinen Out-of-Bounds-Zeiger erstellen. Ein Zeiger muss auf ein Element innerhalb des Arrays oder auf ein Ende nach dem Ende zeigen.

Ganzzahlige Division durch Null

int x = 5 / 0;    // Undefined behavior

Division durch 0 ist mathematisch undefiniert, und als solches macht es Sinn, dass es sich um undefiniertes Verhalten handelt.

Jedoch:

float x = 5.0f / 0.0f;   // x is +infinity

Die meisten Implementierungen implementieren IEEE-754, das die Gleitkommadivision durch Null definiert, um NaN (wenn der Zähler 0.0f ), infinity (wenn der Zähler positiv ist) oder -infinity (wenn der Zähler negativ ist) zurückzugeben.

Signierter Integer-Überlauf

int x = INT_MAX + 1;

// x can be anything -> Undefined behavior

Wenn während der Auswertung eines Ausdrucks das Ergebnis nicht mathematisch definiert ist oder nicht im Bereich der für seinen Typ darstellbaren Werte liegt, ist das Verhalten undefiniert.

(C ++ 11 Standard Absatz 5/4)

Dies ist einer der unangenehmeren, da es in der Regel zu reproduzierbarem, nicht abstürzendem Verhalten führt, so dass Entwickler möglicherweise versucht sind, sich stark auf das beobachtete Verhalten zu verlassen.


Auf der anderen Seite:

unsigned int x = UINT_MAX + 1;

// x is 0

ist gut definiert seit:

Vorzeichenlose Ganzzahlen, die als vorzeichenlos deklariert werden, müssen den Gesetzen der Arithmetik modulo 2^n wobei n die Anzahl der Bits in der Wertedarstellung dieser bestimmten Ganzzahlgröße ist.

(C ++ 11 Standard Absatz 3.9.1 / 4)

Manchmal können Compiler ein undefiniertes Verhalten ausnutzen und optimieren

signed int x ;
if(x > x + 1)
{
    //do something
}

Da hier kein vorzeichenbehafteter Integer-Überlauf definiert ist, kann der Compiler davon ausgehen, dass dies möglicherweise niemals vorkommt, und kann daher den "if" -Block optimieren

Verwendung einer nicht initialisierten lokalen Variablen

int a;
std::cout << a; // Undefined behavior!

Dies führt zu undefiniertem Verhalten , da a nicht initialisiert ist.

Es wird oft fälschlicherweise behauptet, dass dies darauf zurückzuführen ist, dass der Wert "unbestimmt" ist oder "welcher Wert zuvor an diesem Speicherplatz war". Es ist jedoch der Vorgang des Zugriffs auf den Wert von a in dem obigen Beispiel, der undefiniertes Verhalten ergibt. In der Praxis ist das Drucken eines "Müllwerts" in diesem Fall ein häufiges Symptom. Dies ist jedoch nur eine mögliche Form von undefiniertem Verhalten.

Obwohl dies in der Praxis sehr unwahrscheinlich ist (da er auf spezifische Hardware-Unterstützung angewiesen ist), könnte der Compiler beim Programmieren des obigen Codebeispiels den Programmierer gleichfalls durch Stromschlag beschädigen. Mit einer solchen Compiler- und Hardwareunterstützung würde eine solche Reaktion auf undefiniertes Verhalten das Verständnis der durchschnittlichen (lebenden) Programmierer für die wahre Bedeutung von undefiniertem Verhalten deutlich erhöhen - was bedeutet, dass der Standard dem resultierenden Verhalten keine Beschränkungen auferlegt.

C ++ 14

Die Verwendung eines unbestimmten Werts eines unsigned char führt nicht zu undefiniertem Verhalten, wenn der Wert wie folgt verwendet wird:

  • der zweite oder dritte Operand des ternären bedingten Operators;
  • der rechte Operand des eingebauten Kommaoperators;
  • der Operand einer Konvertierung in unsigned char ;
  • der rechte Operand des Zuweisungsoperators, wenn der linke Operand ebenfalls vom Typ unsigned char ;
  • der Initialisierer für ein Objekt unsigned char ;

oder wenn der Wert verworfen wird. In solchen Fällen propagiert der unbestimmte Wert gegebenenfalls einfach zum Ergebnis des Ausdrucks.

Beachten Sie, dass eine static Variable immer nullinitialisiert wird (wenn möglich):

static int a;
std::cout << a; // Defined behavior, 'a' is 0

Mehrere nicht identische Definitionen (die One-Definition-Regel)

Wenn eine Klasse, eine Enumeration, eine Inline-Funktion, eine Vorlage oder ein Member einer Vorlage über eine externe Verknüpfung verfügt und in mehreren Übersetzungseinheiten definiert ist, müssen alle Definitionen identisch sein oder das Verhalten ist gemäß der Definition der One Definition Rule (ODR) undefiniert.

foo.h :

class Foo {
  public:
    double x;
  private:
    int y;
};

Foo get_foo();

foo.cpp :

#include "foo.h"
Foo get_foo() { /* implementation */ }

main.cpp :

// I want access to the private member, so I am going to replace Foo with my own type
class Foo {
  public:
    double x;
    int y;
};
Foo get_foo(); // declare this function ourselves since we aren't including foo.h
int main() {
    Foo foo = get_foo();
    // do something with foo.y
}

Das obige Programm weist ein undefiniertes Verhalten auf, da es zwei Definitionen der Klasse ::Foo , die über eine externe Verknüpfung verfügt, in verschiedenen Übersetzungseinheiten. Die beiden Definitionen sind jedoch nicht identisch. Im Gegensatz zur Neudefinition einer Klasse innerhalb derselben Übersetzungseinheit muss dieses Problem nicht vom Compiler diagnostiziert werden.

Falsche Paarung von Speicherzuweisung und Freigabe

Ein Objekt kann nur dann delete wenn es von new zugewiesen wurde und kein Array ist. Wenn das zu delete Argument nicht von new oder ein Array ist, ist das Verhalten undefiniert.

Ein Objekt kann nur durch delete[] freigegeben werden, wenn es von new zugewiesen wurde und ein Array ist. Wenn das zu delete[] Argument delete[] nicht von new oder kein Array ist, ist das Verhalten undefiniert.

Wenn das Argument free nicht von malloc , ist das Verhalten undefiniert.

int* p1 = new int;
delete p1;      // correct
// delete[] p1; // undefined
// free(p1);    // undefined

int* p2 = new int[10];
delete[] p2;    // correct
// delete p2;   // undefined
// free(p2);    // undefined

int* p3 = static_cast<int*>(malloc(sizeof(int)));
free(p3);       // correct
// delete p3;   // undefined
// delete[] p3; // undefined

Solche Probleme können vermieden werden, indem Sie malloc und free in C ++ - Programmen vollständig vermeiden, die intelligenten Zeiger der Standardbibliothek gegenüber raw new und delete und std::vector und std::string gegenüber raw new und delete[] vorziehen.

Zugriff auf ein Objekt als falscher Typ

In den meisten Fällen ist es illegal, auf ein Objekt eines Typs zuzugreifen, als wäre es ein anderer Typ (ohne Berücksichtigung von cv-qualifiers). Beispiel:

float x = 42;
int y = reinterpret_cast<int&>(x);

Das Ergebnis ist undefiniertes Verhalten.

Es gibt einige Ausnahmen von dieser strengen Aliasing- Regel:

  • Auf ein Objekt des Klassentyps kann zugegriffen werden, als wäre es ein Typ, der eine Basisklasse des tatsächlichen Klassentyps ist.
  • Auf einen beliebigen Typ kann als char oder als unsigned char zugegriffen werden. Das Gegenteil ist jedoch nicht der Fall: Auf ein Zeichenarray kann nicht wie bei einem beliebigen Typ zugegriffen werden.
  • Auf einen vorzeichenbehafteten Integer-Typ kann als entsprechender vorzeichenloser Typ zugegriffen werden und umgekehrt .

Eine verwandte Regel besagt, dass beim Aufruf einer nicht statischen Memberfunktion für ein Objekt, das nicht denselben Typ wie die definierende Klasse der Funktion oder eine abgeleitete Klasse hat, ein undefiniertes Verhalten auftritt. Dies gilt auch dann, wenn die Funktion nicht auf das Objekt zugreift.

struct Base {
};
struct Derived : Base {
    void f() {}
};
struct Unrelated {};
Unrelated u;
Derived& r1 = reinterpret_cast<Derived&>(u); // ok
r1.f();                                      // UB
Base b;
Derived& r2 = reinterpret_cast<Derived&>(b); // ok
r2.f();                                      // UB

Fließpunktüberlauf

Wenn eine arithmetische Operation, die einen Gleitkommatyp ergibt, einen Wert ergibt, der nicht im Bereich der darstellbaren Werte des Ergebnistyps liegt, ist das Verhalten gemäß dem C ++ - Standard undefiniert, kann aber durch andere Standards definiert werden, denen die Maschine möglicherweise entspricht. wie IEEE 754.

float x = 1.0;
for (int i = 0; i < 10000; i++) {
    x *= 10.0; // will probably overflow eventually; undefined behavior
}

(Reine) virtuelle Member vom Konstruktor oder Destruktor aufrufen

Der Standard (10.4) besagt:

Elementfunktionen können von einem Konstruktor (oder Destruktor) einer abstrakten Klasse aufgerufen werden. Der Effekt, einen virtuellen Aufruf (10.3) direkt oder indirekt für eine reine virtuelle Funktion für das Objekt auszuführen, das von einem solchen Konstruktor (oder Destruktor) erstellt (oder zerstört) wird, ist nicht definiert.

Im Allgemeinen schlagen einige C ++ - Behörden, z. B. Scott Meyers, vor , virtuelle Funktionen (auch nicht reine Funktionen) nie von Konstruktoren und Konstruktoren aufzurufen.

Betrachten Sie das folgende Beispiel, das vom obigen Link geändert wurde:

class transaction
{
public:
    transaction(){ log_it(); }
    virtual void log_it() const = 0;
};

class sell_transaction : public transaction
{
public:
    virtual void log_it() const { /* Do something */ }
};

Angenommen, wir erstellen ein sell_transaction Objekt:

sell_transaction s;

Dies ruft implizit den Konstruktor von sell_transaction , der zuerst den Konstruktor von transaction aufruft. Wenn der Konstruktor von transaction aufgerufen wird, ist das Objekt jedoch noch nicht vom Typ sell_transaction , sondern nur vom Typ transaction .

Folglich führt der Aufruf von transaction::transaction() zu log_it nicht zu dem, was vielleicht intuitiv erscheint - nämlich sell_transaction::log_it .

  • Wenn log_it wie in diesem Beispiel rein virtuell ist, ist das Verhalten undefiniert.

  • Wenn log_it nicht rein virtuell ist, wird transaction::log_it aufgerufen.

Löschen eines abgeleiteten Objekts über einen Zeiger auf eine Basisklasse, die keinen virtuellen Destruktor besitzt.

class base { };
class derived: public base { }; 

int main() {
    base* p = new derived();
    delete p; // The is undefined behavior!
}

In Abschnitt [expr.delete] §5.3.5 / 3 sagt der Standard, dass wenn delete für ein Objekt aufgerufen wird, dessen statischer Typ keinen virtual Destruktor hat:

Wenn sich der statische Typ des zu löschenden Objekts von seinem dynamischen Typ unterscheidet, muss der statische Typ eine Basisklasse des dynamischen Typs des zu löschenden Objekts sein und der statische Typ muss einen virtuellen Destruktor haben oder das Verhalten ist undefiniert.

Dies ist unabhängig von der Frage der Fall, ob die abgeleitete Klasse Datenelemente zur Basisklasse hinzugefügt hat.

Zugriff auf eine baumelnde Referenz

Es ist illegal, auf einen Verweis auf ein Objekt zuzugreifen, das den Gültigkeitsbereich verlassen hat oder auf andere Weise zerstört wurde. Es wird gesagt, dass eine solche Referenz baumelt, da sie sich nicht mehr auf ein gültiges Objekt bezieht.

#include <iostream>
int& getX() {
    int x = 42;
    return x;
}
int main() {
    int& r = getX();
    std::cout << r << "\n";
}

In diesem Beispiel getX die lokale Variable x Gültigkeitsbereich, wenn getX zurückgegeben wird. (Beachten Sie, dass die Lebensdauerverlängerung die Lebensdauer einer lokalen Variablen nicht über den Gültigkeitsbereich des Blocks hinaus verlängern kann, in dem sie definiert ist.) Daher ist r eine schwankende Referenz. Dieses Programm hat das Verhalten nicht definiert, obwohl es zu arbeiten und drucken erscheint 42 in einigen Fällen.

Erweiterung des "std" oder "posix" Namespaces

Der Standard (17.6.4.2.1 / 1) verbietet im Allgemeinen die Erweiterung des std Namespaces:

Das Verhalten eines C ++ - Programms ist undefiniert, wenn es Deklarationen oder Definitionen zum Namespace std oder einem Namespace innerhalb des Namespace std hinzufügt, sofern nichts anderes angegeben ist.

Gleiches gilt für posix (17.6.4.2.2 / 1):

Das Verhalten eines C ++ - Programms ist undefiniert, wenn es Deklarationen oder Definitionen zum Namespace posix oder einem Namespace innerhalb des Namespace posix hinzufügt, sofern nichts anderes angegeben ist.

Folgendes berücksichtigen:

#include <algorithm>

namespace std
{
    int foo(){}
}

Nichts im Standard verbietet den algorithm (oder einen der darin enthaltenen Header), die dieselbe Definition definieren. Daher würde dieser Code gegen die One-Definitions-Regel verstoßen.

Das ist also generell verboten. Es sind jedoch bestimmte Ausnahmen zulässig . Am nützlichsten ist es vielleicht, Spezialisierungen für benutzerdefinierte Typen hinzuzufügen. Nehmen Sie beispielsweise an, Ihr Code hat dies

class foo
{
    // Stuff
};

Dann ist das Folgende in Ordnung

namespace std
{
    template<>
    struct hash<foo>
    {
    public:
        size_t operator()(const foo &f) const;
    };
}

Überlauf während der Konvertierung in oder vom Gleitkommatyp

Wenn während der Umwandlung von:

  • einen ganzzahligen Typ zu einem Gleitkommatyp,
  • einem Gleitkommatyp zu einem Ganzzahlentyp oder
  • einem Gleitkommatyp zu einem kürzeren Gleitkommatyp,

Der Quellwert liegt außerhalb des Wertebereichs, der im Zieltyp dargestellt werden kann. Das Ergebnis ist undefiniertes Verhalten. Beispiel:

double x = 1e100;
int y = x; // int probably cannot hold numbers that large, so this is UB

Ungültige statische Umwandlung von Basis zu abgeleiteten Elementen

Wenn mit static_cast ein Zeiger (bzw. eine Referenz) zur Basisklasse in einen Zeiger (bzw. eine Referenz) für eine abgeleitete Klasse konvertiert wird, zeigt der Operand jedoch nicht auf ein Objekt des abgeleiteten Klassentyps (Verhalten) ist nicht definiert. Siehe Konvertierung von Basis zu abgeleiteter Klasse .

Funktionsaufruf durch nicht übereinstimmenden Funktionszeigertyp

Um eine Funktion über einen Funktionszeiger aufzurufen, muss der Typ des Funktionszeigers genau dem Typ der Funktion entsprechen. Ansonsten ist das Verhalten undefiniert. Beispiel:

int f();
void (*p)() = reinterpret_cast<void(*)()>(f);
p(); // undefined

Ein const-Objekt ändern

Jeder Versuch, ein const Objekt zu ändern, führt zu undefiniertem Verhalten. Dies gilt für const Variablen, Member von const Objekten und für const deklarierte Klassenmitglieder. (Ein mutable Member eines const Objekts ist jedoch nicht const .)

Ein solcher Versuch kann mit const_cast :

const int x = 123;
const_cast<int&>(x) = 456;
std::cout << x << '\n';

Ein Compiler wird inline in der Regel den Wert eines const int - Objekt, ist es so möglich , dass dieser Code kompiliert und druckt 123 . Compiler können die Werte von const Objekten auch in den Nur-Lese-Speicher legen, sodass ein Segmentierungsfehler auftreten kann. In jedem Fall ist das Verhalten undefiniert und das Programm kann irgendetwas tun.

Das folgende Programm verbirgt einen weitaus subtileren Fehler:

#include <iostream>

class Foo* instance;

class Foo {
  public:
    int get_x() const { return m_x; }
    void set_x(int x) { m_x = x; }
  private:
    Foo(int x, Foo*& this_ref): m_x(x) {
        this_ref = this;
    }
    int m_x;
    friend const Foo& getFoo();
};

const Foo& getFoo() {
    static const Foo foo(123, instance);
    return foo;
}

void do_evil(int x) {
    instance->set_x(x);
}

int main() {
    const Foo& foo = getFoo();
    do_evil(456);
    std::cout << foo.get_x() << '\n';
}

In diesem Code erstellt getFoo ein Singleton vom Typ const Foo und sein Member m_x wird mit 123 initialisiert. Dann wird do_evil aufgerufen und der Wert von foo.m_x wird offensichtlich in 456 geändert. Was ist schief gelaufen?

Trotz seines Namens tut do_evil nichts besonders Böses; Alles, was es tut, ist ein Setter mit einem Foo* anzurufen. Dieser Zeiger zeigt jedoch auf ein const Foo Objekt, obwohl const_cast nicht verwendet wurde. Dieser Zeiger wurde durch den Konstruktor von Foo . Ein const - Objekt wird nicht const , bis seine Initialisierung abgeschlossen ist, so this hat Typ Foo* , nicht const Foo* , im Konstruktor.

Daher tritt undefiniertes Verhalten auf, obwohl in diesem Programm keine offensichtlich gefährlichen Konstrukte vorhanden sind.

Zugriff auf nicht vorhandenes Mitglied durch Zeiger auf Mitglied

Wenn auf ein nicht statisches Member eines Objekts über einen Zeiger auf Member zugegriffen wird, ist das Verhalten undefiniert, wenn das Objekt tatsächlich nicht das durch den Pointer angegebene Member enthält. (Ein solcher Zeiger auf member kann über static_cast abgerufen werden.)

struct Base { int x; };
struct Derived : Base { int y; };
int Derived::*pdy = &Derived::y;
int Base::*pby = static_cast<int Base::*>(pdy);

Base* b1 = new Derived;
b1->*pby = 42; // ok; sets y in Derived object to 42
Base* b2 = new Base;
b2->*pby = 42; // undefined; there is no y member in Base

Ungültige Umwandlung von Basis zu Basis für Zeiger auf Mitglieder

Wenn mit static_cast TD::* in TB::* konvertiert wird, muss das Member, auf das verwiesen wird, zu einer Klasse gehören, die eine Basisklasse oder eine abgeleitete Klasse von B . Ansonsten ist das Verhalten undefiniert. Weitere Informationen finden Sie unter Abgeleitete in Basisumwandlung für Zeiger auf Mitglieder

Ungültige Zeigerarithmetik

Die folgenden Verwendungen von Zeigerarithmetik verursachen undefiniertes Verhalten:

  • Addition oder Subtraktion einer Ganzzahl, wenn das Ergebnis nicht zu demselben Array-Objekt gehört wie der Zeigeroperand. (Hier gilt das Element eins nach dem Ende als zum Array gehörig.)

    int a[10];
    int* p1 = &a[5];
    int* p2 = p1 + 4; // ok; p2 points to a[9]
    int* p3 = p1 + 5; // ok; p2 points to one past the end of a
    int* p4 = p1 + 6; // UB
    int* p5 = p1 - 5; // ok; p2 points to a[0]
    int* p6 = p1 - 6; // UB
    int* p7 = p3 - 5; // ok; p7 points to a[5]
    
  • Subtraktion von zwei Zeigern, wenn beide nicht zum selben Array-Objekt gehören. (Wiederum wird das Element um eins nach dem Ende als zu dem Array gehörig betrachtet.) Die Ausnahme ist, dass zwei Nullzeiger subtrahiert werden können, was 0 ergibt.

    int a[10];
    int b[10];
    int *p1 = &a[8], *p2 = &a[3];
    int d1 = p1 - p2; // yields 5
    int *p3 = p1 + 2; // ok; p3 points to one past the end of a
    int d2 = p3 - p2; // yields 7
    int *p4 = &b[0];
    int d3 = p4 - p1; // UB
    
  • Subtraktion von zwei Zeigern, wenn das Ergebnis std::ptrdiff_t .

  • Jede Zeigerarithmetik, bei der der Pointee-Typ eines Operanden nicht mit dem dynamischen Typ des Objekts übereinstimmt, auf das er zeigt (Ignorieren der cv-Qualifikation) Gemäß dem Standard "kann insbesondere ein Zeiger auf eine Basisklasse nicht für die Zeigerarithmetik verwendet werden, wenn das Array Objekte eines abgeleiteten Klassentyps enthält."

    struct Base { int x; };
    struct Derived : Base { int y; };
    Derived a[10];
    Base* p1 = &a[1];           // ok
    Base* p2 = p1 + 1;          // UB; p1 points to Derived
    Base* p3 = p1 - 1;          // likewise
    Base* p4 = &a[2];           // ok
    auto p5 = p4 - p1;          // UB; p4 and p1 point to Derived
    const Derived* p6 = &a[1];
    const Derived* p7 = p6 + 1; // ok; cv-qualifiers don't matter
    

Verschiebung um eine ungültige Anzahl von Positionen

Für den eingebauten Verschiebungsoperator muss der rechte Operand nicht negativ sein und ist strikt unter der Bitbreite des beförderten linken Operanden. Ansonsten ist das Verhalten undefiniert.

const int a = 42;
const int b = a << -1; // UB
const int c = a << 0;  // ok
const int d = a << 32; // UB if int is 32 bits or less
const int e = a >> 32; // also UB if int is 32 bits or less
const signed char f = 'x';
const int g = f << 10; // ok even if signed char is 10 bits or less;
                       // int must be at least 16 bits

Rückkehr von einer [[noreturn]] - Funktion

C ++ 11

Beispiel aus dem Standard, [dcl.attr.noreturn]:

[[ noreturn ]] void f() {
  throw "error"; // OK
}
[[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0
  if (i > 0)
    throw "positive";
}

Zerstörung eines bereits zerstörten Objekts

In diesem Beispiel wird ein Destruktor explizit für ein Objekt aufgerufen, das später automatisch gelöscht wird.

struct S {
    ~S() { std::cout << "destroying S\n"; }
};
int main() {
    S s;
    s.~S();
} // UB: s destroyed a second time here

Ein ähnliches Problem tritt auf, wenn ein std::unique_ptr<T> auf ein T mit automatischer oder statischer Speicherdauer verweist.

void f(std::unique_ptr<S> p);
int main() {
    S s;
    std::unique_ptr<S> p(&s);
    f(std::move(p)); // s destroyed upon return from f
}                    // UB: s destroyed

Eine andere Möglichkeit, ein Objekt zweimal zu zerstören, besteht darin, dass zwei shared_ptr s das Objekt verwalten, ohne dass der Besitz miteinander geteilt wird.

void f(std::shared_ptr<S> p1, std::shared_ptr<S> p2);
int main() {
    S* p = new S;
    // I want to pass the same object twice...
    std::shared_ptr<S> sp1(p);
    std::shared_ptr<S> sp2(p);
    f(sp1, sp2);
} // UB: both sp1 and sp2 will destroy s separately
// NB: this is correct:
// std::shared_ptr<S> sp(p);
// f(sp, sp);

Unendliche Vorlagenrekursion

Beispiel aus dem Standard, [temp.inst] / 17:

template<class T> class X {
    X<T>* p; // OK
    X<T*> a; // implicit generation of X<T> requires
             // the implicit instantiation of X<T*> which requires
             // the implicit instantiation of X<T**> which ...
};


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow