Ist eine Nullreferenz möglich?

102

Ist dieser Code gültig (und definiertes Verhalten)?

int &nullReference = *(int*)0;

Sowohl g ++ und Klirren ++ kompilieren , ohne jede Vorwarnung, auch bei der Verwendung von -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Natürlich ist die Referenz nicht wirklich null, da nicht auf sie zugegriffen werden kann (dies würde bedeuten, dass ein Nullzeiger dereferenziert wird), aber wir könnten überprüfen, ob sie null ist oder nicht, indem wir ihre Adresse überprüfen:

if( & nullReference == 0 ) // null reference
Peoro
quelle
1
Können Sie einen Fall nennen, in dem dies tatsächlich nützlich wäre? Mit anderen Worten, ist dies nur eine theoretische Frage?
CDhowie
Sind Referenzen jemals unverzichtbar? Zeiger können immer anstelle von ihnen verwendet werden. Mit einer solchen Nullreferenz können Sie eine Referenz auch dann verwenden, wenn Sie kein Objekt haben, auf das Sie verweisen können. Ich weiß nicht, wie schmutzig es ist, aber bevor ich daran dachte, war ich an seiner Legalität interessiert.
Peoro
8
Ich denke, es ist verpönt
Standard
22
"wir könnten nachsehen" - nein, das kannst du nicht. Es gibt Compiler, die die Anweisung in verwandeln if (false)und die Prüfung eliminieren, gerade weil Referenzen sowieso nicht null sein können. Eine besser dokumentierte Version existierte im Linux-Kernel, wo eine sehr ähnliche NULL-Prüfung optimiert wurde: isc.sans.edu/diary.html?storyid=6820
MSalters
2
"Einer der Hauptgründe für die Verwendung einer Referenz anstelle eines Zeigers besteht darin, dass Sie nicht mehr testen müssen, ob es sich um ein gültiges Objekt handelt." Diese Antwort im Link von Default klingt ziemlich gut!
Peoro

Antworten:

75

Referenzen 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. [Hinweis: Insbesondere kann eine Nullreferenz in einem genau definierten Programm nicht existieren, da die einzige Möglichkeit, eine solche Referenz zu erstellen, darin besteht, sie an das „Objekt“ zu binden, das durch Dereferenzieren eines Nullzeigers erhalten wird, was zu undefiniertem Verhalten führt. Wie in 9.6 beschrieben, kann eine Referenz nicht direkt an ein Bitfeld gebunden werden. ]]

1,9 / 4:

Bestimmte andere Operationen werden in dieser Internationalen Norm als undefiniert beschrieben (z. B. der Effekt der Dereferenzierung des Nullzeigers).

Wie Johannes in einer gelöschten Antwort sagt, gibt es Zweifel, ob "Dereferenzieren eines Nullzeigers" kategorisch als undefiniertes Verhalten bezeichnet werden sollte. Dies ist jedoch nicht einer der Fälle, die Zweifel aufkommen lassen, da ein Nullzeiger sicherlich nicht auf ein "gültiges Objekt oder eine gültige Funktion" verweist und innerhalb des Normungsausschusses kein Wunsch besteht, Nullreferenzen einzuführen.

Steve Jessop
quelle
Ich habe meine Antwort entfernt, da mir klar wurde, dass das bloße Problem, einen Nullzeiger zu dereferenzieren und einen Wert zu erhalten, der sich darauf bezieht, etwas anderes ist, als tatsächlich einen Verweis darauf zu binden, wie Sie erwähnen. Obwohl sich l-Werte auch auf Objekte oder Funktionen beziehen sollen (in diesem Punkt gibt es also wirklich keinen Unterschied zu einer Referenzbindung), sind diese beiden Dinge immer noch getrennte Anliegen. Für den bloßen Akt der Dereferenzierung hier der Link: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb
1
@MSalters (Antwort auf Kommentar zur gelöschten Antwort; hier relevant) Ich kann der dort vorgestellten Logik nicht besonders zustimmen. Es mag zwar zweckmäßig sein, &*pals puniversell zu gelten, dies schließt jedoch undefiniertes Verhalten nicht aus (das seiner Natur nach "zu funktionieren scheint"). und ich bin nicht einverstanden, dass ein typeidAusdruck, der versucht, den Typ eines "dereferenzierten Nullzeigers" zu bestimmen, den Nullzeiger tatsächlich dereferenziert. Ich habe Leute gesehen, die ernsthaft darüber gestritten haben, dass &a[size_of_array]man sich nicht darauf verlassen kann und sollte, und trotzdem ist es einfacher und sicherer, einfach zu schreiben a + size_of_array.
Karl Knechtel
@Default-Standards in den [c ++] -Tags sollten hoch sein. Meine Antwort klang, als wären beide Handlungen ein und dasselbe :) Während der Dereferenzierung und des Erhaltens eines Werts, den Sie nicht weitergeben, der sich auf "kein Objekt" bezieht, könnte es machbar sein, ihn in einer Referenz zu speichern, die diesem begrenzten Umfang entgeht und plötzlich Auswirkungen haben könnte viel mehr Code.
Johannes Schaub - litb
@Karl gut in C ++ bedeutet "Dereferenzierung" nicht, einen Wert zu lesen. Einige Leute denken, "Dereferenzierung" bedeutet, tatsächlich auf den gespeicherten Wert zuzugreifen oder ihn zu ändern, aber das ist nicht wahr. Die Logik ist, dass C ++ sagt, dass sich ein Wert auf "ein Objekt oder eine Funktion" bezieht. Wenn dem so ist, ist die Frage, worauf sich der Wert *pbezieht, wann pein Nullzeiger ist. C ++ hat derzeit nicht die Vorstellung eines leeren Wertes, den das Problem 232 einführen wollte.
Johannes Schaub - Litb
Erkennung von dereferenzierten Nullzeigern in typeidWerken, die auf der Syntax anstatt auf der Semantik basieren. Das heißt, wenn Sie das tun typeid(0, *(ostream*)0)Sie tun haben undefinierte Verhalten - nicht bad_typeidgarantiert geworfen werden, auch wenn Sie einen L - Wert übergeben semantisch von einem Null - Pointer - Dereference führt. Aber syntaktisch gesehen ist es auf oberster Ebene keine Dereferenzierung, sondern ein Kommaoperatorausdruck.
Johannes Schaub - Litb
26

Die Antwort hängt von Ihrem Standpunkt ab:


Wenn Sie nach dem C ++ - Standard urteilen, können Sie keine Nullreferenz erhalten, da Sie zuerst ein undefiniertes Verhalten erhalten. Nach diesem ersten Auftreten von undefiniertem Verhalten lässt der Standard zu, dass alles passiert. Wenn Sie also schreiben *(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 Optimierungs-Compiler das undefinierte Verhalten erkennt 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 wird also nur Code generiert, der jeden Zeiger in die entsprechende Referenz verwandelt. (Übrigens: Dies ist ein Noop, da Zeiger und Referenzen im Assembler genau dasselbe Tier sind.) Nun, wenn Sie eine andere Datei user.cppmit dem Code haben

#include "converter.h"

void foo() {
    int& nullRef = toReference(nullptr);
    cout << nullRef;    //crash happens here
}

Der Compiler weiß nicht, dass toReference()der übergebene Zeiger dereferenziert wird, und geht davon aus, dass er eine gültige Referenz zurückgibt, die in der Praxis zufällig eine Nullreferenz ist. Der Aufruf ist erfolgreich, aber wenn Sie versuchen, die Referenz zu verwenden, stürzt das Programm ab. Hoffnungsvoll. Der Standard lässt zu, dass alles passiert, einschließlich des Aussehens von rosa Elefanten.

Sie fragen sich vielleicht, warum dies relevant ist, schließlich wurde das undefinierte Verhalten bereits im Inneren ausgelöst toReference(). Die Antwort lautet Debugging: Nullreferenzen können sich wie Nullzeiger verbreiten und vermehren. Wenn Sie nicht wissen, dass Nullreferenzen vorhanden sein können, und lernen, diese nicht zu erstellen, müssen Sie möglicherweise einige Zeit damit verbringen, herauszufinden, warum Ihre Mitgliedsfunktion abstürzt, wenn nur versucht wird, ein einfaches altes intMitglied zu lesen (Antwort: die Instanz) Im Aufruf des Mitglieds befand sich eine Nullreferenz, ebenso thiswie ein Nullzeiger, und Ihr Mitglied wird so berechnet, dass es sich als Adresse befindet. 8).


Wie wäre es also mit der Suche nach Nullreferenzen? Du hast die Zeile gegeben

if( & nullReference == 0 ) // null reference

in deiner Frage. Nun, das wird nicht funktionieren: Gemäß dem Standard haben Sie undefiniertes Verhalten, wenn Sie einen Nullzeiger dereferenzieren, und Sie können keine Nullreferenz erstellen, ohne einen Nullzeiger zu dereferenzieren, sodass Nullreferenzen nur im Bereich des undefinierten Verhaltens existieren. Da Ihr Compiler möglicherweise davon ausgeht, dass Sie kein undefiniertes Verhalten auslösen, kann er davon ausgehen, dass es keine Nullreferenz gibt (obwohl er leicht Code ausgibt, der Nullreferenzen generiert!). Als solches sieht es die if()Bedingung, kommt zu dem Schluss, dass es nicht wahr sein kann, und wirft einfach die gesamte if()Aussage weg . Mit der Einführung von Verbindungszeitoptimierungen ist es offensichtlich unmöglich geworden, auf robuste Weise nach Nullreferenzen zu suchen.


TL; DR:

Nullreferenzen sind eine schreckliche Existenz:

Ihre Existenz scheint unmöglich (= nach Standard),
aber sie existieren (= nach dem generierten Maschinencode),
aber Sie können sie nicht sehen, wenn sie existieren (= Ihre Versuche werden optimiert),
aber sie können Sie trotzdem töten, ohne es zu wissen (= 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 dich nicht verfolgen!

cmaster - Monica wieder einsetzen
quelle
2
Was genau ist ein "Ping-Elefant"?
Pharap
2
@Pharap Ich habe keine Ahnung, es war nur ein Tippfehler. Aber der C ++ - Standard würde es sowieso nicht interessieren, ob es rosa oder Ping-Elefanten sind ;-)
cmaster - Monica
9

Wenn Sie eine Möglichkeit finden möchten, null in einer Aufzählung von Singleton-Objekten darzustellen, ist es eine schlechte Idee, null (de) zu referenzieren (it C ++ 11, nullptr).

Warum nicht das statische Singleton-Objekt, das NULL innerhalb der Klasse darstellt, wie folgt deklarieren und einen Operator für die Umwandlung in einen Zeiger hinzufügen, der nullptr zurückgibt?

Bearbeiten: Mehrere Fehlertypen wurden korrigiert und eine if-Anweisung in main () hinzugefügt, um zu testen, ob der Operator für die Umwandlung in einen Zeiger tatsächlich funktioniert (was ich vergessen habe .. mein schlechtes) - 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;
  }
}
David Lee
quelle
weil es langsam ist
wandalen
6

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.
Jan Kratochvil
quelle