2014-09-08 8 views
11

Der folgende Code (oder dessen Äquivalent, die explizite Abgüsse von null wörtlichen loszuwerden temporäre Variable verwendet) wird häufig verwendet, den Offset eines bestimmten Mitglieds Variable innerhalb einer Klasse oder Struktur zu berechnen:Führt die Übernahme der Adresse der Membervariablen über einen Nullzeiger zu undefiniertem Verhalten?

class Class { 
public: 
    int first; 
    int second; 
}; 

Class* ptr = 0; 
size_t offset = reinterpret_cast<char*>(&ptr->second) - 
       reinterpret_cast<char*>(ptr); 

&ptr->second Aussehen wie es entspricht der folgenden ist:

&(ptr->second) 

wiederum die zu

entspricht
&((*ptr).second) 

, die einen Objektinstanzzeiger dereferenziert und ein undefiniertes Verhalten für Nullzeiger ergibt.

So ist die ursprüngliche Strafe oder gibt es UB?

+0

"offset" wie in 'offsetof' sollte eine' size_t' sein, wie Sie haben, aber der Unterschied von zwei Zeigern sollte 'ptrdiff_t' sein, also etwas falsch hier –

+4

Sie versuchen, ['offsetof'] (http: //de.cppreference.com/w/cpp/types/offsetof)? Betrachtet man die "mögliche Implementierung", ist die Antwort auf Ihre Frage ** nein **, es ist nicht UB (falls die Klasse ein Standardlayouttyp ist; auch auf der verlinkten Seite erwähnt). – leemes

+0

@leemes cppreference ist nicht der Standard. – Yakk

Antwort

9

Trotz der Tatsache, dass es tut nichts, char* foo = 0; *foo; ist undefiniertes Verhalten sein könnte.

Dereferenzierung eines Null-Pointer istkönnte undefined Verhalten sein. Und ja, ptr->foo entspricht (*ptr).foo, und *ptr dereferenziert einen Nullzeiger.

Es gibt zur Zeit ein open issue in den Arbeitsgruppen, wenn *(char*)0 undefiniertes Verhalten ist, wenn Sie nicht lesen oder schreiben. Teile des Standards implizieren, dass es ist, andere Teile implizieren, dass es nicht ist. Die aktuellen Noten scheinen sich darauf zu konzentrieren, sie zu definieren.

Jetzt ist dies in der Theorie. Wie wäre es in der Praxis? Das funktioniert unter den meisten Compilern, weil zum Dereferenzierungszeitpunkt keine Überprüfung durchgeführt wird: Speicher um den herum der Nullzeigerpunkt gegen Zugriff geschützt ist, und der obige Ausdruck nimmt einfach eine Adresse von etwas um Null herum, liest oder schreibt nicht der Wert dort.

Deshalb listet cpp reference offsetof so ziemlich diesen Trick als mögliche Implementierung auf. Die Tatsache, dass einige (viele? Die meisten? Alle, die ich überprüft habe?) Compiler implementieren offsetof in einer ähnlichen oder gleichwertigen Weise bedeutet nicht, dass das Verhalten unter dem C++ - Standard gut definiert ist. Aufgrund der Mehrdeutigkeit können Compiler jedoch bei jedem Befehl, der einen Zeiger dereferenziert, Prüfroutinen hinzufügen und beliebigen Code ausführen (z. B. schnelle Fehlerberichte), wenn Null tatsächlich dereferenziert wird. Eine solche Instrumentierung kann sogar nützlich sein, um Fehler dort zu finden, wo sie auftreten, und nicht dort, wo das Symptom auftritt. Und auf Systemen mit beschreibbarem Speicher in der Nähe von 0 könnte eine solche Instrumentierung der Schlüssel sein (vor OSX hatte MacOS einen beschreibbaren Speicher, der Systemfunktionen in der Nähe von 0 steuerte).

Solche Compiler könnten immer noch offsetof auf diese Weise schreiben und pragma s oder dergleichen einführen, um die Instrumentierung im generierten Code zu blockieren. Oder sie könnten zu einem intrinsischen wechseln.

Noch einen Schritt weiter, C++ lässt viel Spielraum für die Anordnung von Nicht-Standard-Layout-Daten. Theoretisch könnten Klassen als ziemlich komplexe Datenstrukturen implementiert werden, und nicht die fast Standard-Layout-Strukturen, die wir zu erwarten pflegen, und der Code wäre immer noch gültig C++.Es könnte problematisch sein, Member-Variablen auf Nicht-Standard-Layout-Typen zuzugreifen und deren Adresse zu übernehmen: Ich weiß nicht, ob es eine Garantie gibt, dass der Offset einer Member-Variable in einem nicht standardmäßigen Layout-Typ sich nicht zwischen Instanzen ändert!

Schließlich haben einige Compiler aggressive Optimierungseinstellungen, die Code finden, der undefiniertes Verhalten ausführt (zumindest unter bestimmten Zweigen oder Bedingungen), und verwendet, um diesen Zweig als nicht erreichbar zu markieren. Wenn entschieden wird, dass die Null-Dereferenzierung ein undefiniertes Verhalten ist, könnte dies ein Problem sein. Ein klassisches Beispiel ist der aggressive vorzeichenbehaftete Ganzzahlüberlauf-Verzweigungseliminator von gcc. Wenn der Standard diktiert, dass etwas nicht definiert ist, kann der Compiler diese Verzweigung als nicht erreichbar betrachten. Wenn sich die Null-Dereferenzierung nicht hinter einer Verzweigung in einer Funktion befindet, kann der Compiler den gesamten Code, der diese Funktion aufruft, als nicht erreichbar deklarieren und rekursiv ausführen.

Und es wäre frei, dies nicht in der aktuellen, sondern in der nächsten Version Ihres Compilers zu tun.

Das Schreiben von Code, der Standard-gültig ist, ist nicht nur das Schreiben von Code, der heute sauber kompiliert. Während der Grad, in dem Dereferenzierung und nicht die Verwendung eines Null-Zeigers definiert ist, derzeit mehrdeutig ist, ist es riskant, sich auf etwas zu verlassen, das nur zweideutig definiert ist.

Verwandte Themen