2015-07-11 7 views
5

Wenn Sie bei get suchen, für std::tuple die Helferfunktion, wird die folgende Überlastung feststellen:Warum Helfer von std :: tuple Rückkehr rvalue Referenz bekommt statt Wert

template< std::size_t I, class... Types > 
constexpr std::tuple_element_t<I, tuple<Types...> >&& 
get(tuple<Types...>&& t); 

Mit anderen Worten, es gibt eine rvalue-Referenz, wenn das Eingabetupel selbst eine rvalue-Referenz ist. Warum nicht als Wert zurückgeben und im Funktionsblock move aufrufen? Mein Argument ist wie folgt: Die Rückgabe von get wird entweder an eine Referenz oder an einen Wert gebunden (es könnte an nichts gebunden sein, aber das sollte kein üblicher Anwendungsfall sein). Wenn es an einen Wert gebunden ist, wird trotzdem eine Move-Konstruktion stattfinden. Du verlierst also nichts, indem du nach Wert zurückkehrst. Wenn Sie an eine Referenz binden, kann die Rückgabe einer R-Referenz tatsächlich unsicher sein. Um ein Beispiel zu zeigen:

struct Hello { 
    Hello() { 
    std::cerr << "Constructed at : " << this << std::endl; 
    } 

    ~Hello() { 
    std::cerr << "Destructed at : " << this << std::endl; 
    } 

    double m_double; 
}; 

struct foo { 
    Hello m_hello; 
    Hello && get() && { return std::move(m_hello); } 
}; 

int main() { 
    const Hello & x = foo().get(); 
    std::cerr << x.m_double; 
} 

Wenn er gestartet wird, dieses Programm druckt:

Constructed at : 0x7ffc0e12cdc0 
Destructed at : 0x7ffc0e12cdc0 
0 

Mit anderen Worten, x ist sofort eine baumelnden Referenz. Wenn Sie nur foo wie folgt geschrieben haben:

struct foo { 
    Hello m_hello; 
    Hello get() && { return std::move(m_hello); } 
}; 

Dieses Problem tritt nicht auf. Außerdem, wenn Sie dann foo wie folgt verwenden:

Hello x(foo().get()); 

Es scheint nicht, wie es einen zusätzlichen Aufwand, ob Sie als Wert zurückgeben, oder rvalue Referenz. Ich habe Code wie diesen getestet, und es scheint, als würde er ganz konsequent nur eine einzige Move-Konstruktion ausführen. Z.B. wenn ich Mitglied hinzufügen:

Hello(Hello &&) { std::cerr << "Moved" << std::endl; } 

Und ich x-Konstrukt, wie oben, mein Programm druckt nur „verschoben“ einmal unabhängig davon, ob ich nach Wert oder rvalue Referenz zurück.

Gibt es einen guten Grund, dass ich vermisse, oder ist das ein Versehen?

Hinweis: Es gibt eine gute verwandte Frage hier: Return value or rvalue reference?. Es scheint so zu sein, dass Value Return im Allgemeinen in dieser Situation vorzuziehen ist, aber die Tatsache, dass es in der STL auftaucht, macht mich neugierig, ob die STL diese Argumentation ignoriert hat oder ob sie besondere Gründe hat, die möglicherweise nicht zutreffend sind allgemein.

Bearbeiten: Jemand hat vorgeschlagen, diese Frage ist ein Duplikat von Is there any case where a return of a RValue Reference (&&) is useful?. Das ist nicht der Fall; Diese Antwort schlägt vor, dass die Rückgabe von R-Werten als eine Möglichkeit zum Kopieren von Datenelementen verwendet wird. Wie ich oben ausführlich besprochen habe, wird das Kopieren weggelassen, ob Sie nach Wert oder R-Wert-Referenz zurückgeben, vorausgesetzt, Sie rufen zuerst move.

+0

Dies ist nicht entfernt ein Duplikat. Meine Antwort beschreibt explizit und im Detail, wie die Rückgabe nach Wert und das Aufrufen von std :: move im Voraus den gleichen Effekt des Verschiebens von einem Datenelement erzielt. Meine Frage bezieht sich auf die Unterschiede in den beiden Ansätzen. Die Frage, die ich selbst anführte, hängt mehr mit meiner Frage zusammen als mit der, die Sie zitiert haben. Bitte lesen Sie aufmerksamer, bevor Sie etwas als Duplikat bezeichnen. –

+0

Wenn Sie nach Wert zurückgeben, erstellen Sie eine Kopie. Wenn Sie es verschieben, verschieben Sie eine Kopie. Wenn Sie eine r-Wert-Referenz zurückgeben, können Sie sie entweder verschieben, dann wird das ursprüngliche Objekt verschoben oder es wird nicht mit move sematics behandelt, dann verhält es sich wie eine normale Referenz. Der Punkt ist, dass Sie das Kopieren nicht erzwingen, wenn Sie eine r-Wert-Referenz zurückgeben. –

+0

Warum müssen Sie die Konstruierbarkeit erreichen? – Columbo

Antwort

4

Ihr Beispiel, wie dies verwendet werden kann, um eine hängende Referenz zu erstellen, ist sehr interessant, aber es ist wichtig, die richtige Lektion aus dem Beispiel zu lernen.

ein viel einfacheres Beispiel betrachten, die keine && überall hat:

const int &x = vector<int>(1) .front(); 

.front() gibt ein & -Referenz auf das erste Element des neuen konstruierten Vektor. Der Vektor ist natürlich sofort zerstört und Sie haben eine freie Referenz.

Die Lektion zu lernen ist, dass die Verwendung einer const-Referenz im Allgemeinen nicht die Lebensdauer verlängert. Es verlängert die Lebensdauer von Non-Referenzen. Wenn die rechte Seite von = eine Referenz ist, dann müssen Sie die Verantwortung für die Lebensdauer selbst übernehmen.

Dies war schon immer der Fall, daher würde es keinen Sinn ergeben, etwas anderes zu tun. tuple::get darf eine Referenz zurückgeben, genauso wie vector::front immer gewesen ist.

Sie sprechen über verschieben und kopieren Konstruktoren und über Geschwindigkeit. Die schnellste Lösung ist, keinerlei Konstruktoren zu verwenden. Man stelle sich eine Funktion zwei Vektoren verketten:

vector<int> concat(const vector<int> &l_, const vector<int> &r) { 
    vector<int> l(l_); 
    l.insert(l.end(), r.cbegin(), r.cend()); 
    return l; 
} 

Dies würde eine optimierte zusätzliche Überlast ermöglichen:

vector<int>&& concat(vector<int>&& l, const vector<int> &r) { 
    l.insert(l.end(), r.cbegin(), r.cend()); 
    return l; 
} 

Diese Optimierung hält die Anzahl der Konstruktionen auf ein Minimum

vector<int> a{1,2,3}; 
    vector<int> b{3,4,5}; 
    vector<int> c = concat(
    concat(
     concat(
      concat(vector<int>(), a) 
     , b) 
     , a 
    , b); 

Die letzte Zeile , mit vier Aufrufen an concat, wird nur zwei Konstruktionen haben: Der Startwert (vector<int>()) und das Move-Konstrukt in c. Sie könnten dort 100 verschachtelte Aufrufe an concat ohne zusätzliche Konstruktionen haben.

So kann die Rückkehr von && schneller sein. Ja, Bewegungen sind schneller als Kopien, aber noch schneller, wenn Sie beides vermeiden können.

Zusammenfassend ist es für die Geschwindigkeit getan. Erwägen Sie die Verwendung einer verschachtelten Serie von get auf einem Tupel innerhalb eines Tupels innerhalb eines Tupels. Außerdem ermöglicht es das Arbeiten mit Typen, die weder Konstruktoren zum Kopieren noch zum Verschieben enthalten.

Und das bringt keine neuen Risiken in Bezug auf die Lebensdauer. Das vector<int>().front() "Problem" ist kein neues.

+2

Ich muss etwas mehr Zeit mit deiner verbringen antworte, um es vollständig aufzunehmen. Aber bedenken Sie, dass Ihr Beispiel mit vector.front nicht großzügig wäre, wenn es eine Überladung T front() && gäbe. –

+2

Ich dachte daran, das zu erwähnen. Ja, das Komitee sollte '&' und '&&' in den Standard einfügen, genauso wie Ihr Beispiel in Ihrem Kommentar. Aber ich denke, das würde die Kompatibilität zerstören. Außerdem sollten alle Methoden "&" standardmäßig sein, aber auch das wäre eine ziemlich dramatische Änderung der Sprache. –

+0

Verallgemeinern, wenn eine Funktion eines der Argumente oder ein Unterobjekt eines Arguments zurückgibt und dieses Argument '&&' ist, dann sehe ich keinen Grund, nicht immer durch '&&' zurückzukommen. Gehe ich hier zu weit? –