GCC 6 verfügt über eine neue Optimierungsfunktion : Es wird davon ausgegangen, dass diese this
immer nicht null ist, und darauf basierend optimiert.
Die Weitergabe des Wertebereichs setzt nun voraus, dass der Zeiger this der C ++ - Mitgliedsfunktionen nicht null ist. Dies eliminiert häufige Nullzeigerprüfungen, unterbricht jedoch auch einige nicht konforme Codebasen (wie Qt-5, Chromium, KDevelop) . Als vorübergehende Problemumgehung können -fno-delete-null-Zeigerprüfungen verwendet werden. Falscher Code kann mit -fsanitize = undefined identifiziert werden.
Das Änderungsdokument weist dies eindeutig als gefährlich aus, da es eine überraschende Menge häufig verwendeten Codes zerstört.
Warum sollte diese neue Annahme den praktischen C ++ - Code brechen? Gibt es bestimmte Muster, bei denen unachtsame oder nicht informierte Programmierer auf dieses bestimmte undefinierte Verhalten angewiesen sind? Ich kann mir nicht vorstellen, if (this == NULL)
dass jemand schreibt, weil das so unnatürlich ist.
quelle
this
es als impliziter Parameter übergeben wird, und sie beginnen dann, es so zu verwenden, als wäre es ein expliziter Parameter. Es ist nicht. Wenn Sie eine Null dereferenzieren, rufen Sie UB so auf, als hätten Sie einen anderen Nullzeiger dereferenziert. Das ist alles was dazu gehört. Wenn Sie nullptrs weitergeben möchten, verwenden Sie den expliziten Parameter DUH . Es wird nicht langsamer, es wird nicht klobiger sein, und der Code, der eine solche API hat, ist sowieso tief in den Interna enthalten, hat also einen sehr begrenzten Umfang. Ende der Geschichte denke ich.Antworten:
Ich denke, die Frage, die beantwortet werden muss, warum gut gemeinte Leute die Schecks überhaupt ausstellen würden.
Der häufigste Fall ist wahrscheinlich, wenn Sie eine Klasse haben, die Teil eines natürlich vorkommenden rekursiven Aufrufs ist.
Wenn du hättest:
in C könnten Sie schreiben:
In C ++ ist es schön, dies zu einer Mitgliedsfunktion zu machen:
In den frühen Tagen von C ++ (vor der Standardisierung) wurde betont, dass Mitgliedsfunktionen syntaktischer Zucker für eine Funktion sind, bei der der
this
Parameter implizit ist. Code wurde in C ++ geschrieben, in äquivalentes C konvertiert und kompiliert. Es gab sogar explizite Beispiele dafür, dass der Vergleichthis
mit null sinnvoll war, und der ursprüngliche Cfront-Compiler nutzte dies ebenfalls aus. Ausgehend von einem C-Hintergrund ist die offensichtliche Wahl für die Prüfung:Hinweis: Bjarne Stroustrup erwähnt sogar, dass sich die Regeln für
this
hier im Laufe der Jahre geändert habenUnd das hat viele Jahre bei vielen Compilern funktioniert. Als die Standardisierung stattfand, änderte sich dies. In jüngerer Zeit nutzten Compiler den Vorteil, eine Member-Funktion aufzurufen, bei der
this
es sichnullptr
um ein undefiniertes Verhalten handelt. Dies bedeutet, dass diese Bedingung immerfalse
besteht und der Compiler sie weglassen kann.Das bedeutet, dass Sie zum Durchlaufen dieses Baums entweder:
Führen Sie alle Überprüfungen durch, bevor Sie anrufen
traverse_in_order
Dies bedeutet auch, an JEDER Anrufstelle zu prüfen, ob Sie einen Nullstamm haben könnten.
Verwenden Sie keine Mitgliedsfunktion
Dies bedeutet, dass Sie den alten Code im C-Stil (möglicherweise als statische Methode) schreiben und ihn mit dem Objekt explizit als Parameter aufrufen. z.B. Sie schreiben wieder
Node::traverse_in_order(node);
und nichtnode->traverse_in_order();
an der Anrufstelle.Ich glaube, der einfachste / sauberste Weg, dieses spezielle Beispiel auf eine Weise zu reparieren, die Standards entspricht, besteht darin, tatsächlich einen Sentinel-Knoten anstelle eines zu verwenden
nullptr
.Keine der ersten beiden Optionen scheint so ansprechend zu sein, und obwohl Code damit durchkommen könnte, haben sie schlechten Code mit geschrieben,
this == nullptr
anstatt eine richtige Korrektur zu verwenden.Ich vermute, so haben sich einige dieser Codebasen entwickelt, um sie zu
this == nullptr
überprüfen.quelle
1 == 0
undefiniertes Verhalten sein? Es ist einfachfalse
.this == nullptr
idiom ist ein undefiniertes Verhalten, da Sie zuvor eine Member-Funktion für ein nullptr-Objekt aufgerufen haben, die undefiniert ist. Und der Compiler kann die Prüfung weglassenthis
aufrufe " verwendet, ohne überhaupt daran zu denken, jemals nullbar zu sein. Ich denke, vielleicht ist dies der Vorteil des Lernens von C ++ in einer Zeit, in der SO existiert, um die Gefahren von UB in meinem Gehirn zu verankern und mich davon abzubringen, bizarre Hacks wie diesen zu machen.Dies liegt daran, dass der "praktische" Code fehlerhaft war und zunächst undefiniertes Verhalten beinhaltete. Es gibt keinen Grund, eine Null zu verwenden
this
, außer als Mikrooptimierung, normalerweise eine sehr verfrühte.Dies ist eine gefährliche Vorgehensweise, da die Anpassung von Zeigern aufgrund des Durchlaufs der Klassenhierarchie eine Null
this
in eine Nicht-Null-Null verwandeln kann . Zumindest muss die Klasse, deren Methoden mit einer Null arbeitenthis
sollen, eine endgültige Klasse ohne Basisklasse sein: Sie kann von nichts abgeleitet werden und sie kann nicht von abgeleitet werden. Wir gehen schnell vom praktischen zum hässlichen Hack-Land über .In der Praxis muss der Code nicht hässlich sein:
Wenn der Baum leer ist, auch bekannt als Null
Node* root
, sollten Sie keine nicht statischen Methoden dafür aufrufen. Zeitraum. Es ist vollkommen in Ordnung, C-ähnlichen Baumcode zu haben, der einen Instanzzeiger durch einen expliziten Parameter nimmt.Das Argument hier scheint darauf hinauszulaufen, dass nicht statische Methoden für Objekte geschrieben werden müssen, die von einem Nullinstanzzeiger aufgerufen werden könnten. Es gibt keine solche Notwendigkeit. Die Art und Weise, wie C-with-Objects solchen Code schreiben, ist in der C ++ - Welt noch viel besser, da sie zumindest typsicher sein kann. Grundsätzlich ist die Null
this
eine solche Mikrooptimierung mit einem so engen Anwendungsbereich, dass es meiner Meinung nach vollkommen in Ordnung ist, sie nicht zuzulassen. Keine öffentliche API sollte von einer Null abhängenthis
.quelle
this
Überprüfungen werden von verschiedenen statischen Code-Analysatoren ausgewählt, sodass es nicht so ist, als müsste jemand sie alle manuell suchen. Der Patch würde wahrscheinlich ein paar hundert Zeilen mit trivialen Änderungen enthalten.this
Dereferenzierung ein sofortiger Absturz. Diese Probleme werden sehr schnell erkannt, selbst wenn niemand einen statischen Analysator über den Code ausführen möchte. C / C ++ folgt dem Mantra "Nur für Funktionen bezahlen, die Sie verwenden". Wenn Sie Überprüfungen wünschen, müssen Sie diese explizit angeben. Dies bedeutet, dass Sie sie nicht ausführen müssenthis
, wenn es zu spät ist, da der Compiler davon ausgeht, dass siethis
nicht null sind. Andernfalls müsste es überprüft werdenthis
, und für 99,9999% des Codes da draußen sind solche Überprüfungen Zeitverschwendung.Das Dokument nennt es nicht gefährlich. Es wird auch nicht behauptet, dass es eine überraschende Menge an Code zerstört . Es werden lediglich einige beliebte Codebasen aufgezeigt, von denen behauptet wird, dass sie auf diesem undefinierten Verhalten beruhen und aufgrund der Änderung nicht funktionieren würden, wenn nicht die Problemumgehungsoption verwendet wird.
Wenn praktischer C ++ - Code auf undefiniertem Verhalten beruht, können Änderungen an diesem undefinierten Verhalten das Verhalten beeinträchtigen. Aus diesem Grund ist UB zu vermeiden, auch wenn ein darauf basierendes Programm wie beabsichtigt zu funktionieren scheint.
Ich weiß nicht, ob es ein weit verbreitetes Anti- Muster ist, aber ein nicht informierter Programmierer könnte denken, dass er sein Programm vor Abstürzen schützen kann, indem er Folgendes tut:
Wenn der eigentliche Fehler darin besteht, einen Nullzeiger an einer anderen Stelle zu dereferenzieren.
Ich bin sicher, wenn der Programmierer nicht ausreichend informiert ist, kann er fortgeschrittenere (Anti) Muster entwickeln, die auf dieser UB basieren.
Ich kann.
quelle
if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere();
Zum Beispiel ein schönes, einfach zu lesendes Protokoll einer Abfolge von Ereignissen, von denen ein Debugger Ihnen nicht leicht erzählen kann. Viel Spaß beim Debuggen, ohne stundenlang überall Schecks zu platzieren, wenn in einem großen Datensatz plötzlich eine zufällige Null in Code vorliegt, den Sie nicht geschrieben haben ... Und die UB-Regel dazu wurde später erstellt, nachdem C ++ erstellt wurde. Es war früher gültig.-fsanitize=null
ist.-fsanitize=null
die Fehler mithilfe von SPI auf der SD / MMC-Karte an den Pins 5, 6, 10, 11 protokolliert werden? Das ist keine universelle Lösung. Einige haben argumentiert, dass es gegen objektorientierte Prinzipien verstößt, auf ein Nullobjekt zuzugreifen, aber einige OOP-Sprachen haben ein Nullobjekt, das bearbeitet werden kann, so dass es keine universelle Regel von OOP ist. 1/2Einige der "praktischen" (lustige Art, "Buggy" zu buchstabieren) Codes, die kaputt waren, sahen folgendermaßen aus:
und es wurde vergessen, die Tatsache zu berücksichtigen, dass
p->bar()
manchmal ein Nullzeiger zurückgegeben wird, was bedeutet, dass die Dereferenzierung zum Aufrufbaz()
undefiniert ist.Nicht der gesamte fehlerhafte Code enthielt explizite
if (this == nullptr)
oderif (!p) return;
Überprüfungen. Einige Fälle waren einfach Funktionen, die nicht auf Mitgliedsvariablen zugegriffen haben und daher in Ordnung zu sein schienen . Beispielsweise:In diesem Code gibt es beim Aufrufen
func<DummyImpl*>(DummyImpl*)
mit einem Nullzeiger eine "konzeptionelle" Dereferenzierung des aufzurufenden Zeigersp->DummyImpl::valid()
, aber tatsächlich kehrt diefalse
Elementfunktion nur ohne Zugriff zurück*this
. Dasreturn false
kann inline sein und so muss in der Praxis überhaupt nicht auf den Zeiger zugegriffen werden. Bei einigen Compilern scheint es also in Ordnung zu sein: Es gibt keinen Segfault für die Dereferenzierung von Null,p->valid()
ist falsch, daher ruft der Code aufdo_something_else(p)
, der nach Nullzeigern sucht, und tut nichts. Es wird kein Absturz oder unerwartetes Verhalten beobachtet.Mit GCC 6 erhalten Sie immer noch den Aufruf von
p->valid()
, aber der Compiler leitet jetzt aus diesem Ausdruck ab,p
der nicht null sein darf (andernfallsp->valid()
wäre dies ein undefiniertes Verhalten), und notiert diese Informationen. Diese abgeleiteten Informationen werden vom Optimierer verwendet, sodassdo_something_else(p)
dieif (p)
Prüfung jetzt als redundant betrachtet wird , wenn der Aufruf von inline ausgeführt wird , da der Compiler sich daran erinnert, dass sie nicht null ist, und den Code daher wie folgt einfügt :Dies dereferenziert jetzt wirklich einen Nullzeiger, und so funktioniert Code, der zuvor zu funktionieren schien, nicht mehr.
In diesem Beispiel befindet sich der Fehler
func
, der zuerst auf null hätte prüfen sollen (oder die Anrufer hätten ihn niemals mit null aufrufen sollen):Ein wichtiger Punkt, an den Sie sich erinnern sollten, ist, dass die meisten Optimierungen wie diese nicht vom Compiler stammen, der sagt: "Ah, der Programmierer hat diesen Zeiger gegen Null getestet, ich werde ihn entfernen, nur um ärgerlich zu sein." Was passiert, ist, dass verschiedene Standardoptimierungen wie Inlining und Wertebereichsausbreitung zusammen diese Überprüfungen überflüssig machen, da sie nach einer früheren Überprüfung oder einer Dereferenzierung erfolgen. Wenn der Compiler weiß, dass ein Zeiger an Punkt A in einer Funktion nicht null ist und der Zeiger nicht vor einem späteren Punkt B in derselben Funktion geändert wird, weiß er, dass er auch an B nicht null ist. Wenn Inlining auftritt Die Punkte A und B können tatsächlich Codeteile sein, die sich ursprünglich in separaten Funktionen befanden, jetzt aber zu einem Codeteil zusammengefasst sind, und der Compiler kann sein Wissen anwenden, dass der Zeiger an mehreren Stellen nicht null ist.
quelle
this
?this
zu null] " ?this
, dann verwenden Sie einfach-fsanitize=undefined
Der C ++ - Standard ist in wichtigen Punkten gebrochen. Anstatt die Benutzer vor diesen Problemen zu schützen, haben sich die GCC-Entwickler leider dafür entschieden, undefiniertes Verhalten als Ausrede für die Implementierung geringfügiger Optimierungen zu verwenden, selbst wenn ihnen klar erklärt wurde, wie schädlich es ist.
Hier eine viel klügere Person, als ich ausführlich erläutere. (Er spricht über C, aber dort ist die Situation dieselbe).
Warum ist es schädlich?
Durch einfaches Neukompilieren von zuvor funktionierendem, sicherem Code mit einer neueren Version des Compilers können Sicherheitslücken entstehen . Während das neue Verhalten mit einem Flag deaktiviert werden kann, ist bei vorhandenen Makefiles dieses Flag offensichtlich nicht gesetzt. Und da keine Warnung ausgegeben wird, ist es für den Entwickler nicht offensichtlich, dass sich das zuvor vernünftige Verhalten geändert hat.
In diesem Beispiel hat der Entwickler eine Überprüfung auf Ganzzahlüberlauf mit eingefügt
assert
, die das Programm beendet, wenn eine ungültige Länge angegeben wird. Das GCC-Team hat die Prüfung auf der Grundlage entfernt, dass der Ganzzahlüberlauf undefiniert ist. Daher kann die Prüfung entfernt werden. Dies führte dazu, dass echte In-the-Wild-Instanzen dieser Codebasis nach Behebung des Problems wieder anfällig gemacht wurden.Lesen Sie das Ganze. Es ist genug, um dich zum Weinen zu bringen.
OK, aber was ist mit diesem?
Vor langer Zeit gab es eine ziemlich verbreitete Redewendung, die ungefähr so lautete:
Die Redewendung lautet also: Wenn
pObj
nicht null, verwenden Sie das darin enthaltene Handle, andernfalls verwenden Sie ein Standardhandle. Dies ist in derGetHandle
Funktion gekapselt .Der Trick besteht darin, dass beim Aufrufen einer nicht virtuellen Funktion der
this
Zeiger nicht verwendet wird, sodass keine Zugriffsverletzung vorliegt.Ich verstehe es immer noch nicht
Es gibt viel Code, der so geschrieben ist. Wenn jemand es einfach neu kompiliert, ohne eine Zeile zu ändern, ist jeder Anruf bei
DoThing(NULL)
ein Absturzfehler - wenn Sie Glück haben.Wenn Sie kein Glück haben, werden Aufrufe von abstürzenden Fehlern zu Sicherheitslücken bei der Remote-Ausführung.
Dies kann sogar automatisch erfolgen. Sie haben ein automatisiertes Build-System, oder? Ein Upgrade auf den neuesten Compiler ist harmlos, oder? Aber jetzt ist es nicht - nicht, wenn Ihr Compiler GCC ist.
OK, sag es ihnen!
Ihnen wurde gesagt. Sie tun dies in voller Kenntnis der Konsequenzen.
aber wieso?
Wer kann das schon sagen? Vielleicht:
Oder vielleicht etwas anderes. Wer kann das schon sagen?
quelle