Wie übergebe ich ein unique_ptr-Argument an einen Konstruktor oder eine Funktion?


400

Ich bin neu in der Verschiebung der Semantik in C ++ 11 und weiß nicht genau, wie ich mit unique_ptrParametern in Konstruktoren oder Funktionen umgehen soll . Betrachten Sie diese Klasse, die sich selbst referenziert:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

Soll ich so Funktionen schreiben, die unique_ptrArgumente verwenden?

Und muss ich std::moveden aufrufenden Code verwenden?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


1
Ist es nicht ein Segmentierungsfehler, wenn Sie b1-> setNext für einen leeren Zeiger aufrufen?
Balki

Antworten:


836

Hier sind die möglichen Möglichkeiten, einen eindeutigen Zeiger als Argument zu verwenden, sowie die zugehörige Bedeutung.

(A) Nach Wert

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Damit der Benutzer dies aufrufen kann, muss er einen der folgenden Schritte ausführen:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Wenn Sie einen eindeutigen Zeiger als Wert verwenden, übertragen Sie das Eigentum an dem Zeiger auf die betreffende Funktion / das betreffende Objekt / usw. Nach dem newBaseBau nextBaseist garantiert leer . Sie besitzen das Objekt nicht mehr und haben nicht einmal mehr einen Zeiger darauf. Es ist weg.

Dies ist gewährleistet, da wir den Parameter als Wert nehmen. bewegtstd::move eigentlich nichts; Es ist nur eine schicke Besetzung. Gibt a zurück , auf das ein r-Wert verweist . Das ist alles was es tut.std::move(nextBase)Base&&nextBase

Da Base::Base(std::unique_ptr<Base> n)C ++ sein Argument eher nach Wert als nach R-Wert-Referenz verwendet, erstellt es automatisch eine temporäre Datei für uns. Es entsteht ein std::unique_ptr<Base>aus dem Base&&wir die Funktion über gegeben haben std::move(nextBase). Es ist die Konstruktion dieses Temporärs, die den Wert tatsächlich nextBasein das Funktionsargument verschiebt n.

(B) Durch nicht konstante l-Wert-Referenz

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

Dies muss für einen tatsächlichen l-Wert (eine benannte Variable) aufgerufen werden. Es kann nicht mit einem temporären wie diesem aufgerufen werden:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

Die Bedeutung ist dieselbe wie die Bedeutung jeder anderen Verwendung von nicht konstanten Referenzen: Die Funktion kann das Eigentum an dem Zeiger beanspruchen oder nicht . Angesichts dieses Codes:

Base newBase(nextBase);

Es gibt keine Garantie, nextBasedie leer ist. Es kann leer sein; es darf nicht. Es kommt wirklich darauf an, was man machen Base::Base(std::unique_ptr<Base> &n)will. Aus diesem Grund ist nicht nur anhand der Funktionssignatur ersichtlich, was passieren wird. Sie müssen die Implementierung (oder die zugehörige Dokumentation) lesen.

Aus diesem Grund würde ich dies nicht als Schnittstelle vorschlagen.

(C) Durch konstante l-Wert-Referenz

Base(std::unique_ptr<Base> const &n);

Ich zeige keine Implementierung, da Sie nicht von einem wechseln können const&. Wenn Sie a übergeben const&, sagen Sie, dass die Funktion Baseüber den Zeiger auf das zugreifen kann , es aber nirgendwo speichern kann. Es kann kein Eigentum daran beanspruchen.

Dies kann nützlich sein. Nicht unbedingt für Ihren speziellen Fall, aber es ist immer gut, jemandem einen Zeiger geben zu können und zu wissen, dass er nicht (ohne gegen die Regeln von C ++ zu verstoßen, wie kein Wegwerfen const) das Eigentum daran beanspruchen kann. Sie können es nicht speichern. Sie können es an andere weitergeben, aber diese anderen müssen sich an dieselben Regeln halten.

(D) Durch r-Wert-Referenz

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Dies ist mehr oder weniger identisch mit dem Fall "durch nicht konstante l-Wert-Referenz". Die Unterschiede sind zwei Dinge.

  1. Sie können eine vorübergehende übergeben:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Sie müssen verwenden, std::movewenn Sie nicht temporäre Argumente übergeben.

Letzteres ist wirklich das Problem. Wenn Sie diese Zeile sehen:

Base newBase(std::move(nextBase));

Sie haben eine vernünftige Erwartung, dass diese Zeile nach Abschluss nextBaseleer sein sollte. Es hätte verschoben werden sollen. Immerhin std::movesitzt das da und sagt dir, dass Bewegung stattgefunden hat.

Das Problem ist, dass es nicht hat. Es ist nicht garantiert , dass es verschoben wurde. Es wurde möglicherweise verschoben, aber Sie werden es nur anhand des Quellcodes erkennen. Sie können nicht nur an der Funktionssignatur erkennen.

Empfehlungen

  • (A) Nach Wert: Wenn Sie damit meinen, dass eine Funktion das Eigentum an a beansprucht unique_ptr, nehmen Sie es nach Wert.
  • (C) Mit const l-Wert-Referenz: Wenn Sie für eine Funktion einfach die unique_ptrfür die Dauer der Ausführung dieser Funktion verwenden möchten, nehmen Sie sie durch const&. Alternativ können Sie a &oder const&an den tatsächlichen Typ übergeben, auf den verwiesen wird, anstatt a zu verwenden unique_ptr.
  • (D) Durch r-Wert-Referenz: Wenn eine Funktion (abhängig von internen Codepfaden) den Besitz beanspruchen kann oder nicht, nehmen Sie sie vorbei &&. Ich rate jedoch dringend davon ab, wann immer dies möglich ist.

So manipulieren Sie unique_ptr

Sie können a nicht kopieren unique_ptr. Sie können es nur bewegen. Der richtige Weg, dies zu tun, ist mit der std::moveStandardbibliotheksfunktion.

Wenn Sie einen unique_ptrBy-Wert nehmen, können Sie sich frei davon bewegen. Aber Bewegung passiert eigentlich nicht wegen std::move. Nehmen Sie die folgende Aussage:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

Das sind wirklich zwei Aussagen:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(Hinweis: Der obige Code wird technisch nicht kompiliert, da nicht temporäre R-Wert-Referenzen keine R-Werte sind. Er dient nur zu Demozwecken.)

Dies temporaryist nur ein R-Wert-Verweis auf oldPtr. Es ist im Konstruktor von newPtrwo die Bewegung geschieht. unique_ptrDer Bewegungskonstruktor (ein Konstruktor, der a &&für sich nimmt) ist das, was die eigentliche Bewegung bewirkt.

Wenn Sie einen haben unique_ptrWert und Sie wollen es irgendwo zu speichern, Sie müssen verwenden std::moveden Speicher zu tun.


5
@Nicol: benennt aber std::movenicht den Rückgabewert. Denken Sie daran, dass benannte rWertreferenzen lWerte sind. ideone.com/VlEM3
R. Martinho Fernandes

31
Ich stimme dieser Antwort grundsätzlich zu, habe aber einige Bemerkungen. (1) Ich glaube nicht, dass es einen gültigen Anwendungsfall für die Übergabe eines Verweises auf const lvalue gibt: Alles, was der Angerufene damit tun könnte, kann er auch mit einem Verweis auf const (nackten) Zeiger tun, oder noch besser auf den Zeiger selbst [und Es geht ihn nichts an zu wissen, dass das Eigentum durch a gehalten wird unique_ptr; Möglicherweise benötigen einige andere Aufrufer dieselbe Funktionalität, halten jedoch shared_ptrstattdessen einen Aufruf.] (2) Aufruf durch lWertreferenz könnte nützlich sein, wenn die aufgerufene Funktion den Zeiger ändert , z. B. das Hinzufügen oder Entfernen von (im Besitz einer Liste befindlichen) Knoten zu einer verknüpften Liste.
Marc van Leeuwen

8
... (3) Obwohl Ihr Argument, die Übergabe von Werten gegenüber der Übergabe von rvalue-Referenzen zu bevorzugen, sinnvoll ist, denke ich, dass der Standard selbst unique_ptrWerte immer von rvalue-Referenzen übergibt (zum Beispiel bei der Umwandlung in shared_ptr). Der Grund dafür könnte sein, dass es etwas effizienter ist (es wird nicht zu temporären Zeigern gewechselt), während es dem Aufrufer genau die gleichen Rechte einräumt (kann r-Werte oder eingewickelte l-Werte übergeben std::move, aber keine nackten l-Werte).
Marc van Leeuwen

19
Nur um zu wiederholen, was Marc gesagt hat, und um Sutter zu zitieren : "Verwenden Sie keine const unique_ptr & als Parameter; verwenden Sie stattdessen das Widget *"
Jon

17
Wir haben ein Problem mit dem By-Value entdeckt - die Verschiebung erfolgt während der Argumentinitialisierung, die in Bezug auf andere Argumentauswertungen ungeordnet ist (außer natürlich in einer initializer_list). Während das Akzeptieren einer r-Wert-Referenz die Verschiebung nach dem Funktionsaufruf und daher nach der Auswertung anderer Argumente stark anordnet. Das Akzeptieren einer r-Wert-Referenz sollte daher bevorzugt werden, wenn das Eigentum übernommen wird.
Ben Voigt

57

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_ptrKlassenvorlage verwaltet wird . std::auto_ptrDies 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 nodedirekt anstelle von a speichernlist . Es nodemuss 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 listist natürlich voreingenommen unique_ptr, aber die Definition könnte geändert werden, um auto_ptroder 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 getMethode oder über den Adressoperator)& ).

Zum Beispiel sollte die Funktion zum Berechnen der Länge einer solchen Liste kein listArgument, 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 headkann diese Funktion als aufrufen length(head.get()), während ein Client, der stattdessen node neine 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- constKnoten 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 copyverdient 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 listals, wenn das nextFeld 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 newWü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 listerstellt für die Initialisiererliste, und ihr Destruktor bereinigt diese Teilliste). Übrigens sollte man der Versuchung widerstehen, (wie ich es ursprünglich tat) die zweite nullptrdurch 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::movewenn 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_ptroder std::auto_ptrist 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 reversedBeispiel 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_ptroder std::auto_ptr): da eine tatsächliche Verschiebungsoperation stattfindet, während eine Zeigervariable übergeben wirdpDurch den Ausdruck std::move(p)kann nicht angenommen werden, dass per 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 pvor dem Funktionsaufruf keine Verschiebung von erfolgt, sodass andere Argumente sicher auf einen Wert zugreifen können p.

Bei Verwendung mit std::shared_ptrist 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), xwä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::moveist eine Umwandlung von Y&&bis Y&mit Ydem 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 listfü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 prependAnruf mangels freien Speichers fehlschlägt. Dann wird der newAnruf werfen std::bad_alloc; Da zu diesem Zeitpunkt keine nodezugewiesen 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 nextFeld der nodenicht zugewiesenen zu erstellen. Der ursprüngliche intelligente Zeiger enthält also limmer 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, lfalls sie dank einer ausreichend frühen catchKlausel ü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_ptrInstanz 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 prependund remove_firstnicht von Clients aufgerufen werden können, die eine lokale nodeVariable 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::moveeinschließ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 listTyp zu finden, der die Argumentübergabe im Modus 3 verwendet. Das Verschieben einer Liste ban das Ende einer anderen Liste aist 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 ldirekt 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 pwie in aufgerufen werden std::shared_ptr<T> q(p), wonach per 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 reversedobigen 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 catchKlausel 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 returnAnweisung verwendet wird). Niemand möchte einen Verweis auf einen Zeiger erhalten, der wahrscheinlich gerade nicht gemixt wurde.


1
+1 für den Modus 0 - Übergabe des zugrunde liegenden Zeigers anstelle des unique_ptr. Etwas abseits des Themas (da es um die Übergabe eines unique_ptr geht), aber es ist einfach und vermeidet Probleme.
Machta

" Modus 1 verursacht hier keinen Speicherverlust " - das bedeutet, dass Modus 3 einen Speicherverlust verursacht, was nicht der Fall ist. Unabhängig davon, ob unique_ptres verschoben wurde oder nicht, wird der Wert trotzdem gut gelöscht, wenn er bei jeder Zerstörung oder Wiederverwendung noch gespeichert wird.
Rustyx

@RustyX: Ich kann nicht sehen, wie Sie diese Implikation interpretieren, und ich wollte nie sagen, was Sie denken, dass ich impliziert habe. Alles was ich damit gemeint habe ist, dass wie anderswo die Verwendung von unique_ptreinen Speicherverlust verhindert (und somit in gewissem Sinne seinen Vertrag erfüllt), aber hier (dh unter Verwendung von Modus 1) kann es (unter bestimmten Umständen) etwas verursachen, das als noch schädlicher angesehen werden kann , nämlich ein Datenverlust (Zerstörung des angegebenen Wertes), der mit Modus 3 hätte vermieden werden können.
Marc van Leeuwen

4

Ja, das müssen Sie, wenn Sie den unique_ptrby-Wert im Konstruktor übernehmen. Explizität ist eine schöne Sache. Da unique_ptres nicht kopierbar ist (Private Copy Ctor), sollte das, was Sie geschrieben haben, Ihnen einen Compilerfehler geben.


3

Bearbeiten: Diese Antwort ist falsch, obwohl der Code genau genommen funktioniert. Ich lasse es nur hier, weil die Diskussion darunter zu nützlich ist. Diese andere Antwort ist die beste Antwort zum Zeitpunkt meiner letzten Bearbeitung: Wie übergebe ich ein unique_ptr-Argument an einen Konstruktor oder eine Funktion?

Die Grundidee von ::std::moveist, dass Leute, die an Ihnen vorbeikommen, es verwenden unique_ptrsollten, um das Wissen auszudrücken, dass sie wissen, dass sie unique_ptrvorbeikommen, und das Eigentum verlieren.

Dies bedeutet, dass Sie unique_ptrin Ihren Methoden einen r-Wert-Verweis auf a verwenden sollten, nicht auf a unique_ptrselbst. Dies funktioniert sowieso nicht, da für die Übergabe einer einfachen alten unique_ptrVersion eine Kopie erstellt werden muss. Dies ist in der Benutzeroberfläche für ausdrücklich verboten unique_ptr. Interessanterweise verwandelt die Verwendung einer benannten rvalue-Referenz diese wieder in einen lvalue, sodass Sie sie auch ::std::move in Ihren Methoden verwenden müssen.

Dies bedeutet, dass Ihre beiden Methoden folgendermaßen aussehen sollten:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Dann würden Leute, die die Methoden anwenden, dies tun:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Wie Sie sehen, ::std::movedrückt das aus, dass der Zeiger an dem Punkt den Besitz verlieren wird, an dem es am relevantesten und hilfreichsten ist, dies zu wissen. Wenn dies unsichtbar passiert, wäre es für Leute, die Ihre Klasse benutzen, sehr verwirrend, objptrplötzlich ohne ersichtlichen Grund den Besitz zu verlieren.


2
Benannte rWertreferenzen sind lWerte.
R. Martinho Fernandes

Bist du sicher, dass es so ist Base fred(::std::move(objptr));und nicht Base::UPtr fred(::std::move(objptr));?
Codablank1

1
Um meinen vorherigen Kommentar zu ergänzen: Dieser Code wird nicht kompiliert. Sie müssen weiterhin std::movesowohl den Konstruktor als auch die Methode implementieren. Und selbst wenn Sie einen Wert übergeben, muss der Aufrufer immer noch std::movelvalues ​​übergeben. Der Hauptunterschied besteht darin, dass diese Schnittstelle mit dem Pass-by-Value klar macht, dass das Eigentum verloren geht. Siehe Nicol Bolas Kommentar zu einer anderen Antwort.
R. Martinho Fernandes

@ codablank1: Ja. Ich demonstriere, wie der Konstruktor und die Methoden in base verwendet werden, die rvalue-Referenzen verwenden.
Omnifarious

@ R.MartinhoFernandes: Oh, interessant. Ich nehme an, das macht Sinn. Ich hatte erwartet, dass Sie sich irren, aber die tatsächlichen Tests haben gezeigt, dass Sie richtig liegen. Jetzt behoben.
Omnifarious

0
Base(Base::UPtr n):next(std::move(n)) {}

sollte viel besser sein als

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

und

void setNext(Base::UPtr n)

sollte sein

void setNext(Base::UPtr&& n)

mit dem gleichen Körper.

Und ... was ist evtin handle()??


3
Die Verwendung std::forwardhier hat keinen Vorteil : Es Base::UPtr&&handelt sich immer um einen r-Wert-Referenztyp, std::moveder als r-Wert übergeben wird. Es ist bereits richtig weitergeleitet.
R. Martinho Fernandes

7
Ich bin absolut anderer Meinung. Wenn eine Funktion einen unique_ptrBy-Wert annimmt, wird Ihnen garantiert, dass ein Verschiebungskonstruktor für den neuen Wert aufgerufen wurde (oder einfach, dass Sie einen temporären Wert erhalten haben). Dadurch wird sichergestellt, dass die unique_ptrVariable des Benutzers jetzt leer ist . Wenn Sie es &&stattdessen nehmen, wird es nur geleert, wenn Ihr Code eine Verschiebungsoperation aufruft. Auf Ihre Weise ist es möglich, dass die Variable, von der der Benutzer nicht verschoben wurde. Das macht den Benutzer std::moveverdächtig und verwirrend. Die Verwendung std::movesollte immer sicherstellen, dass etwas bewegt wurde .
Nicol Bolas

@NicolBolas: Du hast recht. Ich werde meine Antwort löschen, da Ihre Beobachtung absolut korrekt ist, solange sie funktioniert.
Omnifarious

0

Nach oben gewählte Antwort. Ich ziehe es vor, an einer Wertreferenz vorbeizukommen.

Ich verstehe, was das Problem beim Übergeben einer R-Wert-Referenz verursachen kann. Aber lassen Sie uns dieses Problem auf zwei Seiten aufteilen:

  • für Anrufer:

Ich muss Code schreiben Base newBase(std::move(<lvalue>))oder Base newBase(<rvalue>).

  • für Angerufene:

Der Bibliotheksautor sollte garantieren, dass der unique_ptr tatsächlich verschoben wird, um das Mitglied zu initialisieren, wenn er das Eigentum besitzen möchte.

Das ist alles.

Wenn Sie die rvalue-Referenz übergeben, wird nur eine "move" -Anweisung aufgerufen. Wenn Sie jedoch den Wert übergeben, sind es zwei.

Ja, wenn der Bibliotheksautor kein Experte in diesem Bereich ist, kann er unique_ptr möglicherweise nicht verschieben, um das Mitglied zu initialisieren, aber es ist das Problem des Autors, nicht Sie. Unabhängig davon, ob es sich um einen Wert oder eine Wertreferenz handelt, ist Ihr Code derselbe!

Wenn Sie eine Bibliothek schreiben, wissen Sie jetzt, dass Sie dies garantieren sollten. Tun Sie es einfach. Die Übergabe der rvalue-Referenz ist eine bessere Wahl als der Wert. Der Client, der Ihre Bibliothek verwendet, schreibt nur denselben Code.

Nun zu Ihrer Frage. Wie übergebe ich ein unique_ptr-Argument an einen Konstruktor oder eine Funktion?

Sie wissen, was die beste Wahl ist.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.