Ist ein Nullbezug möglich?

Lesezeit: 11 Minuten

Benutzer-Avatar
Junge

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

  • 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

Benutzer-Avatar
Steve Jessop

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.

  • Ich habe meine Antwort entfernt, da mir klar wurde, dass das bloße Problem, einen Nullzeiger zu dereferenzieren und einen Lvalue zu erhalten, der sich darauf bezieht, eine andere Sache ist, als tatsächlich einen Verweis darauf zu binden, wie Sie erwähnen. Obwohl lvalues ​​auch auf Objekte oder Funktionen verweisen sollen (in diesem Punkt gibt es also wirklich keinen Unterschied zu einer Referenzbindung), sind diese beiden Dinge immer noch getrennte Angelegenheiten. Für den bloßen Akt der Dereferenzierung ist hier der Link: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102

    – Johannes Schaub – litb

    6. Dezember 2010 um 9:03 Uhr


  • @MSalters (Antwort auf Kommentar zu gelöschter Antwort; hier relevant) Ich kann der dort vorgestellten Logik nicht besonders zustimmen. Während es bequem sein kann, zu eliminieren &*p als p generell schließt das undefiniertes Verhalten nicht aus (das von Natur aus „zu funktionieren scheint“); und ich bin anderer Meinung, dass a typeid Ausdruck, der versucht, den Typ eines “dereferenzierten Nullzeigers” zu bestimmen, dereferenziert tatsächlich den Nullzeiger. Ich habe Leute gesehen, die das ernsthaft argumentierten &a[size_of_array] man kann und sollte sich nicht darauf verlassen, und außerdem ist es einfacher und sicherer, einfach zu schreiben a + size_of_array.

    – Karl Knechtel

    6. Dezember 2010 um 9:08 Uhr

  • @Default Standards in der [c++] Tags sollten hoch sein. Meine Antwort klang so, als wären beide Akte ein und dasselbe 🙂 Während das Dereferenzieren und Abrufen eines lvalue, den Sie nicht weitergeben, der sich auf “kein Objekt” bezieht, machbar sein könnte, könnte das Speichern in einer Referenz diesem begrenzten Umfang entkommen und sich plötzlich auswirken viel mehr Code.

    – Johannes Schaub – litb

    6. Dezember 2010 um 9:09 Uhr


  • @Karl Nun, in C++ bedeutet “Dereferenzieren” nicht, einen Wert zu lesen. Einige Leute denken, dass “Dereferenzieren” bedeutet, tatsächlich auf den gespeicherten Wert zuzugreifen oder ihn zu ändern, aber das stimmt nicht. Die Logik ist, dass C++ sagt, dass sich ein lvalue auf “ein Objekt oder eine Funktion” bezieht. Wenn dem so ist, dann ist die Frage, was der lvalue ist *p bezieht sich darauf, wann p ist ein Nullzeiger. C++ hat derzeit nicht den Begriff eines leeren lvalue, den die Ausgabe 232 einführen wollte.

    – Johannes Schaub – litb

    6. Dezember 2010 um 9:12 Uhr


  • Nur zur bestätigung; das bleibt der Fall durch C++14, 17 und 20, ja?

    – Jason C

    19. Dezember 2020 um 0:57 Uhr

Benutzer-Avatar
cmaster – monica wieder einsetzen

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!

  • Was in aller Welt meinen Sie mit “Sie können keine Nullreferenz erstellen, ohne einen Nullzeiger zu dereferenzieren”? Der Compiler validiert Verweise, indem er sie bei der Erfassung/Initialisierung dereferenziert. Außerdem habe ich ein Codefragment erstellt, das eine Nullreferenz erstellt, ohne einen Nullzeiger zu dereferenzieren.

    – Saphir_Ziegel

    3. Januar 2021 um 6:39 Uhr

  • @Sapphire_Brick Nun, in Ihrem Codebeispiel erstellen Sie keine Nullreferenz, sondern eine nicht initialisiert Referenz: Wenn Sie die initialisieren union, setzen Sie den Zeiger, nicht die Referenz. Wenn Sie die Referenz in der nächsten Zeile verwenden, rufen Sie undefiniertes Verhalten auf, indem Sie das nicht initialisierte Union-Member verwenden. Natürlich steht es Ihrem Compiler frei, Ihnen in diesem Fall eine Null-Referenz zu geben, und praktisch alle Compiler werden das tun: Die Referenz ist nur ein Zeiger unter der Haube und teilt seinen Speicher mit einem Zeiger, auf den Sie gesetzt haben nullptr.

    – Cmaster – Wiedereinsetzung von Monica

    3. Januar 2021 um 8:42 Uhr

  • @Sapphire_Brick So war es, bevor es strenge Aliasing-Regeln gab. Jetzt ist es genauso undefiniertes Verhalten wie das Wortspiel eines Zeigers. Dem Compiler steht es frei, das Lesen vor dem Schreiben zu planen. Der einzig sichere Weg, Bits neu zu interpretieren, ist ein Aufruf von memcpy().

    – Cmaster – Wiedereinsetzung von Monica

    3. Januar 2021 um 23:57 Uhr

  • @Sapphire_Brick volatile erzwingt nur die exakte Reihenfolge und keine ausgelassenen Lese-/Schreibvorgänge bei flüchtigen Variablen, es bietet keine Garantien in Bezug auf andere Variablen. Es sollte nur für speicherabgebildete Hardwareregister verwendet werden. Implizite Bitmusterumwandlung zwischen volatile Werte über Typ-Punning oder Unions bleiben undefiniertes Verhalten, afaik.

    – Cmaster – Wiedereinsetzung von Monica

    4. Januar 2021 um 19:01 Uhr

  • @Sapphire_Brick Ja, das war der springende Punkt bei strengen Aliasing-Regeln: Um Compiler-Optimierungen zu ermöglichen, die sie nach früheren Standards nicht hätten durchführen dürfen. Natürlich hat dies bestehenden Code gebrochen. Der Aufwand, alle Speicherzugriffe als gleich zu betrachten, war in der gesamten C-Codebasis sichtbar, aber die Fälle von Pointer Punning und union Missbrauch war rar gesät. Folglich wurde die positive Wirkung strenger Aliasing-Regeln als wichtiger erachtet als das sporadische Fehlverhalten von bestehendem Code. Und dieses Fehlverhalten könnte leicht behoben werden, indem man einige hinzufügt memcpy() Anrufe.

    – Cmaster – Wiedereinsetzung von Monica

    21. Januar 2021 um 18:53 Uhr

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.

Benutzer-Avatar
David Lee

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;
  }
}

1013380cookie-checkIst ein Nullbezug möglich?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy