2010-11-30 4 views
232

unique_ptr<T> erlaubt keine Kopierkonstruktion, stattdessen unterstützt sie die Bewegungssemantik. Dennoch kann ich eine unique_ptr<T> von einer Funktion zurückgeben und den zurückgegebenen Wert einer Variablen zuweisen.unique_ptr aus Funktionen zurückgeben

#include <iostream> 
#include <memory> 

using namespace std; 

unique_ptr<int> foo() 
{ 
    unique_ptr<int> p(new int(10)); 

    return p;     // 1 
    //return move(p);   // 2 
} 

int main() 
{ 
    unique_ptr<int> p = foo(); 

    cout << *p << endl; 
    return 0; 
} 

Der obige Code kompiliert und funktioniert wie vorgesehen. Also, wie kommt es, dass die Zeile 1 den Kopierkonstruktor nicht aufruft und Compilerfehler verursacht? Wenn ich stattdessen die Zeile 2 verwenden müsste, wäre das sinnvoll (die Verwendung der Zeile 2 funktioniert ebenfalls, aber wir müssen das nicht tun).

Ich weiß, dass C++ 0x diese Ausnahme zu unique_ptr zulässt, da der Rückgabewert ein temporäres Objekt ist, das zerstört wird, sobald die Funktion beendet wird, wodurch die Eindeutigkeit des zurückgegebenen Zeigers garantiert wird. Ich bin neugierig, wie das implementiert wird, ist es speziell im Compiler oder gibt es eine andere Klausel in der Sprachspezifikation, die dieses ausnutzt?

+0

Wenn Sie eine * factory * -Methode implementieren würden, würden Sie am ehesten 1 oder 2 bevorzugen, um die Werksausgabe zurückzugeben? Ich nehme an, dass dies die gebräuchlichste Verwendung von 1 wäre, weil Sie bei einer richtigen Fabrik tatsächlich möchten, dass das Eigentum des konstruierten Objekts an den Anrufer weitergegeben wird. – Xharlie

+5

@ Xharlie? Sie beide übergeben den Besitz des 'unique_ptr'.Die ganze Frage ist, dass 1 und 2 zwei verschiedene Wege sind, um dasselbe zu erreichen. – Praetorian

Antwort

155

gibt es eine andere Klausel in der Sprachspezifikation, dass diese Taten?

Ja, siehe 12.8 §34 und §35:

Wenn bestimmte Kriterien erfüllt sind, wird eine Implementierung erlaubt das Kopieren/Verschieben Bau eines Klassenobjekts [...] Diese wegzulassen elision von kopieren/verschieben-Operationen, die so genannte Kopie elision, erlaubt ist [...] in einer return-Anweisung in einer Funktion mit einem Klasse Rückgabetyp, , wenn der Ausdruck der Name ein nichtflüchtiges automatisches Objekt ist mit dem gleichen cv-unqualifizierten Typ wie die Funktion Rückgabetyp [...]

Wenn die Kriterien für die Auslassung eines Kopiervorgangs erfüllt sind und das Objekt wird von einem L-Wert kopiert bezeichnet, Überladungsauflösung den Konstruktor für die Kopie wählen zuerst ausgeführt als ob das Objekt durch einen R-Wert bezeichnet wurden.


Ich wollte nur noch einen Punkt hinzufügen, dass hier den Standard von Wert zurück Wahl, weil ein im schlimmsten Fall, dh in der return-Anweisung benannten Wert sein sollte, ohne Elisionen in C++ 11, C + +14 und C++ 17 wird als ein rvalue behandelt. So zum Beispiel stellt die folgende Funktion mit der -fno-elide-constructors Flagge

std::unique_ptr<int> get_unique() { 
    auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1 
    return ptr; // <- 2, moved into the to be returned unique_ptr 
} 

... 

auto int_uptr = get_unique(); // <- 3 

Mit dem Flag gesetzt auf Zusammenstellung gibt es zwei Züge (1 und 2) passiert in dieser Funktion und dann einem Zug später (3).

+0

heh, war gerade dabei, eine Vermutung zu sagen, die dasselbe sagte, aber ich hatte nicht den neuen Standard vor mir. –

+0

@juanchopanza Im Wesentlichen meinen Sie, dass 'foo()' in der Tat auch zerstört wird (wenn es keinem zugewiesen wurde), genau wie der Rückgabewert innerhalb der Funktion, und daher macht es Sinn, dass C++ einen Move-Konstruktor verwendet wenn Sie 'unique_ptr tun p = foo();'? – 7cows

+0

Diese Antwort sagt eine Implementierung * ist erlaubt * etwas zu tun ... es sagt nicht, dass es muss, also, wenn dies der einzige relevante Abschnitt wäre, würde das bedeuten, sich auf dieses Verhalten zu verlassen ist nicht tragbar. Aber ich denke nicht, dass das richtig ist. Ich bin geneigt zu denken, dass die richtige Antwort mehr mit dem Move Constructor zu tun hat, wie in der Antwort von Nikola Smiljanic und Bartosz Milewski beschrieben. –

74

Dies ist in keiner Weise spezifisch für unique_ptr, sondern gilt für jede Klasse, die beweglich ist. Dies wird durch die Sprachregeln garantiert, da Sie nach Wert zurückgeben. Der Compiler versucht, Kopien zu erstellen, ruft einen Verschiebungskonstruktor auf, wenn er keine Kopien entfernen kann, ruft einen Kopierkonstruktor auf, wenn er nicht verschoben werden kann, und kompiliert nicht, wenn er nicht kopieren kann.

Wenn Sie eine Funktion haben, die unique_ptr als Argument akzeptiert, können Sie p nicht an sie übergeben. Sie müssten explizit move constructor aufrufen, aber in diesem Fall sollten Sie die Variable p nicht nach dem Aufruf von bar() verwenden.

void bar(std::unique_ptr<int> p) 
{ 
} 

int main() 
{ 
    unique_ptr<int> p = foo(); 
    bar(p); // error, can't implicitly invoke move constructor on lvalue 
    bar(std::move(p)); // OK but don't use p afterwards 
} 
+2

@Fred - naja, nicht wirklich. Obwohl 'p' kein temporäres ist, ist das Ergebnis von' foo() ', was zurückgegeben wird; somit ist es ein rvalue und kann verschoben werden, was die Zuordnung in 'main' ermöglicht. Ich würde sagen, du hast dich geirrt, außer dass Nikola dann diese Regel auf "p" anzuwenden scheint, was irrtümlich ist. –

+0

Genau das, was ich sagen wollte, konnte aber die Worte nicht finden. Ich habe diesen Teil der Antwort entfernt, da es nicht sehr klar war. –

+0

Ich habe eine Frage: Gibt es in der ursprünglichen Frage einen wesentlichen Unterschied zwischen der Zeile "1" und der Zeile "2"? Meiner Ansicht nach ist es das gleiche, seit es bei der Konstruktion von 'p' in' main' nur um den Rückgabetyp von 'foo' geht, richtig? –

32

unique_ptr verfügt nicht über den traditionellen Kopierkonstruktor. Stattdessen hat es eine "move Konstruktor", die rvalue Referenzen verwendet:

unique_ptr::unique_ptr(unique_ptr && src); 

rvalue Referenz (der Doppelampersand) wird nur an einen R-Wert binden. Deshalb erhalten Sie einen Fehler, wenn Sie versuchen, einen lvalue unique_ptr an eine Funktion zu übergeben. Auf der anderen Seite wird ein Wert, der von einer Funktion zurückgegeben wird, als R-Wert behandelt, sodass der Move-Konstruktor automatisch aufgerufen wird.

By the way, wird diese korrekt funktionieren:

bar(unique_ptr<int>(new int(44)); 

Die temporäre unique_ptr hier ein R-Wert ist.

+8

Ich denke, der Punkt ist mehr, warum 'p' -" offensichtlich "ein _lvalue_ - in der return-Anweisung' return p; 'in der Definition von' foo' als _rvalue_ behandelt werden kann. Ich glaube nicht, dass es einen Nachteil gibt, dass der Rückgabewert der Funktion selbst "verschoben" werden kann. –

+0

Bedeutet das Umbrechen des zurückgegebenen Werts von der Funktion in std :: move, dass es zweimal verschoben wird? –

+3

@RodrigoSalazar std :: move ist nur eine Fantasiedarstellung von einer L-Wert-Referenz (&) zu einer R-Wert-Referenz (&&). Die Fremdverwendung von std :: move auf einem rvalue Verweis wird einfach ein Noop – TiMoch

1

Eine Sache, die ich nicht in anderen Antworten gesehen haben ist Um another answers klarstellen, dass es ein Unterschied zwischen der Rückkehr std :: unique_ptr, die innerhalb einer Funktion erstellt wurden, und eine, die zu dieser Funktion gegeben wurde.

Das Beispiel könnte so aussehen:

class Test 
{int i;}; 
std::unique_ptr<Test> foo1() 
{ 
    std::unique_ptr<Test> res(new Test); 
    return res; 
} 
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t) 
{ 
    // return t; // this will produce an error! 
    return std::move(t); 
} 

//... 
auto test1=foo1(); 
auto test2=foo2(std::unique_ptr<Test>(new Test)); 
+0

Es wird in der [Antwort von Fredoverflow] (/ a/4316948) - deutlich hervorgehoben "** automatische ** Objekt" erwähnt. Eine Referenz (einschließlich einer R-Referenz) ist kein automatisches Objekt. –

+0

@TobySpeight Ok, tut mir leid. Ich denke, mein Code ist nur eine Klarstellung. – v010dya

1

Ich denke, es perfekt ist in Artikel 25 von Scott Meyers' Effective Modern C++ erklärt. Hier ein Auszug:

Der Teil des Standard-blessing der RVO geht weiter zu sagen, dass, wenn die Voraussetzungen für die RVO erfüllt sind, aber Compiler entscheiden, nicht Kopie elision auszuführen, wobei das Objekt zurückgegeben werden muss als ein behandelt werden rvalue. In der Tat erfordert der Standard, dass, wenn der RVO erlaubt ist, entweder eine Eliminierung der Kopie stattfindet oder std::move implizit auf lokale Objekte angewendet wird, die zurückgegeben werden. Hier

, RVO bezieht sich auf Rückgabewert Optimierung und , wenn die Voraussetzungen für die RVO erfüllt sind Mittel, um das lokale Objekt in der Funktion deklariert Rückkehr, die Sie erwarten würden, die RVO zu tun, was auch in Punkt 25 seines Buches unter Bezugnahme auf den Standard erklärt wird (hier beinhaltet das lokale Objekt die temporären Objekte, die durch die return-Anweisung erzeugt werden). Der größte Auszug aus dem Auszug ist entweder Kopie Elision findet statt oder std::move wird implizit auf lokale Objekte angewendet, die zurückgegeben werden. Scott erwähnt in Punkt 25, dass std::move implizit angewendet wird, wenn der Compiler die Kopie nicht auswählt und der Programmierer dies nicht explizit tun sollte.

In Ihrem Fall ist der Code eindeutig ein Kandidat für RVO wie es das lokale Objekt zurückgibt p und die Art der p ist der gleiche wie der Rückgabetyp, der in Kopie elision führt. Und wenn der Compiler entscheidet, die Kopie aus irgendeinem Grund nicht zu verlieren, würde std::move in Zeile 1 gekickt werden.

Verwandte Themen