Können Zweige mit undefiniertem Verhalten als nicht erreichbar angenommen und als toter Code optimiert werden?

88

Betrachten Sie die folgende Aussage:

*((char*)NULL) = 0; //undefined behavior

Es ruft eindeutig undefiniertes Verhalten hervor. 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?

Wäre das folgende Programm genau definiert, falls der Benutzer die Nummer nie eingibt 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Oder ist es völlig undefiniertes Verhalten, egal was der Benutzer eingibt?

Kann der Compiler auch davon ausgehen, dass undefiniertes Verhalten zur Laufzeit niemals ausgeführt wird? Das würde es ermöglichen, rechtzeitig rückwärts zu argumentieren:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Hier könnte der Compiler argumentieren, dass für den Fall, dass num == 3wir immer undefiniertes Verhalten aufrufen. Daher muss dieser Fall unmöglich sein und die Nummer muss nicht gedruckt werden. Die gesamte ifAussage könnte optimiert werden. Ist diese Art des Rückwärtsdenkens nach dem Standard zulässig?

usr
quelle
19
Manchmal frage ich mich, ob Benutzer mit vielen Wiederholungen mehr Upvotes zu Fragen erhalten, weil "Oh, sie haben viele Wiederholungen, das muss eine gute Frage sein" ... aber in diesem Fall habe ich die Frage gelesen und gedacht "Wow, das ist großartig "bevor ich den Fragesteller überhaupt ansah.
turbulencetoo
4
Ich denke, dass die Zeit, in der das undefinierte Verhalten auftritt, undefiniert ist.
Eerorika
6
Der C ++ - Standard besagt ausdrücklich, dass ein Ausführungspfad mit undefiniertem Verhalten zu jedem Zeitpunkt vollständig undefiniert ist. Ich würde es sogar so interpretieren, dass jedes Programm mit undefiniertem Verhalten auf dem Pfad völlig undefiniert ist (was vernünftige Ergebnisse für andere Teile beinhaltet, aber nicht garantiert ist). Compiler können das undefinierte Verhalten verwenden, um Ihr Programm zu ändern. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html enthält einige schöne Beispiele.
Jens
4
@Jens: Es bedeutet wirklich nur den Ausführungspfad. Sonst geraten Sie in Schwierigkeiten const int i = 0; if (i) 5/i;.
MSalters
1
Der Compiler kann im Allgemeinen nicht beweisen, dass er PrintToConsolenicht aufruft, std::exitdaher muss er den Aufruf ausführen.
MSalters

Antworten:

65

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 pnull 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-checkswerden, 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".

Steve Jessop
quelle
4
Tatsächlich gab es in der Vergangenheit aufgrund dessen einige Sicherheitsprobleme. Insbesondere bei einer nachträglichen Überlaufprüfung besteht die Gefahr, dass sie dadurch wegoptimiert wird. Zum Beispiel void can_add(int x) { if (x + 100 < x) complain(); }kann es vollständig x+100 wegoptimiert werden , denn wenn es nicht zu einem Überlauf kommt, passiert nichts, und wenn x+100 es zu einem Überlauf kommt, ist das UB gemäß dem Standard, sodass möglicherweise nichts passiert.
FGP
3
@fgp: Richtig, das ist eine Optimierung, über die sich die Leute bitter beschweren, wenn sie darüber stolpern, weil es sich anfühlt, als würde der Compiler absichtlich Ihren Code brechen, um Sie zu bestrafen. "Warum hätte ich es so geschrieben, wenn ich wollte, dass du es entfernst!" ;-) Aber ich denke, manchmal ist es für den Optimierer nützlich, größere arithmetische Ausdrücke zu bearbeiten, anzunehmen, dass es keinen Überlauf gibt, und alles zu vermeiden, was nur in diesen Fällen teuer wäre.
Steve Jessop
2
Wäre es richtig zu sagen, dass das Programm nicht undefiniert ist, wenn der Benutzer niemals 3 eingibt, aber wenn er während einer Ausführung 3 eingibt, wird die gesamte Ausführung undefiniert? Sobald es zu 100% sicher ist, dass das Programm undefiniertes Verhalten aufruft (und nicht früher als das), darf das Verhalten irgendetwas sein. Sind meine Aussagen zu 100% richtig?
usr
3
@usr: Ich glaube das ist richtig, ja. Mit Ihrem speziellen Beispiel (und einigen Annahmen über die Unvermeidlichkeit der verarbeiteten Daten) könnte eine Implementierung im gepufferten STDIN im Prinzip nach vorne schauen, 3wenn es wollte, und für den Tag nach Hause packen, sobald sie eines sah eingehend.
Steve Jessop
3
Eine zusätzliche +1 (wenn ich könnte) für Ihre PS
Fred Larson
10

Der Standard besagt 1,9 / 4

[Hinweis: Diese Internationale Norm stellt keine Anforderungen an das Verhalten von Programmen, die undefiniertes Verhalten enthalten. - Endnote]

Der interessante Punkt ist wahrscheinlich, was "enthalten" bedeutet. Wenig später bei 1,9 / 5 heißt es:

Wenn eine solche Ausführung jedoch 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).

Hier wird ausdrücklich "Ausführung ... mit dieser Eingabe" erwähnt. Ich würde das so interpretieren, dass undefiniertes Verhalten in einem möglichen Zweig, der gerade nicht ausgeführt wird, keinen Einfluss auf den aktuellen Ausführungszweig hat.

Ein anderes Problem sind jedoch Annahmen, die auf undefiniertem Verhalten während der Codegenerierung basieren. Weitere Informationen hierzu finden Sie in der Antwort von Steve Jessop.

Danvil
quelle
1
Wörtlich genommen ist dies das Todesurteil für alle existierenden realen Programme.
usr
6
Ich glaube nicht, dass die Frage war, ob UB erscheinen kann, bevor der Code tatsächlich erreicht ist. Die Frage war, wie ich es verstanden habe, ob die UB erscheinen könnte, wenn der Code nicht einmal erreicht würde. Und natürlich lautet die Antwort darauf "nein".
sepp2k
Nun, der Standard ist in 1.9 / 4 nicht so klar, aber 1.9 / 5 kann möglicherweise als das interpretiert werden, was Sie gesagt haben.
Danvil
1
Notizen sind nicht normativ. 1,9 / 5 übertrumpft die Note in 1,9 / 4
MSalters
5

Ein lehrreiches Beispiel ist

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Sowohl der aktuelle GCC als auch der aktuelle Clang optimieren dies (auf x86) auf

xorl %eax,%eax
ret

weil sie daraus ableiten, dassx der UB im Steuerpfad immer Null istif (x) . GCC gibt Ihnen nicht einmal eine Warnung zur Verwendung eines nicht initialisierten Werts! (weil der Durchlauf, der die obige Logik anwendet, vor dem Durchlauf ausgeführt wird, der Warnungen mit nicht initialisierten Werten generiert)

zwol
quelle
1
Interessantes Beispiel. Es ist ziemlich unangenehm, dass das Aktivieren der Optimierung die Warnung verbirgt. Dies ist nicht einmal dokumentiert - die GCC-Dokumente sagen nur, dass das Aktivieren der Optimierung mehr Warnungen erzeugt .
Sleske
@sleske Es ist böse, ich stimme zu, aber nicht initialisierte Wertwarnungen sind notorisch schwer zu "korrigieren" - sie perfekt zu machen ist gleichbedeutend mit dem Problem des Anhaltens, und Programmierer werden seltsam irrational, wenn sie "unnötige" Variableninitialisierungen hinzufügen, um falsch positive Ergebnisse zu unterdrücken. Compiler-Autoren landen also über einem Fass. Früher habe ich GCC gehackt und ich erinnere mich, dass jeder Angst hatte, sich mit dem nicht initialisierten Wert-Warnpass herumzuschlagen.
zwol
@zwol: Ich frage mich, wie viel von der "Optimierung", die sich aus einer solchen Beseitigung von totem Code ergibt, nützlichen Code tatsächlich kleiner macht und wie viel letztendlich dazu führt, dass Programmierer Code größer machen (z. B. durch Hinzufügen von Code zum Initialisieren, aselbst wenn unter allen Umständen ein uninitialized awürde an die Funktion übergeben, mit der die Funktion niemals etwas anfangen würde)?
Supercat
@supercat Ich war seit ~ 10 Jahren nicht mehr tief in die Compilerarbeit involviert und es ist fast unmöglich, über Optimierungen anhand von Spielzeugbeispielen nachzudenken. Diese Art der Optimierung ist in der Regel mit einer Reduzierung der Codegröße um 2-5% bei realen Anwendungen verbunden, wenn ich mich richtig erinnere.
zwol
1
@supercat 2-5% ist riesig, wie diese Dinge gehen. Ich habe Leute gesehen, die um 0,1% geschwitzt haben.
zwol
4

Der aktuelle C ++ - Arbeitsentwurf besagt dies in 1.9.4

Diese Internationale Norm stellt keine Anforderungen an das Verhalten von Programmen, die unbestimmtes Verhalten enthalten.

Auf dieser Grundlage würde ich sagen, dass ein Programm, das undefiniertes Verhalten auf einem beliebigen Ausführungspfad enthält, zu jedem Zeitpunkt seiner Ausführung alles tun kann.

Es gibt zwei wirklich gute Artikel über undefiniertes Verhalten und was Compiler normalerweise tun:

Jens
quelle
1
Das macht keinen Sinn. Die Funktion int f(int x) { if (x > 0) return 100/x; else return 100; }ruft sicherlich niemals undefiniertes Verhalten auf, obwohl sie 100/0natürlich undefiniert ist.
FGP
1
@fgp Was der Standard (insbesondere 1,9 / 5) , sagt aber, dass, wenn nicht definiertes Verhalten kann erreicht werden, es spielt keine Rolle , wenn es erreicht ist. Zum Beispiel printf("Hello, World"); *((char*)NULL) = 0 wird nicht garantiert, dass etwas gedruckt wird. Dies unterstützt die Optimierung, da der Compiler Operationen, von denen er weiß, dass sie irgendwann auftreten werden, frei neu anordnen kann (natürlich abhängig von Abhängigkeitsbeschränkungen), ohne undefiniertes Verhalten berücksichtigen zu müssen.
FGP
Ich würde sagen, dass ein Programm mit Ihrer Funktion kein undefiniertes Verhalten enthält, da es keine Eingabe gibt, bei der 100/0 ausgewertet wird.
Jens
1
Genau - es kommt also darauf an, ob die UB tatsächlich ausgelöst werden kann oder nicht, nicht ob sie theoretisch ausgelöst werden kann. Oder sind Sie bereit zu argumentieren, int x,y; std::cin >> x >> y; std::cout << (x+y);dass "1 + 1 = 17" gesagt werden darf, nur weil es einige Eingaben gibt, bei denen x+yÜberläufe auftreten (was UB ist, da intes sich um einen vorzeichenbehafteten Typ handelt).
FGP
Formal würde ich sagen, dass das Programm ein undefiniertes Verhalten hat, weil es Eingaben gibt, die es auslösen. Sie haben jedoch Recht, dass dies im Kontext von C ++ keinen Sinn ergibt, da es unmöglich wäre, ein Programm ohne undefiniertes Verhalten zu schreiben. Ich würde es mögen, wenn es in C ++ weniger undefiniertes Verhalten gab, aber so funktioniert die Sprache nicht (und es gibt einige gute Gründe dafür, aber sie betreffen nicht meinen täglichen Gebrauch ...).
Jens
3

Das Wort "Verhalten" bedeutet, dass etwas getan wird . Ein Status, der niemals ausgeführt wird, ist kein "Verhalten".

Eine Illustration:

*ptr = 0;

Ist das undefiniertes Verhalten? Angenommen, wir sind uns ptr == nullptrmindestens einmal während der Programmausführung 100% sicher . Die Antwort sollte ja sein.

Was ist damit?

 if (ptr) *ptr = 0;

Ist das undefiniert? (Erinnerst ptr == nullptrdu dich mindestens einmal?) Ich hoffe nicht, sonst kannst du überhaupt kein nützliches Programm schreiben.

Bei der Beantwortung dieser Antwort wurde kein Srandardese verletzt.

n. 'Pronomen' m.
quelle
3

Das undefinierte Verhalten tritt auf, wenn das Programm undefiniertes Verhalten verursacht, unabhängig davon, was als nächstes passiert. Sie haben jedoch das folgende Beispiel angegeben.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Wenn der Compiler die Definition von nicht kennt PrintToConsole, kann er die if (num == 3)Bedingung nicht entfernen . Nehmen wir an, Sie haben einen LongAndCamelCaseStdio.hSystemheader mit der folgenden Deklaration von PrintToConsole.

void PrintToConsole(int);

Nichts zu hilfreich, alles klar. Lassen Sie uns nun sehen, wie böse (oder vielleicht nicht so böse, undefiniertes Verhalten hätte schlimmer sein können) der Anbieter ist, indem wir die tatsächliche Definition dieser Funktion überprüfen.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Der Compiler muss tatsächlich davon ausgehen, dass eine beliebige Funktion, von der der Compiler nicht weiß, was er tut, eine Ausnahme beenden oder auslösen kann (im Fall von C ++). Sie können feststellen, dass dies *((char*)NULL) = 0;nicht ausgeführt wird, da die Ausführung nach dem PrintToConsoleAufruf nicht fortgesetzt wird .

Das undefinierte Verhalten tritt auf, wenn es PrintToConsoletatsächlich zurückkehrt. Der Compiler erwartet, dass dies nicht geschieht (da dies dazu führen würde, dass das Programm undefiniertes Verhalten ausführt, egal was passiert), daher kann alles passieren.

Betrachten wir jedoch etwas anderes. Angenommen, wir führen eine Nullprüfung durch und verwenden die Variable nach der Nullprüfung.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

In diesem Fall ist leicht zu erkennen, dass lol_null_checkein Nicht-NULL-Zeiger erforderlich ist. Das Zuweisen zur globalen nichtflüchtigen warningVariablen kann das Programm nicht beenden oder Ausnahmen auslösen. Das pointerist auch nicht flüchtig, so dass es seinen Wert in der Mitte der Funktion nicht magisch ändern kann (wenn ja, ist es undefiniertes Verhalten). Das Aufrufen lol_null_check(NULL)führt zu einem undefinierten Verhalten, das dazu führen kann, dass die Variable nicht zugewiesen wird (da zu diesem Zeitpunkt bekannt ist, dass das Programm das undefinierte Verhalten ausführt).

Das undefinierte Verhalten bedeutet jedoch, dass das Programm alles tun kann. Daher hindert nichts das undefinierte Verhalten daran, in die Zeit zurückzukehren und Ihr Programm vor der ersten int main()Ausführungszeile zum Absturz zu bringen . Es ist undefiniertes Verhalten, es muss keinen Sinn ergeben. Es kann auch nach der Eingabe von 3 abstürzen, aber das undefinierte Verhalten wird in der Zeit zurückgehen und abstürzen, bevor Sie überhaupt 3 eingeben. Und wer weiß, vielleicht überschreibt undefiniertes Verhalten Ihren System-RAM und führt dazu, dass Ihr System 2 Wochen später abstürzt. während Ihr undefiniertes Programm nicht läuft.

Konrad Borowski
quelle
Alle gültigen Punkte. PrintToConsoleist mein Versuch, einen programmexternalen Nebeneffekt einzufügen, der auch nach Abstürzen sichtbar und stark sequenziert ist. Ich wollte eine Situation schaffen, in der wir mit Sicherheit feststellen können, ob diese Aussage optimiert wurde. Aber Sie haben Recht damit, dass es niemals zurückkehren könnte.; Ihr Beispiel für das Schreiben in ein globales Format kann anderen Optimierungen unterliegen, die nicht mit UB zusammenhängen. Beispielsweise kann eine nicht verwendete globale Datei gelöscht werden. Haben Sie eine Idee, einen externen Nebeneffekt so zu erzeugen, dass die Kontrolle garantiert wieder hergestellt wird?
usr
Können durch Code beobachtbare Nebenwirkungen außerhalb der Welt durch Code erzeugt werden, bei dem ein Compiler die Möglichkeit hätte, Rückgaben anzunehmen? Nach meinem Verständnis könnte sogar eine Methode, die einfach eine volatileVariable liest, eine E / A-Operation auslösen, die wiederum den aktuellen Thread sofort unterbrechen könnte. Der Interrupt-Handler könnte dann den Thread beenden, bevor er die Möglichkeit hat, etwas anderes auszuführen. Ich sehe keine Rechtfertigung dafür, dass der Compiler vor diesem Punkt undefiniertes Verhalten pushen könnte.
Supercat
Unter dem Gesichtspunkt des C-Standards wäre es nicht illegal, wenn undefiniertes Verhalten dazu führen würde, dass der Computer eine Nachricht an einige Personen sendet, die alle Beweise für die vorherigen Aktionen des Programms aufspüren und zerstören würden. Wenn eine Aktion jedoch einen Thread beenden könnte, dann Alles, was vor dieser Aktion sequenziert wurde, musste vor einem undefinierten Verhalten geschehen, das danach auftrat.
Supercat
1

Wenn das Programm eine Anweisung erreicht, die undefiniertes Verhalten aufruft, werden keinerlei Anforderungen an die Ausgabe / das Verhalten des Programms gestellt. Es spielt keine Rolle, ob sie "vor" oder "nach" stattfinden, wenn undefiniertes Verhalten aufgerufen wird.

Ihre Argumentation zu allen drei Codefragmenten ist richtig. Insbesondere kann ein Compiler jede Anweisung, die bedingungslos undefiniertes Verhalten aufruft, so behandeln, __builtin_unreachable()wie GCC es behandelt : als Optimierungshinweis, dass die Anweisung nicht erreichbar ist (und damit alle Codepfade, die bedingungslos zu ihr führen, auch nicht erreichbar sind). Andere ähnliche Optimierungen sind natürlich möglich.

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
1
Wann kam __builtin_unreachable()es aus Neugier zu Effekten, die zeitlich sowohl vorwärts als auch rückwärts gingen? Angesichts so etwas wie extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }ich das sehen konnte , builtin_unreachable()als gutes die Compiler wissen lassen es die weglassen returnAnweisung, aber das wäre etwas anders zu sagen , dass die vorhergehende Code weggelassen werden könnte.
Supercat
@supercat Da RESET_TRIGGER flüchtig ist, kann das Schreiben an diesen Speicherort beliebige Nebenwirkungen haben. Für den Compiler ist es wie ein undurchsichtiger Methodenaufruf. Daher kann nicht nachgewiesen werden (und ist dies nicht der Fall), dass dies __builtin_unreachableerreicht wird. Dieses Programm ist definiert.
usr
@usr: Ich würde denken, dass Compiler auf niedriger Ebene flüchtige Zugriffe als undurchsichtige Methodenaufrufe behandeln sollten, aber weder clang noch gcc tun dies. Unter anderem kann ein undurchsichtiger Methodenaufruf dazu führen, dass alle Bytes eines Objekts, dessen Adresse der Außenwelt ausgesetzt war und auf das von einem Live- restrictZeiger weder zugegriffen wurde noch wird, mit einem geschrieben werden unsigned char*.
Supercat
@usr: Wenn ein Compiler einen flüchtigen Zugriff in Bezug auf Zugriffe auf exponierte Objekte nicht als undurchsichtigen Methodenaufruf behandelt, sehe ich keinen besonderen Grund zu der Annahme, dass er dies für andere Zwecke tun würde. Der Standard verlangt dies nicht für Implementierungen, da es einige Hardwareplattformen gibt, auf denen ein Compiler möglicherweise alle möglichen Auswirkungen eines flüchtigen Zugriffs erkennen kann. Ein Compiler, der für die eingebettete Verwendung geeignet ist, sollte jedoch erkennen, dass flüchtige Zugriffe Hardware auslösen können, die beim Schreiben des Compilers nicht erfunden wurde.
Superkatze
@ Supercat Ich denke du hast recht. Es scheint, dass flüchtige Operationen "keine Auswirkung auf die abstrakte Maschine" haben und daher das Programm nicht beenden oder Nebenwirkungen verursachen können.
usr
1

Viele Standards für viele Arten von Dingen erfordern viel Aufwand bei der Beschreibung von Dingen, die Implementierungen SOLLTEN oder NICHT tun SOLLTEN, wobei eine Nomenklatur verwendet wird, die der in IETF RFC 2119 definierten ähnlich ist (obwohl nicht unbedingt die Definitionen in diesem Dokument zitiert werden). In vielen Fällen sind Beschreibungen von Dingen, die Implementierungen ausführen sollten, außer in Fällen, in denen sie nutzlos oder unpraktisch wären, wichtiger als die Anforderungen, denen alle konformen Implementierungen entsprechen müssen.

Leider neigen C- und C ++ - Standards dazu, Beschreibungen von Dingen zu vermeiden, die zwar nicht zu 100% erforderlich sind, aber dennoch von Qualitätsimplementierungen erwartet werden sollten, die kein gegenteiliges Verhalten dokumentieren. Ein Vorschlag, dass Implementierungen etwas tun sollten, könnte bedeuten, dass diejenigen, die nicht minderwertig sind, und in Fällen, in denen es im Allgemeinen offensichtlich ist, welche Verhaltensweisen bei einer bestimmten Implementierung nützlich oder praktisch oder unpraktisch und nutzlos sind, vorhanden sind wenig wahrgenommene Notwendigkeit für den Standard, solche Urteile zu stören.

Ein cleverer Compiler könnte dem Standard entsprechen und gleichzeitig jeden Code eliminieren, der keine Auswirkungen hätte, außer wenn Code Eingaben empfängt, die unweigerlich undefiniertes Verhalten verursachen würden, aber "clever" und "dumm" sind keine Antonyme. Die Tatsache, dass die Autoren des Standards entschieden haben, dass es einige Arten von Implementierungen geben könnte, bei denen ein nützliches Verhalten in einer bestimmten Situation nutzlos und unpraktisch wäre, impliziert keine Beurteilung, ob solche Verhaltensweisen für andere als praktisch und nützlich angesehen werden sollten. Wenn eine Implementierung eine Verhaltensgarantie ohne Kosten aufrechterhalten könnte, die über den Verlust einer Beschneidungsmöglichkeit für "tote Zweige" hinausgeht, würde fast jeder Wert, den Benutzercode aus dieser Garantie erhalten könnte, die Kosten für deren Bereitstellung übersteigen. Die Beseitigung von toten Ästen kann in Fällen in Ordnung sein, in denen dies nicht der Fall ist.Wenn der Benutzercode jedoch in einer bestimmten Situation fast jedes andere mögliche Verhalten als die Beseitigung von Totzweigen hätte handhaben können , müsste der Benutzercode zur Vermeidung von UB wahrscheinlich den von DBE erzielten Wert überschreiten.

Superkatze
quelle
Es ist ein guter Punkt, dass das Vermeiden von UB dem Benutzercode Kosten auferlegen kann.
usr
@usr: Es ist ein Punkt, den Modernisten völlig vermissen. Soll ich ein Beispiel hinzufügen? Wenn beispielsweise Code ausgewertet werden muss, x*y < zwenn x*ykein Überlauf auftritt, und wenn ein Überlauf auf willkürliche Weise 0 oder 1 ergibt, jedoch ohne Nebenwirkungen, gibt es auf den meisten Plattformen keinen Grund, warum das Erfüllen der zweiten und dritten Anforderung teurer sein sollte als Das Erfüllen des ersten, aber jede Art, den Ausdruck zu schreiben, um in allen Fällen ein standarddefiniertes Verhalten zu gewährleisten, würde in einigen Fällen erhebliche Kosten verursachen. Das Schreiben des Ausdrucks (int64_t)x*y < zkönnte die Berechnungskosten mehr als vervierfachen ...
Supercat
... auf einigen Plattformen und das Schreiben so, als (int)((unsigned)x*y) < zwürde ein Compiler verhindern, dass ansonsten nützliche algebraische Substitutionen verwendet werden (z. B. wenn er das weiß xund zgleich und positiv ist, könnte er den ursprünglichen Ausdruck vereinfachen y<0, aber die Version ohne Vorzeichen würde den Compiler zwingen, die Multiplikation durchzuführen). Wenn der Compiler garantieren kann, obwohl der Standard dies nicht vorschreibt, wird er die Anforderung "Ausbeute 0 oder 1 ohne Nebenwirkungen" einhalten. Benutzercode könnte dem Compiler Optimierungsmöglichkeiten bieten, die er sonst nicht erhalten könnte.
Supercat
Ja, es scheint, dass eine mildere Form von undefiniertem Verhalten hier hilfreich wäre. Der Programmierer könnte einen Modus aktivieren x*y, der im Falle eines Überlaufs einen normalen Wert ausgibt, aber überhaupt einen Wert. Konfigurierbares UB in C / C ++ scheint mir wichtig zu sein.
usr
@usr: Wenn die Autoren des C89-Standards ehrlich sagen würden, dass die Förderung von kurzen, nicht signierten Werten zur Anmeldung die schwerwiegendste bahnbrechende Änderung darstellt und keine unwissenden Dummköpfe sind, würde dies bedeuten, dass sie dies in Fällen erwartet haben, in denen Plattformen dies getan haben Nachdem sie nützliche Verhaltensgarantien definiert hatten, hatten Implementierungen für solche Plattformen diese Garantien den Programmierern zur Verfügung gestellt, und Programmierer hatten sie ausgenutzt. Compiler für solche Plattformen würden weiterhin solche Verhaltensgarantien anbieten, unabhängig davon, ob der Standard sie bestellt hatte oder nicht .
Supercat