C++
Undefiniertes Verhalten
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):
Die semantischen Beschreibungen in dieser Internationalen Norm definieren eine parametrisierte nichtdeterministische abstrakte Maschine. [SCHNITT]
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]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.
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
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.
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
wobein
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.
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 alsunsigned 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, wirdtransaction::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
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 ...
};