Ist dieses Stück Code gültig (und definiertes Verhalten)?
int &nullReference = *(int*)0;
Sowohl g++ als auch clang++ kompilieren es ohne Vorwarnung, selbst wenn es verwendet wird -Wall
, -Wextra
, -std=c++98
, -pedantic
, -Weffc++
…
Natürlich ist die Referenz nicht wirklich null, da nicht auf sie zugegriffen werden kann (es würde bedeuten, einen Nullzeiger zu dereferenzieren), aber wir könnten überprüfen, ob sie null ist oder nicht, indem wir ihre Adresse überprüfen:
if( & nullReference == 0 ) // null reference
Verweise sind keine Zeiger.
8.3.2/1:
Eine Referenz muss initialisiert werden, um auf ein gültiges Objekt oder eine gültige Funktion zu verweisen.
[Note: in particular, a null reference
cannot exist in a well-defined
program, because the only way to
create such a reference would be to
bind it to the “object” obtained by
dereferencing a null pointer, which
causes undefined behavior. As
described in 9.6, a reference cannot
be bound directly to a bit-field. ]
1.9/4:
Bestimmte andere Operationen werden in dieser Internationalen Norm als undefiniert beschrieben (z. B. die Auswirkung der Dereferenzierung des Nullzeigers).
Wie Johannes in einer gelöschten Antwort sagt, gibt es einige Zweifel, ob das “Dereferenzieren eines Nullzeigers” kategorisch als undefiniertes Verhalten bezeichnet werden sollte. Dies ist jedoch keiner der Fälle, die Zweifel hervorrufen, da ein Nullzeiger sicherlich nicht auf ein “gültiges Objekt oder eine gültige Funktion” zeigt und im Normenausschuss kein Wunsch besteht, Nullreferenzen einzuführen.
Die Antwort hängt von Ihrem Standpunkt ab:
Wenn Sie nach dem C++-Standard urteilen, können Sie keine Nullreferenz erhalten, da Sie zuerst undefiniertes Verhalten erhalten. Nach diesem ersten Auftreten von undefiniertem Verhalten lässt der Standard alles zu. Also wenn du schreibst *(int*)0
, haben Sie bereits ein undefiniertes Verhalten, da Sie aus Sicht des Sprachstandards einen Nullzeiger dereferenzieren. Der Rest des Programms ist irrelevant, sobald dieser Ausdruck ausgeführt wird, sind Sie aus dem Spiel.
In der Praxis können Nullreferenzen jedoch leicht aus Nullzeigern erstellt werden, und Sie werden es erst bemerken, wenn Sie tatsächlich versuchen, auf den Wert hinter der Nullreferenz zuzugreifen. Ihr Beispiel ist möglicherweise etwas zu einfach, da jeder gute Optimierungscompiler das undefinierte Verhalten sieht und einfach alles wegoptimiert, was davon abhängt (die Nullreferenz wird nicht einmal erstellt, sondern wegoptimiert).
Diese Optimierung hängt jedoch vom Compiler ab, um das undefinierte Verhalten zu beweisen, was möglicherweise nicht möglich ist. Betrachten Sie diese einfache Funktion in einer Datei converter.cpp
:
int& toReference(int* pointer) {
return *pointer;
}
Wenn der Compiler diese Funktion sieht, weiß er nicht, ob der Zeiger ein Nullzeiger ist oder nicht. Es generiert also nur Code, der jeden Zeiger in die entsprechende Referenz umwandelt. (Übrigens: Dies ist ein Noop, da Zeiger und Referenzen in Assembler genau das gleiche Biest sind.) Nun, wenn Sie eine andere Datei haben user.cpp
mit dem Code
#include "converter.h"
void foo() {
int& nullRef = toReference(nullptr);
cout << nullRef; //crash happens here
}
der Compiler weiß das nicht toReference()
wird den übergebenen Zeiger dereferenzieren und davon ausgehen, dass er eine gültige Referenz zurückgibt, die in der Praxis eine Nullreferenz sein wird. Der Aufruf ist erfolgreich, aber wenn Sie versuchen, die Referenz zu verwenden, stürzt das Programm ab. Hoffentlich. Der Standard lässt alles zu, einschließlich des Erscheinens von rosa Elefanten.
Sie fragen sich vielleicht, warum das relevant ist, schließlich wurde das undefinierte Verhalten bereits im Inneren ausgelöst toReference()
. Die Antwort ist das Debuggen: Nullreferenzen können sich ausbreiten und vermehren, genau wie Nullzeiger. Wenn Sie sich nicht bewusst sind, dass Nullreferenzen existieren können, und lernen, sie zu vermeiden, verbringen Sie möglicherweise einige Zeit damit, herauszufinden, warum Ihre Member-Funktion abzustürzen scheint, wenn sie nur versucht, eine einfache alte zu lesen int
Mitglied (Antwort: Die Instanz im Aufruf des Mitglieds war eine Nullreferenz, also this
ist ein Nullzeiger, und Ihr Mitglied wird als Adresse berechnet 8).
Wie wäre es also mit der Suche nach Nullreferenzen? Du hast die Linie gegeben
if( & nullReference == 0 ) // null reference
in deiner frage. Nun, das wird nicht funktionieren: Gemäß dem Standard haben Sie ein undefiniertes Verhalten, wenn Sie einen Nullzeiger dereferenzieren, und Sie können keine Nullreferenz erstellen, ohne einen Nullzeiger zu dereferenzieren, sodass Nullreferenzen nur innerhalb des Bereichs des undefinierten Verhaltens existieren. Da Ihr Compiler davon ausgehen kann, dass Sie kein undefiniertes Verhalten auslösen, kann er davon ausgehen, dass es keine Nullreferenz gibt (obwohl es leicht Code ausgeben wird, der Nullreferenzen generiert!). Als solches sieht es die if()
Bedingung, kommt zu dem Schluss, dass es nicht wahr sein kann, und wirft das Ganze einfach weg if()
Erklärung. Mit der Einführung von Verbindungszeitoptimierungen ist es einfach unmöglich geworden, robust auf Nullreferenzen zu prüfen.
TL;DR:
Null-Referenzen führen ein ziemlich gruseliges Dasein:
Ihre Existenz scheint unmöglich (= nach dem Standard),
aber sie existieren (= durch den generierten Maschinencode),
aber Sie können sie nicht sehen, wenn sie existieren (= Ihre Versuche werden wegoptimiert),
aber sie können Sie trotzdem unbemerkt töten (= Ihr Programm stürzt an seltsamen Stellen ab oder schlimmer).
Ihre einzige Hoffnung ist, dass sie nicht existieren (= schreiben Sie Ihr Programm, um sie nicht zu erstellen).
Ich hoffe, das wird Sie nicht heimsuchen!
clang++ 3.5 warnt sogar davor:
/tmp/a.C:3:7: warning: reference cannot be bound to dereferenced null pointer in well-defined C++ code; comparison may be assumed to
always evaluate to false [-Wtautological-undefined-compare]
if( & nullReference == 0 ) // null reference
^~~~~~~~~~~~~ ~
1 warning generated.
Wenn Sie einen Weg finden wollten, null in einer Aufzählung von Singleton-Objekten darzustellen, dann ist es eine schlechte Idee, null (in C++11, nullptr) zu (de)referenzieren.
Warum deklarieren Sie nicht wie folgt ein statisches Singleton-Objekt, das NULL innerhalb der Klasse darstellt, und fügen Sie einen Cast-to-Pointer-Operator hinzu, der nullptr zurückgibt?
Bearbeiten: Mehrere Tippfehler korrigiert und if-Anweisung in main() hinzugefügt, um zu testen, ob der Cast-to-Pointer-Operator tatsächlich funktioniert (was ich vergessen habe … mein Fehler) – 10. März 2015 –
// Error.h
class Error {
public:
static Error& NOT_FOUND;
static Error& UNKNOWN;
static Error& NONE; // singleton object that represents null
public:
static vector<shared_ptr<Error>> _instances;
static Error& NewInstance(const string& name, bool isNull = false);
private:
bool _isNull;
Error(const string& name, bool isNull = false) : _name(name), _isNull(isNull) {};
Error() {};
Error(const Error& src) {};
Error& operator=(const Error& src) {};
public:
operator Error*() { return _isNull ? nullptr : this; }
};
// Error.cpp
vector<shared_ptr<Error>> Error::_instances;
Error& Error::NewInstance(const string& name, bool isNull = false)
{
shared_ptr<Error> pNewInst(new Error(name, isNull)).
Error::_instances.push_back(pNewInst);
return *pNewInst.get();
}
Error& Error::NOT_FOUND = Error::NewInstance("NOT_FOUND");
//Error& Error::NOT_FOUND = Error::NewInstance("UNKNOWN"); Edit: fixed
//Error& Error::NOT_FOUND = Error::NewInstance("NONE", true); Edit: fixed
Error& Error::UNKNOWN = Error::NewInstance("UNKNOWN");
Error& Error::NONE = Error::NewInstance("NONE");
// Main.cpp
#include "Error.h"
Error& getError() {
return Error::UNKNOWN;
}
// Edit: To see the overload of "Error*()" in Error.h actually working
Error& getErrorNone() {
return Error::NONE;
}
int main(void) {
if(getError() != Error::NONE) {
return EXIT_FAILURE;
}
// Edit: To see the overload of "Error*()" in Error.h actually working
if(getErrorNone() != nullptr) {
return EXIT_FAILURE;
}
}
Können Sie einen Fall nennen, in dem dies tatsächlich nützlich wäre? Mit anderen Worten, ist dies nur eine Theoriefrage?
– CDhowie
6. Dezember 2010 um 8:42 Uhr
Nun, sind Referenzen jemals unverzichtbar? An ihrer Stelle können immer Zeiger verwendet werden. So ein Null-Referenz würde Sie eine Referenz auch dann verwenden lassen, wenn Sie kein Objekt haben könnten, auf das Sie verweisen könnten. Ich weiß nicht, wie schmutzig es ist, aber bevor ich darüber nachdachte, interessierte ich mich für seine Legalität.
– Junge
6. Dezember 2010 um 8:53 Uhr
Ich denke es ist verpönt
– Ursprünglich
6. Dezember 2010 um 8:55 Uhr
“Wir könnten überprüfen” – nein, das können Sie nicht. Es gibt Compiler, die die Anweisung in umwandeln
if (false)
, wodurch die Prüfung eliminiert wird, gerade weil Verweise sowieso nicht null sein können. Eine besser dokumentierte Version existierte im Linux-Kernel, wo ein sehr ähnlicher NULL-Check herausoptimiert wurde: isc.sans.edu/diary.html?storyid=6820– MSalter
6. Dezember 2010 um 8:56 Uhr
“Einer der Hauptgründe für die Verwendung einer Referenz anstelle eines Zeigers besteht darin, Sie von der Last zu befreien, testen zu müssen, ob es sich auf ein gültiges Objekt bezieht.” Diese Antwort im Link von Default klingt ziemlich gut!
– Junge
6. Dezember 2010 um 9:22 Uhr