it-swarm.com.de

Rückgabe von unique_ptr von Funktionen

unique_ptr<T> Erlaubt keine Kopierkonstruktion, sondern unterstützt die Verschiebungssemantik. Ich kann jedoch einen unique_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 kompiliert und funktioniert wie beabsichtigt. Wie kommt es also, dass Zeile 1 Den Kopierkonstruktor nicht aufruft und zu Compilerfehlern führt? Wenn ich stattdessen die Zeile 2 Verwenden müsste, wäre dies sinnvoll (die Verwendung der Zeile 2 Funktioniert ebenfalls, ist jedoch nicht erforderlich).

Ich weiß, dass C++ 0x diese Ausnahme für 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 dies implementiert wird, ob es sich um eine spezielle Klausel im Compiler handelt oder ob es eine andere Klausel in der Sprachspezifikation gibt, die dies ausnutzt.

316
Praetorian

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 Auswahl von Kopier-/Verschiebungsoperationen, genannt Kopierauswahl , ist in einer return-Anweisung in einer Funktion mit einem Klassenrückgabetyp zulässig, wenn der Ausdruck der Name eines nichtflüchtigen automatischen Objekts ist mit demselben cv-unqualifizierten Typ wie die Funktion Rückgabetyp [...]

Wenn die Kriterien für die Auswahl einer Kopieroperation erfüllt sind und das zu kopierende Objekt durch einen I-Wert gekennzeichnet ist, wird zuerst eine Überladungsauflösung zur Auswahl des Konstruktors für die Kopie ausgefü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 die Standardauswahl sein sollte, da ein benannter Wert in der return-Anweisung im schlimmsten Fall, dh ohne Elisierungen in C++ 11, C++ 14 und C++ 17, behandelt wird als ein Wert. So kompiliert zum Beispiel die folgende Funktion mit dem -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

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

199
fredoverflow

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 entfernen, ruft einen Verschiebungskonstruktor auf, wenn keine Kopien entfernt werden können, ruft einen Kopierkonstruktor auf, wenn keine Verschiebungen möglich sind, und kompiliert nicht, wenn keine Kopien möglich sind.

Wenn Sie eine Funktion hätten, die std::unique_ptr Als Argument akzeptiert, könnten Sie p nicht übergeben. Sie müssten den move-Konstruktor explizit 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
    return 0;
}
95

unique_ptr verfügt nicht über den herkömmlichen Kopierkonstruktor. Stattdessen hat es einen "Verschiebungskonstruktor", der R-Wert-Referenzen verwendet:

unique_ptr::unique_ptr(unique_ptr && src);

Ein R-Wert-Verweis (das doppelte Et-Zeichen) wird nur an einen R-Wert gebunden. Aus diesem Grund erhalten Sie eine Fehlermeldung, wenn Sie versuchen, 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 funktioniert übrigens richtig:

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

Das temporäre unique_ptr ist hier ein r-Wert.

37

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

Der Teil des Standardsegens des RVO besagt weiter, dass, wenn die Bedingungen für das RVO erfüllt sind, die Compiler jedoch keine Kopierentfernung durchführen, das zurückgegebene Objekt als R-Wert behandelt werden muss. Tatsächlich verlangt der Standard, dass bei Erlaubnis des RVO entweder eine Kopierelision stattfindet oder std::move wird implizit auf lokale Objekte angewendet, die zurückgegeben werden.

Hier bezieht sich [~ # ~] rvo [~ # ~] auf Rückgabewertoptimierung und wenn die Bedingungen für die RVO erfüllt sind bedeutet, dass das in der Funktion deklarierte lokale Objekt zurückgegeben wird, von dem Sie erwarten würden, dass es [~ # ~] rvo [~ # ~] ausführt, was auch in Punkt 25 gut erklärt wird seines Buches unter Bezugnahme auf den Standard (hier das local object schließt die temporären Objekte ein, die durch die return-Anweisung erzeugt wurden). Der größte Abzug aus dem Auszug ist entweder wird kopiert oder std::move wird implizit auf lokale Objekte angewendet, die zurückgegeben werden. Scott erwähnt in Punkt 25, dass std::move wird implizit angewendet, wenn der Compiler die Kopie nicht löschen möchte 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 p und den Typ von p ist derselbe wie der Rückgabetyp, was zu einer Kopierentscheidung führt. Und wenn der Compiler die Kopie aus irgendeinem Grund nicht auswählt, std::move wäre in die Warteschlange getreten 1.

10
David Lee

Eine Sache, die ich in anderen Antworten nicht gesehen habe, ist Um zu verdeutlichen , dass es einen Unterschied zwischen der Rückgabe von std :: unique_ptr, das in einer Funktion erstellt wurde, und einer Rückgabe, die dieser Funktion zugewiesen wurde, gibt .

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));
2
v010dya