Wann führt das Aufrufen einer Mitgliedsfunktion für eine Nullinstanz zu einem undefinierten Verhalten?

120

Betrachten Sie den folgenden Code:

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

Wir erwarten (b)einen Absturz, da es kein entsprechendes Mitglied xfür den Nullzeiger gibt. In der Praxis (a)stürzt nicht ab, da der thisZeiger nie verwendet wird.

Da (b)der thisZeiger ( (*this).x = 5;) dereferenziert und thisnull ist, gibt das Programm ein undefiniertes Verhalten ein, da das Dereferenzieren von null immer als undefiniertes Verhalten bezeichnet wird.

Führt (a)zu undefiniertem Verhalten? Was ist, wenn beide Funktionen (und x) statisch sind?

GManNickG
quelle
Wenn beide Funktionen statisch sind , wie könnte x in baz referenziert werden ? (x ist eine nicht statische Mitgliedsvariable)
legends2k
4
@ legends2k: Pretend xwurde auch statisch gemacht. :)
GManNickG
Sicher, aber für den Fall (a) funktioniert es in allen Fällen gleich, dh die Funktion wird aufgerufen. Wenn Sie jedoch den Wert des Zeigers von 0 auf 1 ersetzen (z. B. durch reinterpret_cast), stürzt er fast immer ab. Stellt die Wertzuweisung von 0 und damit NULL wie in Fall a etwas Besonderes für den Compiler dar? Warum stürzt es immer mit einem anderen ihm zugewiesenen Wert ab?
Siddharth Shankaran
5
Interessant: Bei der nächsten Überarbeitung von C ++ wird es überhaupt keine Dereferenzierung von Zeigern mehr geben. Wir werden jetzt die Indirektion durch Zeiger durchführen. Um mehr zu erfahren, führen Sie bitte eine Indirektion über diesen Link durch: N3362
James McNellis
3
Das Aufrufen einer Elementfunktion für einen Nullzeiger ist immer ein undefiniertes Verhalten. Wenn ich mir nur Ihren Code ansehe, kann ich bereits spüren, wie das undefinierte Verhalten langsam meinen Hals hochkrabbelt!
Fredoverflow

Antworten:

113

Beides (a)und (b)führen zu undefiniertem Verhalten. Es ist immer undefiniertes Verhalten, eine Mitgliedsfunktion über einen Nullzeiger aufzurufen. Wenn die Funktion statisch ist, ist sie auch technisch undefiniert, aber es gibt einige Streitigkeiten.


Das erste, was zu verstehen ist, ist, warum es undefiniertes Verhalten ist, einen Nullzeiger zu dereferenzieren. In C ++ 03 gibt es hier tatsächlich ein bisschen Mehrdeutigkeit.

Obwohl "das Dereferenzieren eines Nullzeigers zu undefiniertem Verhalten führt" in den Anmerkungen sowohl in §1.9 / 4 als auch in §8.3.2 / 4 erwähnt wird, wird dies niemals explizit angegeben. (Notizen sind nicht normativ.)

Man kann jedoch versuchen, es aus §3.10 / 2 abzuleiten:

Ein Wert bezieht sich auf ein Objekt oder eine Funktion.

Bei der Dereferenzierung ist das Ergebnis ein Wert. Ein Nullzeiger nicht auf ein Objekt. Wenn wir also den Wert l verwenden, haben wir ein undefiniertes Verhalten. Das Problem ist, dass der vorherige Satz nie angegeben wird. Was bedeutet es also, den Wert zu "verwenden"? Generieren Sie es einfach überhaupt oder verwenden Sie es im formaleren Sinne, um eine Wert-zu-Wert-Konvertierung durchzuführen?

Unabhängig davon kann es definitiv nicht in einen Wert umgewandelt werden (§4.1 / 1):

Wenn das Objekt, auf das sich der Wert bezieht, kein Objekt vom Typ T und kein Objekt von einem von T abgeleiteten Typ ist oder wenn das Objekt nicht initialisiert ist, hat ein Programm, das diese Konvertierung erfordert, ein undefiniertes Verhalten.

Hier ist es definitiv undefiniertes Verhalten.

Die Mehrdeutigkeit ergibt sich daraus, ob es sich um ein undefiniertes Verhalten zur Zurückhaltung handelt oder nicht, aber den Wert eines ungültigen Zeigers nicht verwendet (dh einen l-Wert erhalten, ihn aber nicht in einen r-Wert konvertieren). Wenn nicht, dann int *i = 0; *i; &(*i);ist genau definiert. Dies ist ein aktives Problem .

Wir haben also eine strikte Ansicht "Dereferenzieren eines Nullzeigers, Erhalten eines undefinierten Verhaltens" und eine schwache Ansicht "Verwenden eines dereferenzierten Nullzeigers, Erhalten eines undefinierten Verhaltens".

Nun betrachten wir die Frage.


Ja, (a)führt zu undefiniertem Verhalten. In der Tat, wenn thisdann null ist das Ergebnis unabhängig vom Inhalt der Funktion undefiniert.

Dies folgt aus §5.2.5 / 3:

Wenn E1der Typ "Zeiger auf Klasse X" ist, wird der Ausdruck E1->E2in die entsprechende Form konvertiert(*(E1)).E2;

*(E1) wird zu undefiniertem Verhalten mit einer strengen Interpretation führen, und .E2 konvertiert es in einen r-Wert, wodurch es für die schwache Interpretation zu undefiniertem Verhalten wird.

Daraus folgt auch, dass es sich um ein undefiniertes Verhalten direkt aus (§9.3.1 / 1) handelt:

Wenn eine nicht statische Elementfunktion einer Klasse X für ein Objekt aufgerufen wird, das nicht vom Typ X oder von X abgeleitet ist, ist das Verhalten undefiniert.


Bei statischen Funktionen macht die strikte gegenüber der schwachen Interpretation den Unterschied. Genau genommen ist es undefiniert:

Auf ein statisches Element kann unter Verwendung der Klassenmitgliedszugriffssyntax verwiesen werden. In diesem Fall wird der Objektausdruck ausgewertet.

Das heißt, es wird so ausgewertet, als wäre es nicht statisch, und wir dereferenzieren erneut einen Nullzeiger mit (*(E1)).E2.

Da dies E1jedoch nicht in einem statischen Elementfunktionsaufruf verwendet wird, ist der Aufruf gut definiert, wenn wir die schwache Interpretation verwenden. *(E1)ergibt sich ein lWert, die statische Funktion wird aufgelöst,*(E1) verworfen und die Funktion aufgerufen. Es gibt keine Konvertierung von lWert in rWert, daher gibt es kein undefiniertes Verhalten.

In C ++ 0x bleibt ab n3126 die Mehrdeutigkeit bestehen. Seien Sie vorerst sicher: Verwenden Sie die strenge Interpretation.

GManNickG
quelle
5
+1. Wenn Sie die Pedanterie fortsetzen, wurde unter der "schwachen Definition" die nicht statische Elementfunktion nicht "für ein Objekt vom Typ X" aufgerufen. Es wurde nach einem Wert gerufen, der überhaupt kein Objekt ist. Die vorgeschlagene Lösung fügt der von Ihnen zitierten Klausel den Text "oder wenn der Wert ein leerer Wert ist" hinzu.
Steve Jessop
Könnten Sie etwas klarstellen? Wie lauten insbesondere bei Ihren Links "geschlossene Ausgabe" und "aktive Ausgabe" die Ausgabenummern? Wenn dies ein geschlossenes Problem ist, wie lautet die Ja / Nein-Antwort für statische Funktionen? Ich habe das Gefühl, dass ich den letzten Schritt verpasse, um Ihre Antwort zu verstehen.
Brooks Moses
4
Ich denke nicht, dass der CWG-Defekt 315 so "geschlossen" ist, wie es das Vorhandensein auf der Seite "Geschlossene Probleme" impliziert. Das Grundprinzip besagt, dass es zulässig sein sollte, weil " *pes kein Fehler ist, wenn pes null ist, es sei denn, der l-Wert wird in einen r-Wert konvertiert." Dies beruht jedoch auf dem Konzept eines "leeren Werts", das Teil der vorgeschlagenen Lösung für den CWG-Defekt 232 ist , aber nicht angenommen wurde. Mit der Sprache in C ++ 03 und C ++ 0x ist die Dereferenzierung des Nullzeigers immer noch undefiniert, selbst wenn keine Konvertierung von Wert zu Wert erfolgt.
James McNellis
1
@JamesMcNellis: Wenn nach meinem Verständnis peine Hardwareadresse beim Lesen eine Aktion auslösen würde, aber nicht deklariert würde volatile, wäre die Anweisung *p;nicht erforderlich, aber erlaubt , diese Adresse tatsächlich zu lesen. Die Aussage &(*p);wäre jedoch verboten. Wenn *pwaren volatile, würde die Lese erforderlich. In beiden Fällen, wenn der Zeiger ungültig ist, kann ich nicht sehen, wie die erste Anweisung nicht undefiniertes Verhalten wäre, aber ich kann auch nicht sehen, warum die zweite Anweisung sein würde.
Supercat
1
".E2 wandelt es in einen Wert um" - Äh, nein, tut es nicht
MM
30

Offensichtlich bedeutet undefiniert, dass es nicht definiert ist , aber manchmal kann es vorhersehbar sein. Die Informationen, die ich bereitstellen werde, sollten für den Arbeitscode niemals verwendet werden, da sie sicherlich nicht garantiert sind, aber beim Debuggen nützlich sein können.

Sie könnten denken, dass das Aufrufen einer Funktion für einen Objektzeiger den Zeiger dereferenziert und UB verursacht. In der Praxis , wenn die Funktion nicht virtuell ist, hat sich der Compiler es zu einem einfachen Funktionsaufruf übergeben den Zeiger als ersten Parameter konvertiert diese , die dereferenzieren Umgehung und eine Zeitbombe für die gerufene Memberfunktion zu schaffen. Wenn die Member-Funktion keine Member-Variablen oder virtuellen Funktionen referenziert, ist sie möglicherweise tatsächlich fehlerfrei erfolgreich. Denken Sie daran, dass Erfolg in das Universum von "undefiniert" fällt!

Die MFC-Funktion GetSafeHwnd von Microsoft basiert tatsächlich auf diesem Verhalten. Ich weiß nicht, was sie geraucht haben.

Wenn Sie eine virtuelle Funktion aufrufen, muss der Zeiger dereferenziert werden, um zur vtable zu gelangen, und Sie werden mit Sicherheit UB erhalten (wahrscheinlich ein Absturz, aber denken Sie daran, dass es keine Garantien gibt).

Mark Ransom
quelle
1
GetSafeHwnd führt zuerst eine! Diese Prüfung durch und gibt, wenn true, NULL zurück. Dann beginnt ein SEH-Frame und dereferenziert den Zeiger. Wenn eine Speicherzugriffsverletzung (0xc0000005) vorliegt, wird diese abgefangen und NULL wird an den Anrufer zurückgegeben :) Andernfalls wird der HWND zurückgegeben.
8етър Петров
@ ПетърПетров Es ist schon einige Jahre her, seit ich mir den Code angesehen habe GetSafeHwnd. Es ist möglich, dass sie ihn seitdem verbessert haben. Und vergessen Sie nicht, dass sie Insiderwissen über die Funktionsweise des Compilers haben!
Mark Ransom
Ich sage ein Beispiel für eine mögliche Implementierung, die den gleichen Effekt hat. Was es wirklich tut, ist, mit einem Debugger rückentwickelt zu werden :)
Петър Петров
1
"Sie haben Insiderwissen über die Funktionsweise des Compilers!" - die Ursache für ewige Probleme bei Projekten wie MinGW, die versuchen, g ++ das Kompilieren von Code zu ermöglichen, der die Windows-API aufruft
MM
@ MM Ich denke, wir sind uns alle einig, dass dies unfair ist. Aus diesem Grund denke ich auch, dass es ein Kompatibilitätsgesetz gibt, das es ein bisschen illegal macht, es so zu halten.
v.oddou