Die meisten Antworten hier sprechen nicht die inhärente Mehrdeutigkeit an, einen Rohzeiger in einer Funktionssignatur zu haben, um die Absicht auszudrücken. Die Probleme sind die folgenden:
Der Aufrufer weiß nicht, ob der Zeiger auf ein einzelnes Objekt oder auf den Anfang eines "Arrays" von Objekten zeigt.
Der Aufrufer weiß nicht, ob der Zeiger den Speicher "besitzt", auf den er zeigt. IE, ob die Funktion den Speicher freigeben soll oder nicht. ( foo(new int)
- Ist das ein Speicherverlust?).
Der Anrufer weiß nicht, ob er nullptr
sicher an die Funktion übergeben werden kann oder nicht .
Alle diese Probleme werden durch Referenzen gelöst:
Referenzen beziehen sich immer auf ein einzelnes Objekt.
Referenzen besitzen niemals das Gedächtnis, auf das sie sich beziehen, sie sind lediglich ein Blick in das Gedächtnis.
Referenzen dürfen nicht null sein.
Dies macht Referenzen zu einem viel besseren Kandidaten für die allgemeine Verwendung. Referenzen sind jedoch nicht perfekt - es sind einige Hauptprobleme zu berücksichtigen.
- Keine explizite Indirektion. Dies ist bei einem Rohzeiger kein Problem, da wir den
&
Operator verwenden müssen, um zu zeigen, dass wir tatsächlich einen Zeiger übergeben. Zum Beispiel int a = 5; foo(a);
ist hier überhaupt nicht klar, dass a als Referenz übergeben wird und geändert werden könnte.
- Nullbarkeit. Diese Schwäche von Zeigern kann auch eine Stärke sein, wenn wir tatsächlich wollen, dass unsere Referenzen nullbar sind. Da
std::optional<T&>
Zeiger (aus guten Gründen) nicht gültig sind, geben sie uns die gewünschte Nullfähigkeit.
Wenn wir also eine nullfähige Referenz mit expliziter Indirektion wollen, sollten wir nach einem T*
Recht greifen ? Falsch!
Abstraktionen
In unserer Verzweiflung nach Nullfähigkeit können wir nach T*
allen zuvor aufgeführten Mängeln und semantischen Zweideutigkeiten greifen und diese einfach ignorieren. Stattdessen sollten wir nach dem greifen, was C ++ am besten kann: einer Abstraktion. Wenn wir einfach eine Klasse schreiben, die einen Zeiger umschließt, gewinnen wir die Ausdruckskraft sowie die Nullbarkeit und explizite Indirektion.
template <typename T>
struct optional_ref {
optional_ref() : ptr(nullptr) {}
optional_ref(T* t) : ptr(t) {}
optional_ref(std::nullptr_t) : ptr(nullptr) {}
T& get() const {
return *ptr;
}
explicit operator bool() const {
return bool(ptr);
}
private:
T* ptr;
};
Dies ist die einfachste Oberfläche, die ich mir vorstellen kann, aber sie erledigt den Job effektiv. Sie können die Referenz initialisieren, prüfen, ob ein Wert vorhanden ist, und auf den Wert zugreifen. Wir können es so verwenden:
void foo(optional_ref<int> x) {
if (x) {
auto y = x.get();
// use y here
}
}
int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability
Wir haben unsere Ziele erreicht! Lassen Sie uns nun die Vorteile im Vergleich zum Rohzeiger sehen.
- Die Schnittstelle zeigt deutlich, dass sich die Referenz nur auf ein Objekt beziehen sollte.
- Offensichtlich besitzt es nicht den Speicher, auf den es sich bezieht, da es keinen benutzerdefinierten Destruktor und keine Methode zum Löschen des Speichers hat.
- Der Aufrufer weiß, dass übergeben werden
nullptr
kann, da der Funktionsautor explizit nach einem fragtoptional_ref
Wir könnten die Schnittstelle von hier aus komplexer gestalten, z. B. durch Hinzufügen von Gleichheitsoperatoren, einer Monade get_or
und einer map
Schnittstelle, einer Methode, die den Wert abruft oder eine Ausnahme auslöst, constexpr
Unterstützung. Das können Sie tun.
Anstatt rohe Zeiger zu verwenden, begründen Sie, was diese Zeiger tatsächlich in Ihrem Code bedeuten, und nutzen Sie entweder eine Standard-Bibliotheksabstraktion oder schreiben Sie Ihre eigene. Dadurch wird Ihr Code erheblich verbessert.
new
, einen Zeiger und die daraus resultierenden Eigentumsfragen zu erstellen.