Lassen Sie mich versuchen, die verschiedenen möglichen Modi für die Weitergabe von Zeigern an Objekte anzugeben, deren Speicher von einer Instanz der std::unique_ptr
Klassenvorlage verwaltet wird . std::auto_ptr
Dies gilt auch für die ältere Klassenvorlage (die meines Erachtens alle Verwendungszwecke dieses eindeutigen Zeigers zulässt, für die jedoch zusätzlich modifizierbare l-Werte akzeptiert werden, wenn r-Werte erwartet werden, ohne dass sie aufgerufen werden müssen std::move
) und in gewissem Umfang auch für std::shared_ptr
.
Als konkretes Beispiel für die Diskussion werde ich den folgenden einfachen Listentyp betrachten
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
Instanzen einer solchen Liste (die Teile nicht mit anderen Instanzen teilen oder zirkulär sein dürfen) gehören vollständig demjenigen, der den Anfangszeiger hält list
. Wenn der Clientcode weiß, dass die darin gespeicherte Liste niemals leer sein wird, kann er auch die erste node
direkt anstelle von a speichernlist
. Es node
muss kein Destruktor für definiert werden: Da die Destruktoren für seine Felder automatisch aufgerufen werden, wird die gesamte Liste vom Smart-Zeiger-Destruktor rekursiv gelöscht, sobald die Lebensdauer des anfänglichen Zeigers oder Knotens endet.
Dieser rekursive Typ bietet die Gelegenheit, einige Fälle zu diskutieren, die im Fall eines intelligenten Zeigers auf einfache Daten weniger sichtbar sind. Auch die Funktionen selbst liefern gelegentlich (rekursiv) ein Beispiel für Client-Code. Das typedef für list
ist natürlich voreingenommen unique_ptr
, aber die Definition könnte geändert werden, um auto_ptr
oder zu verwendenshared_ptr
stattdessen, ohne dass viel an dem unten Gesagten muss (insbesondere in Bezug auf Ausnahmesicherheit, ohne dass Destruktoren geschrieben werden müssen).
Modi zum Weitergeben intelligenter Zeiger
Modus 0: Übergeben Sie einen Zeiger oder ein Referenzargument anstelle eines intelligenten Zeigers
Wenn sich Ihre Funktion nicht mit dem Besitz befasst, ist dies die bevorzugte Methode: Lassen Sie sie überhaupt keinen intelligenten Zeiger verwenden. In diesem Fall muss sich Ihre Funktion keine Sorgen machen, wem das Objekt gehört, auf das verwiesen wird, oder auf welche Weise das Eigentum verwaltet wird. Daher ist die Übergabe eines Rohzeigers sowohl absolut sicher als auch die flexibelste Form, da ein Client unabhängig vom Besitz immer kann Erstellen Sie einen Rohzeiger (entweder durch Aufrufen der get
Methode oder über den Adressoperator)&
).
Zum Beispiel sollte die Funktion zum Berechnen der Länge einer solchen Liste kein list
Argument, sondern ein Rohzeiger sein:
size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }
Ein Client, der eine Variable enthält, list head
kann diese Funktion als aufrufen length(head.get())
, während ein Client, der stattdessen node n
eine nicht leere Liste gespeichert hat, aufrufen kannlength(&n)
.
Wenn garantiert ist, dass der Zeiger nicht null ist (was hier nicht der Fall ist, da Listen möglicherweise leer sind), kann es vorziehen, eine Referenz anstelle eines Zeigers zu übergeben. Dies kann ein Zeiger / Verweis auf Nicht- const
Knoten sein, wenn die Funktion den Inhalt der Knoten aktualisieren muss, ohne einen von ihnen hinzuzufügen oder zu entfernen (letzterer würde den Besitz beinhalten).
Ein interessanter Fall, der in die Kategorie Modus 0 fällt, ist das Erstellen einer (tiefen) Kopie der Liste. Während eine Funktion, die dies tut, natürlich das Eigentum an der von ihr erstellten Kopie übertragen muss, geht es ihr nicht um das Eigentum an der Liste, die sie kopiert. Es könnte also wie folgt definiert werden:
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
Dieser Code copy
verdient eine genaue Betrachtung, sowohl für die Frage, warum er überhaupt kompiliert wird (das Ergebnis des rekursiven Aufrufs von in der Initialisiererliste bindet an das Referenzargument rvalue im Verschiebungskonstruktor von unique_ptr<node>
, auch bekannt list
als, wenn das next
Feld des initialisiert wird generiert node
) und für die Frage, warum es ausnahmesicher ist (wenn während des rekursiven Zuordnungsprozesses der Speicher knapp wird und einige Aufrufe von new
Würfen ausgeführt werden std::bad_alloc
, wird zu diesem Zeitpunkt ein Zeiger auf die teilweise erstellte Liste anonym in einem temporären Typ gehalten list
erstellt für die Initialisiererliste, und ihr Destruktor bereinigt diese Teilliste). Übrigens sollte man der Versuchung widerstehen, (wie ich es ursprünglich tat) die zweite nullptr
durch zu ersetzenp
, von dem schließlich bekannt ist, dass es an diesem Punkt null ist: Man kann keinen intelligenten Zeiger von einem (rohen) Zeiger auf eine Konstante konstruieren , selbst wenn bekannt ist, dass er null ist.
Modus 1: Übergeben Sie einen Smart Pointer als Wert
Eine Funktion, die einen Smart-Pointer-Wert als Argument verwendet, nimmt das Objekt in Besitz, auf das sofort verwiesen wird: Der Smart-Pointer, den der Aufrufer gehalten hat (ob in einer benannten Variablen oder einer anonymen temporären Datei), wird am Funktionseingang und beim Aufruf des Aufrufers in den Argumentwert kopiert Der Zeiger ist null geworden (im Falle einer temporären Kopie wurde die Kopie möglicherweise entfernt, aber in jedem Fall hat der Aufrufer den Zugriff auf das Objekt verloren, auf das verwiesen wird). Ich möchte diesen Modus- Anruf mit Bargeld anrufen : Der Anrufer zahlt im Voraus für den angerufenen Dienst und kann sich nach dem Anruf keine Illusionen über den Besitz machen. Um dies zu verdeutlichen, muss der Aufrufer nach den Sprachregeln das Argument einschließenstd::move
wenn der intelligente Zeiger in einer Variablen gehalten wird (technisch gesehen, wenn das Argument ein l-Wert ist); In diesem Fall (jedoch nicht für Modus 3 unten) führt diese Funktion das aus, was der Name andeutet, nämlich den Wert von der Variablen in eine temporäre zu verschieben, wobei die Variable null bleibt.
Für Fälle , in denen die genannte Funktion nimmt bedingungslos Eigentum an (stibitzt) das spitze-zu - Objekt, wird dieser Modus verwendet , mit std::unique_ptr
oder std::auto_ptr
ist ein guter Weg , um einen Zeiger zusammen mit seinem Eigentum an vorbei, die das Risiko von Speicherlecks vermieden werden . Dennoch denke ich, dass es nur sehr wenige Situationen gibt, in denen der folgende Modus 3 nicht (nur geringfügig) dem Modus 1 vorzuziehen ist. Aus diesem Grund werde ich keine Anwendungsbeispiele für diesen Modus geben. (Siehe jedoch das folgende reversed
Beispiel für Modus 3, in dem angemerkt wird, dass Modus 1 mindestens genauso gut funktioniert.) Wenn die Funktion mehr Argumente als nur diesen Zeiger verwendet, kann es vorkommen, dass es zusätzlich einen technischen Grund gibt, den Modus zu vermeiden 1 (mit std::unique_ptr
oder std::auto_ptr
): da eine tatsächliche Verschiebungsoperation stattfindet, während eine Zeigervariable übergeben wirdp
Durch den Ausdruck std::move(p)
kann nicht angenommen werden, dass p
er bei der Bewertung der anderen Argumente einen nützlichen Wert hat (die Reihenfolge der Bewertung ist nicht angegeben), was zu subtilen Fehlern führen kann. Im Gegensatz dazu stellt die Verwendung von Modus 3 sicher, dass p
vor dem Funktionsaufruf keine Verschiebung von erfolgt, sodass andere Argumente sicher auf einen Wert zugreifen können p
.
Bei Verwendung mit std::shared_ptr
ist dieser Modus insofern interessant, als der Aufrufer mit einer einzigen Funktionsdefinition auswählen kann, ob eine Freigabekopie des Zeigers für sich behalten werden soll, während eine neue Freigabekopie erstellt wird, die von der Funktion verwendet werden soll (dies geschieht, wenn ein l-Wert vorliegt Argument wird bereitgestellt, der beim Aufruf verwendete Kopierkonstruktor für gemeinsam genutzte Zeiger erhöht die Referenzanzahl) oder um der Funktion nur eine Kopie des Zeigers zu geben, ohne einen beizubehalten oder die Referenzanzahl zu berühren (dies geschieht möglicherweise, wenn ein rvalue-Argument angegeben wird ein Wert, der in einen Aufruf von std::move
) eingewickelt ist . Zum Beispiel
void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container
void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
f(p); // lvalue argument; store pointer in container but keep a copy
f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
f(std::move(p)); // xvalue argument; p is transferred to container and left null
}
Dasselbe könnte erreicht werden, indem void f(const std::shared_ptr<X>& x)
(für den Fall lvalue) und void f(std::shared_ptr<X>&& x)
(für den Fall rvalue) getrennt definiert werden , wobei sich die Funktionskörper nur dadurch unterscheiden, dass die erste Version die Kopiersemantik aufruft (bei Verwendung der Kopierkonstruktion / -zuweisung), x
während die zweite Version die Bewegungssemantik verschiebt (Schreiben std::move(x)
stattdessen wie im Beispielcode). Für gemeinsam genutzte Zeiger kann Modus 1 hilfreich sein, um eine gewisse Codeduplizierung zu vermeiden.
Modus 2: Übergeben Sie einen Smart Pointer als (modifizierbare) Wertreferenz
Hier erfordert die Funktion lediglich einen veränderbaren Verweis auf den Smart Pointer, gibt jedoch keinen Hinweis darauf, was er damit tun wird. Ich möchte diese Methode Call by Card aufrufen : Der Anrufer stellt die Zahlung durch Angabe einer Kreditkartennummer sicher. Die Referenz kann verwendet werden, um das Eigentum an dem Objekt zu übernehmen, auf das verwiesen wird, muss es aber nicht. Dieser Modus erfordert die Bereitstellung eines modifizierbaren lvalue-Arguments, das der Tatsache entspricht, dass der gewünschte Effekt der Funktion das Belassen eines nützlichen Werts in der Argumentvariablen umfassen kann. Ein Aufrufer mit einem rvalue-Ausdruck, den er an eine solche Funktion übergeben möchte, müsste ihn in einer benannten Variablen speichern, um den Aufruf ausführen zu können, da die Sprache nur eine implizite Konvertierung in eine Konstante bietetlWertreferenz (bezogen auf eine temporäre) aus einem rWert. (Im Gegensatz zur umgekehrten Situation std::move
ist eine Umwandlung von Y&&
bis Y&
mit Y
dem Smart-Pointer-Typ nicht möglich. Diese Konvertierung kann jedoch auf Wunsch durch eine einfache Vorlagenfunktion erzielt werden. Siehe https://stackoverflow.com/a/24868376 / 1436796 ). Für den Fall, dass die aufgerufene Funktion beabsichtigt, das Objekt bedingungslos zu übernehmen und das Argument zu stehlen, gibt die Verpflichtung zur Angabe eines lvalue-Arguments das falsche Signal: Die Variable hat nach dem Aufruf keinen nützlichen Wert. Daher sollte für eine solche Verwendung der Modus 3 bevorzugt werden, der innerhalb unserer Funktion identische Möglichkeiten bietet, die Anrufer jedoch auffordert, einen Wert anzugeben.
Es gibt jedoch einen gültigen Anwendungsfall für Modus 2, nämlich Funktionen, die den Zeiger ändern können , oder das Objekt, auf das in einer Weise verwiesen wird, die Eigentum beinhaltet . Ein Beispiel list
für eine solche Verwendung ist beispielsweise eine Funktion, die einem Knoten einen Präfix voranstellt :
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
Es wäre hier natürlich unerwünscht, Anrufer zur Verwendung zu zwingen std::move
, da ihr intelligenter Zeiger nach dem Anruf immer noch eine gut definierte und nicht leere Liste besitzt, wenn auch eine andere als zuvor.
Auch hier ist es interessant zu beobachten, was passiert, wenn der prepend
Anruf mangels freien Speichers fehlschlägt. Dann wird der new
Anruf werfen std::bad_alloc
; Da zu diesem Zeitpunkt keine node
zugewiesen werden konnte, ist es sicher, dass die übergebene r-Wert-Referenz (Modus 3) von std::move(l)
noch nicht gestohlen werden kann, da dies getan würde, um das next
Feld der node
nicht zugewiesenen zu erstellen. Der ursprüngliche intelligente Zeiger enthält also l
immer noch die ursprüngliche Liste, wenn der Fehler ausgelöst wird. Diese Liste wird entweder vom Smart Pointer Destructor ordnungsgemäß zerstört oder enthält die ursprüngliche Liste, l
falls sie dank einer ausreichend frühen catch
Klausel überlebt .
Das war ein konstruktives Beispiel; Mit einem Augenzwinkern auf diese Frage kann man auch das destruktivere Beispiel für das Entfernen des ersten Knotens mit einem bestimmten Wert geben, falls vorhanden:
void remove_first(int x, list& l)
{ list* p = &l;
while ((*p).get()!=nullptr and (*p)->entry!=x)
p = &(*p)->next;
if ((*p).get()!=nullptr)
(*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next);
}
Auch hier ist die Richtigkeit ziemlich subtil. Insbesondere wird in der letzten Anweisung der Zeiger (*p)->next
, der in dem zu entfernenden Knoten enthalten ist, nicht verknüpft (von release
, was den Zeiger zurückgibt, aber die ursprüngliche Null macht), bevor reset
(implizit) dieser Knoten zerstört wird (wenn er den alten Wert zerstört, der von gehalten wird p
), wodurch sichergestellt wird, dass Zu diesem Zeitpunkt wird nur ein Knoten zerstört. (In der im Kommentar erwähnten alternativen Form würde dieser Zeitpunkt den Interna der Implementierung des Verschiebungszuweisungsoperators der std::unique_ptr
Instanz list
überlassen bleiben; der Standard besagt 20.7.1.2.3; 2, dass dieser Operator "so handeln soll, als ob von anrufen reset(u.release())
", woher sollte das Timing auch hier sicher sein.)
Beachten Sie, dass prepend
und remove_first
nicht von Clients aufgerufen werden können, die eine lokale node
Variable für eine immer nicht leere Liste speichern , und das zu Recht, da die angegebenen Implementierungen in solchen Fällen nicht funktionieren könnten.
Modus 3: Übergeben Sie einen Smart Pointer an eine (modifizierbare) Wertreferenz
Dies ist der bevorzugte Modus, wenn Sie einfach den Zeiger in Besitz nehmen. Ich möchte diese Methode call by check aufrufen : Der Anrufer muss die Unterzeichnung des Eigentums akzeptieren, als ob er Bargeld zur Verfügung stellen würde, indem er den Scheck unterschreibt. Die tatsächliche Auszahlung wird jedoch verschoben, bis die aufgerufene Funktion den Zeiger tatsächlich stiehlt (genau wie bei Verwendung von Modus 2) ). Das "Signieren des Schecks" bedeutet konkret, dass Anrufer ein Argument std::move
einschließen müssen (wie in Modus 1), wenn es sich um einen Wert handelt (wenn es sich um einen Wert handelt, ist der Teil "Aufgeben des Eigentums" offensichtlich und erfordert keinen separaten Code).
Beachten Sie, dass sich Modus 3 technisch genau wie Modus 2 verhält, sodass die aufgerufene Funktion nicht den Besitz übernehmen muss. Ich würde jedoch darauf bestehen , dass , wenn es eine Unsicherheit über die Eigentumsübertragung ist (bei normalem Gebrauch), Modus 2 sollte 3 bis Modus bevorzugt sein, dass so mit Modus 3 ist implizit ein Signal an Anrufer , dass sie sind Eigentum aufzugeben. Man könnte erwidern, dass nur das Übergeben von Argumenten im Modus 1 tatsächlich einen erzwungenen Verlust des Eigentums an Anrufer signalisiert. Wenn ein Client jedoch Zweifel an den Absichten der aufgerufenen Funktion hat, sollte er die Spezifikationen der aufgerufenen Funktion kennen, was jeden Zweifel beseitigen sollte.
Es ist überraschend schwierig, ein typisches Beispiel für unseren list
Typ zu finden, der die Argumentübergabe im Modus 3 verwendet. Das Verschieben einer Liste b
an das Ende einer anderen Liste a
ist ein typisches Beispiel. jedoch a
(die überlebt und hält das Ergebnis der Operation) hindurchgeführt ist besser Modus 2:
void append (list& a, list&& b)
{ list* p=&a;
while ((*p).get()!=nullptr) // find end of list a
p=&(*p)->next;
*p = std::move(b); // attach b; the variable b relinquishes ownership here
}
Ein reines Beispiel für die Übergabe von Argumenten im Modus 3 ist das folgende, das eine Liste (und deren Besitz) übernimmt und eine Liste mit den identischen Knoten in umgekehrter Reihenfolge zurückgibt.
list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
list result(nullptr);
while (p.get()!=nullptr)
{ // permute: result --> p->next --> p --> (cycle to result)
result.swap(p->next);
result.swap(p);
}
return result;
}
Diese Funktion kann wie in aufgerufen werden l = reversed(std::move(l));
, um die Liste in sich selbst umzukehren, aber die umgekehrte Liste kann auch anders verwendet werden.
Hier wird das Argument aus Effizienzgründen sofort in eine lokale Variable verschoben (man hätte den Parameter l
direkt anstelle von verwenden können p
, aber jeder Zugriff darauf würde jedes Mal eine zusätzliche Indirektionsebene erfordern); Daher ist der Unterschied zum Übergeben von Argumenten im Modus 1 minimal. In diesem Modus hätte das Argument tatsächlich direkt als lokale Variable dienen können, wodurch diese anfängliche Verschiebung vermieden wurde. Dies ist nur ein Beispiel für das allgemeine Prinzip, dass, wenn ein als Referenz übergebenes Argument nur dazu dient, eine lokale Variable zu initialisieren, man es genauso gut als Wert übergeben und den Parameter als lokale Variable verwenden kann.
Die Verwendung von Modus 3 scheint vom Standard befürwortet zu werden, was durch die Tatsache belegt wird, dass alle bereitgestellten Bibliotheksfunktionen, die den Besitz von intelligenten Zeigern unter Verwendung von Modus 3 übertragen, ein besonders überzeugendes Beispiel dafür der Konstruktor sind std::shared_ptr<T>(auto_ptr<T>&& p)
. Dieser Konstruktor verwendete (in std::tr1
), um eine modifizierbare lvalue- Referenz zu verwenden (genau wie der auto_ptr<T>&
Kopierkonstruktor), und konnte daher mit einem auto_ptr<T>
lvalue p
wie in aufgerufen werden std::shared_ptr<T> q(p)
, wonach p
er auf null zurückgesetzt wurde. Aufgrund des Wechsels von Modus 2 zu 3 bei der Argumentübergabe muss dieser alte Code jetzt neu geschrieben werden std::shared_ptr<T> q(std::move(p))
und funktioniert dann weiter. Ich verstehe, dass das Komitee den Modus 2 hier nicht mochte, aber sie hatten die Möglichkeit, durch Definition in Modus 1 zu wechselnstd::shared_ptr<T>(auto_ptr<T> p)
Stattdessen hätten sie sicherstellen können, dass alter Code ohne Änderung funktioniert, da (im Gegensatz zu eindeutigen Zeigern) Auto-Zeiger stillschweigend auf einen Wert dereferenziert werden können (das Zeigerobjekt selbst wird dabei auf Null zurückgesetzt). Anscheinend hat das Komitee es so sehr vorgezogen, Modus 3 gegenüber Modus 1 zu befürworten, dass es sich dafür entschieden hat, vorhandenen Code aktiv zu brechen, anstatt Modus 1 selbst für eine bereits veraltete Verwendung zu verwenden.
Wann sollte Modus 3 gegenüber Modus 1 bevorzugt werden?
Modus 1 ist in vielen Fällen perfekt verwendbar und kann gegenüber Modus 3 in Fällen bevorzugt werden, in denen die Annahme des Eigentums ansonsten die Form des Verschiebens des intelligenten Zeigers auf eine lokale Variable wie im reversed
obigen Beispiel annehmen würde . Ich kann jedoch zwei Gründe dafür sehen, Modus 3 im allgemeineren Fall zu bevorzugen:
Es ist etwas effizienter, eine Referenz zu übergeben, als eine temporäre Referenz zu erstellen und den alten Zeiger nicht zu verwenden (der Umgang mit Bargeld ist etwas mühsam). In einigen Szenarien kann der Zeiger mehrmals unverändert an eine andere Funktion übergeben werden, bevor er tatsächlich gestohlen wird. Ein solches Übergeben erfordert im Allgemeinen das Schreiben std::move
(es sei denn, Modus 2 wird verwendet). Beachten Sie jedoch, dass dies nur eine Besetzung ist, die eigentlich nichts tut (insbesondere keine Dereferenzierung), sodass keine Kosten anfallen.
Sollte es denkbar sein, dass irgendetwas eine Ausnahme zwischen dem Start des Funktionsaufrufs und dem Punkt auslöst, an dem es (oder ein enthaltener Aufruf) das Objekt, auf das verwiesen wird, tatsächlich in eine andere Datenstruktur verschiebt (und diese Ausnahme ist nicht bereits in der Funktion selbst gefangen ), dann wird bei Verwendung von Modus 1 das Objekt, auf das der Smart Pointer verweist, zerstört, bevor eine catch
Klausel die Ausnahme behandeln kann (da der Funktionsparameter beim Abwickeln des Stapels zerstört wurde), bei Verwendung von Modus 3 jedoch nicht. Letzteres gibt die Der Anrufer hat in solchen Fällen die Möglichkeit, die Daten des Objekts wiederherzustellen (indem er die Ausnahme abfängt). Beachten Sie, dass Modus 1 hier keinen Speicherverlust verursacht , aber zu einem nicht behebbaren Datenverlust für das Programm führen kann, was ebenfalls unerwünscht sein kann.
Rückgabe eines intelligenten Zeigers: immer nach Wert
Um ein Wort über die Rückgabe eines intelligenten Zeigers zu schließen, der vermutlich auf ein Objekt zeigt, das für die Verwendung durch den Aufrufer erstellt wurde. Dies ist nicht wirklich ein Fall, der mit der Übergabe von Zeigern an Funktionen vergleichbar ist, aber der Vollständigkeit halber möchte ich darauf bestehen, dass in solchen Fällen immer nach Wert zurückgegeben wird (und nicht std::move
in der return
Anweisung verwendet wird). Niemand möchte einen Verweis auf einen Zeiger erhalten, der wahrscheinlich gerade nicht gemixt wurde.