Bedeutet das Vorhandensein einer solchen Anweisung in einem bestimmten Programm, dass das gesamte Programm undefiniert ist oder dass das Verhalten erst dann undefiniert wird, wenn der Kontrollfluss diese Anweisung trifft?
Weder. Die erste Bedingung ist zu stark und die zweite zu schwach.
Der Objektzugriff wird manchmal sequenziert, aber der Standard beschreibt das Verhalten des Programms außerhalb der Zeit. Danvil zitierte bereits:
Wenn eine solche Ausführung eine undefinierte Operation enthält, stellt diese Internationale Norm keine Anforderung an die Implementierung, die dieses Programm mit dieser Eingabe ausführt (nicht einmal in Bezug auf Operationen, die der ersten undefinierten Operation vorausgehen).
Dies kann interpretiert werden:
Wenn die Ausführung des Programms ein undefiniertes Verhalten ergibt, hat das gesamte Programm ein undefiniertes Verhalten.
Eine nicht erreichbare Anweisung mit UB gibt dem Programm also nicht UB. Eine erreichbare Aussage, die (aufgrund der Werte von Eingaben) niemals erreicht wird, gibt dem Programm keine UB. Deshalb ist Ihre erste Bedingung zu stark.
Jetzt kann der Compiler im Allgemeinen nicht sagen, was UB hat. Damit der Optimierer Anweisungen mit potenziellem UB neu anordnen kann, die bei Definition ihres Verhaltens nachbestellbar wären, muss UB vor dem vorhergehenden Sequenzpunkt (oder in C) in die Zeit zurückgreifen und einen Fehler machen ++ 11 Terminologie, damit die UB Dinge beeinflusst, die vor der UB-Sache sequenziert werden). Daher ist Ihre zweite Bedingung zu schwach.
Ein wichtiges Beispiel hierfür ist, wenn der Optimierer auf striktem Aliasing beruht. Der Sinn der strengen Aliasing-Regeln besteht darin, dem Compiler zu ermöglichen, Vorgänge neu zu ordnen, die nicht gültig neu angeordnet werden könnten, wenn es möglich wäre, dass die fraglichen Zeiger denselben Speicher aliasen. Wenn Sie also illegal Aliasing-Zeiger verwenden und UB auftritt, kann dies leicht eine Anweisung "vor" der UB-Anweisung beeinflussen. Für die abstrakte Maschine wurde die UB-Anweisung noch nicht ausgeführt. Der eigentliche Objektcode wurde teilweise oder vollständig ausgeführt. Der Standard versucht jedoch nicht, detailliert darzulegen, was es für den Optimierer bedeutet, Anweisungen neu zu ordnen oder welche Auswirkungen dies auf UB hat. Es gibt nur die Implementierungslizenz, um schief zu gehen, sobald es gefällt.
Sie können sich das als "UB hat eine Zeitmaschine" vorstellen.
Speziell um Ihre Beispiele zu beantworten:
- Das Verhalten ist nur undefiniert, wenn 3 gelesen wird.
- Compiler können und können Code als tot eliminieren, wenn ein Basisblock eine Operation enthält, die sicher undefiniert ist. Sie sind in Fällen zulässig (und ich vermute es auch), die kein grundlegender Block sind, in denen jedoch alle Zweige zu UB führen. Dieses Beispiel ist kein Kandidat, es
PrintToConsole(3)
sei denn, es ist bekannt, dass es sicher zurückkehren wird. Es könnte eine Ausnahme auslösen oder was auch immer.
Ein ähnliches Beispiel wie bei Ihrem zweiten ist die Option gcc -fdelete-null-pointer-checks
, die Code wie diesen annehmen kann (ich habe dieses spezielle Beispiel nicht überprüft, betrachte es als Beispiel für die allgemeine Idee):
void foo(int *p) {
if (p) *p = 3;
std::cout << *p << '\n';
}
und ändern Sie es zu:
*p = 3;
std::cout << "3\n";
Warum? Wenn if p
null ist, hat der Code ohnehin UB, sodass der Compiler davon ausgehen kann, dass er nicht null ist, und entsprechend optimieren kann. Der Linux - Kernel über diese angesprochen ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) im Wesentlichen , weil es in einem Modus arbeitet , wo dereferencing ein Null - Zeiger nicht sollte Bei UB wird erwartet, dass dies zu einer definierten Hardware-Ausnahme führt, die der Kernel verarbeiten kann. Wenn die Optimierung aktiviert ist, muss gcc verwendet -fno-delete-null-pointer-checks
werden, um diese über den Standard hinausgehende Garantie zu gewährleisten.
PS Die praktische Antwort auf die Frage "Wann schlägt undefiniertes Verhalten zu?" ist "10 Minuten bevor Sie für den Tag abreisen wollten".