Rückgabe von unique_ptr von Funktionen

367

unique_ptr<T>erlaubt keine Kopierkonstruktion, sondern unterstützt die Verschiebungssemantik. Dennoch kann ich a zurückgebenunique_ptr<T> von einer Funktion zurückgeben und den zurückgegebenen Wert einer Variablen zuweisen.

#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 wird kompiliert und funktioniert wie vorgesehen. Wie kommt es also, dass die Zeile 1den Kopierkonstruktor nicht aufruft und zu Compilerfehlern führt? Wenn ich Linie benutzen müsste2 stattdessen verwenden müsste, wäre dies sinnvoll (die Verwendung von Leitung 2funktioniert auch, aber wir müssen dies nicht tun).

Ich weiß, dass C ++ 0x diese Ausnahme zulässt, unique_ptrda 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 gespannt, wie dies implementiert wird. Ist es speziell im Compiler enthalten oder gibt es eine andere Klausel in der Sprachspezifikation, die dies ausnutzt?

Prätorianer
quelle
Wenn Sie eine Factory- Methode implementieren würden, würden Sie hypothetisch 1 oder 2 bevorzugen, um die Factory-Ausgabe zurückzugeben? Ich gehe davon aus, dass dies die häufigste Verwendung von 1 ist, da Sie bei einer ordnungsgemäßen Fabrik tatsächlich möchten, dass das Eigentum an dem konstruierten Objekt auf den Anrufer übergeht.
Xharlie
7
@ Xharlie? Sie beide geben das Eigentum an der unique_ptr. Die ganze Frage ist, dass 1 und 2 zwei verschiedene Wege sind, um dasselbe zu erreichen.
Prätorianer
In diesem Fall findet das RVO auch in c ++ 0x statt. Die Zerstörung des unique_ptr-Objekts erfolgt einmal, nachdem die mainFunktion beendet wurde, jedoch nicht, wenn die Funktion beendet wird foo.
Ampawd

Antworten:

218

Gibt es eine andere Klausel in der Sprachspezifikation, die dies ausnutzt?

Ja, siehe 12.8 §34 und §35:

Wenn bestimmte Kriterien erfüllt sind, darf eine Implementierung die Kopier- / Verschiebungskonstruktion eines Klassenobjekts [...] weglassen. Diese Elision von Kopier- / Verschiebeoperationen, die als Kopierelision bezeichnet wird , ist in einer return-Anweisung in [...] zulässig Eine Funktion mit einem Klassenrückgabetyp, wenn der Ausdruck der Name eines nichtflüchtigen automatischen Objekts ist mit demselben cv-unqualifizierten Typ wie der Funktionsrückgabetyp ist [...]

Wenn die Kriterien für die Elision einer Kopieroperation erfüllt sind und das zu kopierende Objekt durch einen l-Wert gekennzeichnet ist, wird zuerst eine Überladungsauflösung zur Auswahl des Konstruktors für die Kopie durchgeführt, als ob das Objekt durch einen r-Wert gekennzeichnet wäre .


Ich wollte nur noch einen Punkt hinzufügen, dass die Rückgabe nach Wert hier die Standardauswahl sein sollte, da im schlimmsten Fall ein benannter Wert in der return-Anweisung, dh ohne Elisionen in C ++ 11, C ++ 14 und C ++ 17, behandelt wird als ein Wert. So wird beispielsweise die folgende Funktion mit dem -fno-elide-constructorsFlag kompiliert

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

Wenn das Flag beim Kompilieren gesetzt ist, finden in dieser Funktion zwei Züge (1 und 2) statt und später ein Zug (3).

Fredoverflow
quelle
@juanchopanza Meinst du im Wesentlichen, dass dies foo()tatsächlich auch zerstört werden wird (wenn es nichts zugewiesen wurde), genau wie der Rückgabewert innerhalb der Funktion, und daher ist es sinnvoll, dass C ++ dabei einen Verschiebungskonstruktor verwendet unique_ptr<int> p = foo();?
7 Kühe
1
Diese Antwort besagt, dass eine Implementierung etwas tun darf ... es heißt nicht, dass dies erforderlich ist. Wenn dies also der einzige relevante Abschnitt wäre, würde dies bedeuten, dass das Verlassen auf dieses Verhalten nicht portabel ist. Aber ich denke nicht, dass das richtig ist. Ich neige dazu zu glauben, dass die richtige Antwort mehr mit dem Verschiebungskonstruktor zu tun hat, wie in der Antwort von Nikola Smiljanic und Bartosz Milewski beschrieben.
Don Hatch
6
@DonHatch Es heißt, dass es in diesen Fällen "erlaubt" ist, eine Kopier- / Verschiebungselision durchzuführen, aber wir sprechen hier nicht über Kopierelision. Es ist der zweite zitierte Absatz, der hier gilt, der sich auf die Regeln für die Kopierelision stützt, aber nicht die Kopierelision selbst ist. Der zweite Absatz enthält keine Unsicherheit - er ist vollständig tragbar.
Joseph Mansfield
@juanchopanza Mir ist klar, dass dies jetzt 2 Jahre später ist, aber haben Sie immer noch das Gefühl, dass dies falsch ist? Wie ich im vorherigen Kommentar erwähnt habe, geht es hier nicht um die Elision von Kopien. Es kommt einfach so vor, dass in den Fällen, in denen die Kopierentfernung angewendet werden kann (auch wenn sie nicht angewendet werden kann std::unique_ptr), es eine spezielle Regel gibt, Objekte zuerst als rWerte zu behandeln. Ich denke, das stimmt völlig mit dem überein, was Nikola geantwortet hat.
Joseph Mansfield
1
Warum wird bei meinem Nur-Verschieben-Typ (entfernter Kopierkonstruktor) immer noch der Fehler "Versuch, auf eine gelöschte Funktion zu verweisen" angezeigt, wenn ich ihn genau wie in diesem Beispiel zurückgebe?
DrumM
104

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

Wenn Sie eine Funktion hätten, die std::unique_ptrals Argument akzeptiert , könnten Sie p nicht an sie übergeben. Sie müssten den Verschiebungskonstruktor explizit aufrufen, aber in diesem Fall sollten Sie die Variable p nach dem Aufruf von nicht verwenden bar().

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
    return 0;
}
Nikola Smiljanić
quelle
3
@Fred - na ja, nicht wirklich. Obwohl dies pnicht vorübergehend ist, ist das Ergebnis dessen foo(), was zurückgegeben wird, Folgendes: Somit ist es ein Wert und kann verschoben werden, was die Zuordnung mainermöglicht. Ich würde sagen, Sie haben sich geirrt, außer dass Nikola diese Regel dann auf sich pselbst anzuwenden scheint, was ein Fehler ist.
Edward Strange
Genau das, was ich sagen wollte, aber die Worte nicht finden konnte. Ich habe diesen Teil der Antwort entfernt, da er nicht sehr klar war.
Nikola Smiljanić
Ich habe eine Frage: Gibt es in der ursprünglichen Frage einen wesentlichen Unterschied zwischen Linie 1und Linie 2? Meiner Ansicht nach ist es die gleiche , da bei der Konstruktion pin main, es nur Sorgen über die Art des Rückgabetypen foo, nicht wahr?
Hongxu Chen
1
@HongxuChen In diesem Beispiel gibt es absolut keinen Unterschied, siehe das Zitat aus dem Standard in der akzeptierten Antwort.
Nikola Smiljanić
Tatsächlich können Sie p danach verwenden, solange Sie es zuweisen. Bis dahin können Sie nicht versuchen, auf den Inhalt zu verweisen.
Alan
38

unique_ptr hat nicht den traditionellen Kopierkonstruktor. Stattdessen hat es einen "Verschiebungskonstruktor", der rWertreferenzen verwendet:

unique_ptr::unique_ptr(unique_ptr && src);

Eine r-Wert-Referenz (das doppelte kaufmännische Und) bindet nur an einen r-Wert. Aus diesem Grund wird eine Fehlermeldung angezeigt, wenn Sie versuchen, einen lvalue unique_ptr an eine Funktion zu übergeben. Andererseits wird ein Wert, der von einer Funktion zurückgegeben wird, als r-Wert behandelt, sodass der Verschiebungskonstruktor automatisch aufgerufen wird.

Das wird übrigens richtig funktionieren:

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

Das temporäre unique_ptr hier ist ein rWert.

Bartosz Milewski
quelle
8
Ich denke, der Punkt ist mehr, warum kann p- "offensichtlich" ein l-Wert - in der return-Anweisung in der Definition von als r- Wert behandelt werden . Ich glaube nicht, dass es ein Problem mit der Tatsache gibt, dass der Rückgabewert der Funktion selbst "verschoben" werden kann. return p;foo
CB Bailey
Bedeutet das Umschließen des zurückgegebenen Werts von der Funktion in std :: move, dass er zweimal verschoben wird?
3
@RodrigoSalazar std :: move ist nur eine ausgefallene Umwandlung von einer Wertreferenz (&) in eine Wertreferenz (&&). Die fremde Verwendung von std :: move auf einer rvalue-Referenz ist einfach ein Noop
TiMoch
13

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

Der Teil des Standard-Segens für die RVO führt weiter aus, dass das zurückgegebene Objekt als rWert behandelt werden muss, wenn die Bedingungen für die RVO erfüllt sind, die Compiler jedoch keine Kopierelision durchführen. Tatsächlich verlangt der Standard, dass, wenn die RVO zulässig ist, entweder eine Kopierelision stattfindet oder std::moveimplizit auf lokale Objekte angewendet wird, die zurückgegeben werden.

Hier RVO bezieht sich auf die Wertoptimierung zurückkehren , und wenn die Bedingungen für die RVO erfüllt sind Mittel , um das lokale Objekt deklariert innerhalb der Funktion zurückkehrt , dass man erwarten würde das tun RVO , die auch schön ist erklärt in Artikel 25 seines Buches unter Bezugnahme auf der Standard (hier enthält das lokale Objekt die temporären Objekte, die von der return-Anweisung erstellt wurden). Die größte Auswirkung auf den Auszug besteht darin, dass entweder eine Kopierelision stattfindet oder std::moveimplizit auf lokale Objekte angewendet wird, die zurückgegeben werden . Scott erwähnt in Punkt 25, dassstd::move implizit angewendet wird, wenn der Compiler die Kopie nicht entfernt und der Programmierer dies nicht explizit tun sollte.

In Ihrem Fall ist der Code eindeutig ein Kandidat für RVO, da er das lokale Objekt zurückgibt pund der Typ von pdem gleichen wie der Rückgabetyp ist, was zu einer Kopierelision führt. Und wenn der Compiler sich aus irgendeinem Grund dafür entscheidet, die Kopie nicht zu entfernen, std::movewäre er in die Reihe getreten 1.

David Lee
quelle
5

Eine Sache, die ich in anderen Antworten nicht gesehen habe, istUm eine andere Antwort zu verdeutlichen , gibt es einen Unterschied zwischen der Rückgabe von std :: unique_ptr, die innerhalb einer Funktion erstellt wurde, und einer, die dieser Funktion zugewiesen wurde.

Das Beispiel könnte folgendermaßen 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));
v010dya
quelle
Es wird in der Antwort von Fredoverflow erwähnt - deutlich hervorgehobenes " automatisches Objekt". Eine Referenz (einschließlich einer rWert-Referenz) ist kein automatisches Objekt.
Toby Speight
@TobySpeight Ok, sorry. Ich denke mein Code ist dann nur eine Klarstellung.
v010dya