2012-03-31 9 views
4

Angenommen, ich möchte eine Funktion implementieren, die ein Objekt verarbeiten und ein neues (möglicherweise geändertes) Objekt zurückgeben soll. Ich möchte das in C + 11 so effizient wie möglich machen. Die Umgebung ist wie folgt:Effiziente Verwendung der Bewegungssemantik zusammen mit (N) RVO

class Object { 
    /* Implementation of Object */ 
    Object & makeChanges(); 
}; 

Die Alternativen, die mir in den Sinn kommen, sind:

// First alternative: 
Object process1(Object arg) { return arg.makeChanges(); } 
// Second alternative: 
Object process2(Object const & arg) { return Object(arg).makeChanges(); } 
Object process2(Object && arg) { return std::move(arg.makeChanges()); } 
// Third alternative: 
Object process3(Object const & arg) { 
    Object retObj = arg; retObj.makeChanges(); return retObj; 
} 
Object process3(Object && arg) { std::move(return arg.makeChanges()); } 

Hinweis: Ich möchte eine Umwicklungsfunktion wie process() verwenden, da sie eine andere Arbeit tun, und ich möchte so viel Code wie möglich wiederverwenden.

Updates:

verwendete ich die makeChanges() mit der gegebenen Unterschrift, da die Objekte, mit denen ich zu tun Methoden liefert mit dieser Art von Signatur. Ich nehme an, dass sie das für die Methodenverkettung benutzt haben. Ich habe auch die beiden genannten Syntaxfehler behoben. Danke, dass du sie rausgebracht hast. Ich habe auch eine dritte Alternative hinzugefügt, und ich werde die folgende Frage ruhen lassen.

Versuchen Sie diese mit clang [d. Object obj2 = process(obj);] ergibt folgendes:

Erste Option ruft zwei Aufrufe des Kopierkonstruktors auf; eine für die Weitergabe des Arguments und eine für die Rückkehr. Man könnte stattdessen return std::move(..) sagen und einen Aufruf des Kopierkonstruktors und einen Aufruf des Move-Konstruktors haben. Ich verstehe, dass RVO einen dieser Aufrufe nicht loswerden kann, weil es sich um den Funktionsparameter handelt.

In der zweiten Option haben wir noch zwei Aufrufe an den Kopierkonstruktor. Hier machen wir einen expliziten Anruf und man wird während der Rückkehr gemacht. Ich hatte damit gerechnet, dass RVO eingreifen und das Letztere loswerden würde, da das Objekt, das wir zurückgeben, ein anderes Objekt als das Argument ist. Es ist jedoch nicht passiert.

In der dritten Option haben wir nur einen Aufruf an den Copy-Konstruktor und das ist der explizite. (N) RVO eliminiert den Aufruf des Kopierkonstruktors, den wir für die Rückgabe ausführen würden.

Meine Fragen sind:

  1. (beantwortet) Warum Kick RVO in der letzten Option und nicht die zweite?
  2. Gibt es einen besseren Weg, dies zu tun?
  3. Hätten wir in einer temporären, 2. und 3. Optionen übergeben würde einen Move-Konstruktor aufrufen, während Sie zurückkehren. Ist es möglich, das mit (N) RVO zu eliminieren?

Vielen Dank!

+2

Warum würde 'makeChanges' ein' Objekt & 'zurückgeben? Es sollte entweder nichts zurückgeben und eine mutierende Funktion sein oder es sollte "const" sein und ein neues Objekt nach Wert zurückgeben. Derzeit, weil es _nicht_ 'const' ist, sind Ihre erste und zweite aufgelistete Optionen nicht einmal kompilierbar, weil Sie eine nicht-konstante Elementfunktion für ein konstantes Objekt aufrufen. Effektiv macht dies Ihre Frage ziemlich unsinnig ohne eine Begründung für die aktuelle Signatur "makeChanges". – ildjarn

+0

@ildjarn: Danke für die Kommentare. Ich habe Änderungen vorgenommen und die Frage gestellt. Ich denke, mir ist immer noch nicht klar, wie/wann RVO ansetzt. Ich würde gerne deine Ideen und Vorschläge hören. – iheap

Antwort

16

Ich mag zu messen, so dass ich diese Object up:

#include <iostream> 

struct Object 
{ 
    Object() {} 
    Object(const Object&) {std::cout << "Object(const Object&)\n";} 
    Object(Object&&) {std::cout << "Object(Object&&)\n";} 

    Object& makeChanges() {return *this;} 
}; 

Und ich theoretisiert, dass einige Lösungen verschiedene Antworten für XValues ​​und prvalues ​​geben kann (die beide rvalues ​​sind). Und so habe ich beschlossen, sie beide zu testen (zusätzlich zu lvalues):

Object source() {return Object();} 

int main() 
{ 
    std::cout << "process lvalue:\n\n"; 
    Object x; 
    Object t = process(x); 
    std::cout << "\nprocess xvalue:\n\n"; 
    Object u = process(std::move(x)); 
    std::cout << "\nprocess prvalue:\n\n"; 
    Object v = process(source()); 
} 

Jetzt ist es eine einfache Sache, alle Ihre Möglichkeiten versuchen, die von anderen beigetragen, und ich warf einen in mir:

#if PROCESS == 1 

Object 
process(Object arg) 
{ 
    return arg.makeChanges(); 
} 

#elif PROCESS == 2 

Object 
process(const Object& arg) 
{ 
    return Object(arg).makeChanges(); 
} 

Object 
process(Object&& arg) 
{ 
    return std::move(arg.makeChanges()); 
} 

#elif PROCESS == 3 

Object 
process(const Object& arg) 
{ 
    Object retObj = arg; 
    retObj.makeChanges(); 
    return retObj; 
} 

Object 
process(Object&& arg) 
{ 
    return std::move(arg.makeChanges()); 
} 

#elif PROCESS == 4 

Object 
process(Object arg) 
{ 
    return std::move(arg.makeChanges()); 
} 

#elif PROCESS == 5 

Object 
process(Object arg) 
{ 
    arg.makeChanges(); 
    return arg; 
} 

#endif 

Die folgende Tabelle fasst meine Ergebnisse zusammen (mit clang -std = C++ 11). Die erste Zahl ist die Anzahl der Kopier Konstruktionen und die zweite Zahl ist die Anzahl der Umzug Konstruktionen:

+----+--------+--------+---------+ 
| | lvalue | xvalue | prvalue | legend: copies/moves 
+----+--------+--------+---------+ 
| p1 | 2/0 | 1/1 | 1/0 | 
+----+--------+--------+---------+ 
| p2 | 2/0 | 0/1 | 0/1 | 
+----+--------+--------+---------+ 
| p3 | 1/0 | 0/1 | 0/1 | 
+----+--------+--------+---------+ 
| p4 | 1/1 | 0/2 | 0/1 | 
+----+--------+--------+---------+ 
| p5 | 1/1 | 0/2 | 0/1 | 
+----+--------+--------+---------+ 

process3 sieht aus wie die beste Lösung für mich. Es erfordert jedoch zwei Überladungen. Eine zur Verarbeitung von Werten und eine zur Verarbeitung von Werten. Wenn dies aus irgendeinem Grund problematisch ist, erledigen die Lösungen 4 und 5 die Aufgabe mit nur einer Überladung auf Kosten von 1 zusätzlichen Verschiebungskonstruktion für glvalues ​​(lvalues ​​und xvalues). Es ist ein Urteilsspruch, ob man eine zusätzliche Move-Konstruktion bezahlen will, um Überladung zu sparen (und es gibt keine richtige Antwort).

(beantwortet) Warum tritt RVO in die letzte Option und nicht in die Sekunde?

Für RVO zu treten, muss der return-Anweisung aussehen:

return arg; 

Wenn Sie komplizieren, dass mit:

return std::move(arg); 

oder:

return arg.makeChanges(); 

dann RVO wird gehemmt.

Gibt es einen besseren Weg, dies zu tun?

Meine Favoriten sind p3 und p5. Meine Präferenz von p5 gegenüber p4 ist lediglich stilistisch.Ich scheue mich davor, move auf die return Anweisung zu setzen, wenn ich weiß, dass es automatisch aus Angst vor versehentlichem Hemmen von RVO angewendet wird. In p5 ist RVO jedoch sowieso keine Option, obwohl die return-Anweisung eine implizite Bewegung erhält. Also sind p5 und p4 wirklich gleichwertig. Wählen Sie Ihren Stil.

Hätten wir in einem temporären, 2. und 3. Optionen übergeben würde eine Bewegung Konstruktor aufrufen, während der Rückkehr. Ist es möglich, das mit (N) RVO zu beseitigen?

Die Spalte "prvalue" vs. "xvalue" adressiert diese Frage. Einige Lösungen fügen eine zusätzliche Move-Konstruktion für xvalues ​​hinzu und manche nicht.

+0

Große Antwort. Danke vielmals. – iheap

2

Keine der Funktionen, die Sie anzeigen, weist signifikante Rückgabewertoptimierungen für ihre Rückgabewerte auf.

makeChanges gibt eine Object& zurück. Daher muss es in einen Wert kopiert werden, da Sie es zurückgeben. Die ersten beiden machen also immer eine Kopie des zurückzugebenden Wertes. In Bezug auf die Anzahl der Kopien erstellt die erste zwei Kopien (eine für den Parameter, eine für den Rückgabewert). Der zweite erzeugt zwei Kopien (eine explizit in der Funktion, eine für den Rückgabewert.

Die dritte sollte nicht einmal kompilieren, da Sie eine l-Wert-Referenz nicht implizit in eine r-Wert-Referenz konvertieren können .

Also wirklich, tu das nicht. Wenn Sie ein Objekt übergeben möchten, und ändern Sie es in-situ, dann ist dies nur tun:

Object &process1(Object &arg) { return arg.makeChanges(); } 

Dies modifiziert das bereitgestellte Objekt. Kein Kopieren oder irgendetwas. Zugegeben, man könnte sich fragen, warum process1 keine Mitgliedfunktion oder etwas ist, aber das macht nichts.

+0

Danke für die Antwort. Ich verstehe, warum die zweite Alternative jetzt noch eine Kopie benötigt. Allerdings verstehe ich nicht, wie Ihre vorgeschlagene Lösung passt. Die Funktion, die ich möchte, muss ein neues "Objekt" generieren. – iheap

0

Der schnellste Weg dies zu tun ist - wenn das Argument Lvalue ist, dann kopieren Sie es und geben Sie diese Kopie zurück - wenn rvalue, dann verschieben Sie es. Die Rückgabe kann immer verschoben werden oder RVO/NRVO angewendet haben. Dies ist leicht zu erreichen.

Object process1(Object arg) { 
    return std::move(arg.makeChanges()); 
} 

Dies ist sehr ähnlich zu den kanonischen C++ 11 Formen vieler Arten von Operator Überladungen.

+0

Wenn ich versuchte, sehe ich, dass Move Constructor immer aufgerufen wird. RVO tritt nicht ein. Angesichts der ersten Antwort dachte ich, dies sei aufgrund der Tatsache, dass 'std :: move' uns einen (r-Wert) Verweis gibt, aber wir geben den Wert zurück. Willst du damit sagen, dass RVO eingetreten sein sollte? Wenn nicht, scheint 'process3' etwas effizienter zu sein (kein Aufruf zum Verschieben des Konstruktors). – iheap

+0

@iheap: RVO/NRVO kann für diesen Fall absolut einspringen. Die genauen Situationen, in denen es angewendet werden kann, sind bestenfalls implementierungsabhängig. Alles, was Sie tun können, ist es zu erlauben - es liegt an dem Compiler, es tatsächlich zu tun. – Puppy

+3

@DeadMG: Dafür gibt es kein RVO, weil der Rückgabeausdruck eine r-Wert-Referenz ist. Da der Rückgabetyp "Objekt" ist, muss der Rückgabewert aus der r-Wert-Referenz erstellt werden. RVO ist nicht dasselbe wie ein temporäres zu bewegen; Das bedeutet, dass die Kopie vollständig aus der Funktion entfernt wird. Und das kannst du mit dem Code, den du hier angegeben hast, nicht machen. –