2016-12-02 2 views
2

Wie im Titel ausgedrückt, fragte ich mich, ob es einen gültigen Grund/Beispiel gibt, für die eine gegebene Klassenmethode, Move Konstruktor und move Zuweisungsoperator, oder free-Funktion sollte nehmen, als Eingabeparameter, a R-Wert-Referenz.Gibt es einen gültigen Grund, für den eine Methode eine R-Wert-Referenz als Eingabe nehmen sollte?

+0

Warum würden Sie freie Funktionen ausschließen, aber keine Mitgliedsfunktionen? Was ist der Unterschied? –

+0

Was ist mit Klassen, die Ressourcen verwalten sollen? Es ist sinnvoll, dass sie in der Lage sind, eine Instanz ihrer verwalteten Ressource nach rvalue-Referenz zu übernehmen, sodass Sie ihnen direkt das Ergebnis einer Operation übergeben können, die diese Ressource zurückgibt, ohne einen Zwischenhändler zu benötigen. –

+1

@ChristianHackl Ich habe keine Free-Funktionen ausgeschlossen, ich habe nur den Move-Konstruktor und den Zuweisungsoperator ausgeschlossen, da es sich um "triviale" Beispiele handelt. – FdeF

Antwort

6

Es gibt zwei Möglichkeiten, ein Senkenargument zu modellieren.

Die erste ist durch Neben Wert annimmt:

void foo(std::string); 

die zweite ist durch rvalue Referenz:

void foo(std::string&&); 

mit der möglichen Variante einschließlich einer const& Überlastungs Arbeit für den Anrufer zu vereinfachen.

inline void foo(std::string const& s){ 
    auto tmp = s; 
    return foo(std::move(tmp)); 
} 

Die take-sink-by-Wert hat einen zusätzlichen Overhead von einem einzigen std::move über sie durch && Einnahme und const& (oder Anrufer erfordern manuell einen nicht temporären Wert kopieren und in sich zu bewegen). Es erfordert nicht die 2. Überlast.

Also, wenn diese eine Bewegung ist es wert, Buchhaltung, unter nehmen Argumente durch const& und && Sparen Sie einen Umzug. Plus, wenn Kopie ist teuer, können Sie es schwierig an der Aufruf-Website machen und so entmutigen.


Aber das ist nicht der einzige Grund. Manchmal möchten Sie feststellen, ob etwas ein R-Wert oder L-Wert ist, und nur kopieren, wenn es ein R-Wert ist.

Angenommen, wir hatten einen Bereichsadapter backwards. backwards nimmt einen geeigneten Bereich (etwas, das Sie for(:) über, und deren Iteratoren kann umgekehrt werden) und gibt einen Bereich, der es rückwärts durchlaufen.

Naiv, alles, was Sie tun müssen, ist begin und end aus dem Quellbereich zu bekommen, dann Reverse-Iteratoren machen und speichern sie und bringt sie von Ihren eigenen begin und end Methoden.Leider

, das bricht:

std::vector<int> get_some_ints(); 

for(int x : backwards(get_some_ints())) { 
    std::cout << x << "\n"; 
} 

da die Lebensdauer der temporären Rückkehr aus get_some_ints wird nicht durch die Schleife for(:) verlängert!

Das for(:) erweitert grob:

{ 
    auto&& __range_expression = backwards(get_some_ints()); 
    auto __it = std::begin(__range_expression); 
    auto __end = std::end(__range_expression); 
    for (; __it != __end; ++__it) { 
    int x = *__it; 
    std::cout << x << "\n"; 
    } 
} 

(Es gibt einige kleine Lügen über den Kindern erzählt, aber es ist nahe genug für diese Diskussion).

Insbesondere diese Zeile:

auto&& __range_expression = backwards(get_some_ints()); 

der Rückgabewert von backwards wird Lebensdauer verlängert; aber die Lebensdauer seiner Argumente ist nicht!

Wenn also backwards eine R const& übernimmt, wird die vector im Hintergrund vor der Schleife zerstört, und die beteiligten Iteratoren sind ungültig.

Also backwardsmuss eine Kopie der vector für den obigen Code, um gültig zu sein. Das ist unsere einzige Chance, den Vektor lange genug zu halten!

Auf der anderen Seite, in einem herkömmlichen Fall:

auto some_ints = get_some_ints(); 
for(int x : backwards(some_ints)) { 
    std::cout << x << "\n"; 
} 

eine zusätzliche Kopie von some_ints Speicherung wäre eine schreckliche Idee und ganz unerwartet.

In diesem Fall muss backwards erkennen, ob sein Argument ein rvalue oder ein lvalue ist, und wenn es ein rvalue ist, muss es kopiert und im Rückgabewert gespeichert werden, und wenn es ein lvalue ist, muss es um entweder nur Iteratoren oder eine Referenz darauf zu speichern.

8

Vermeidung unnötiger Kopien, wenn Werte beibehalten werden. Ich beziehe mich gerne auf diese Funktionen als "Sink-Funktionen". Dies tritt sehr häufig auf, wenn Setter definiert werden.

class A 
{ 
private: 
    std::string _s; 

public: 
    void setS(const std::string& s) { _s = s; } 
    void setS(std::string&& s) { _s = std::move(s); } 
}; 

int main() 
{ 
    A a; 
    std::string s{"some long string ......."}; 

    a.setS(s); // copies and retains `s` 
    a.setS(std::move(s)); // moves and retains `s` 
} 
+0

Ich denke eher als 'std :: string && s' mit' const std :: string & 'zu vergleichen, es ist interessanter zu vergleichen es mit nur "std :: string s" nach Wert. Das ist normalerweise die Alternative, die Leute zu 'std :: string &&' im Sinn haben. –

+0

Siehe auch: http://stackoverflow.com/questions/37935393/pass-by-value-vs-pass-by-rvalue-reference –

+3

@ChrisBeck: Ja, aber es ist nicht so effizient wie mit zwei Überladungen. Es gibt eine Menge Diskussion darüber online: http://blogs.microsoft.co.il/sasha/2014/08/21/c-sink-parameter-passing/ –

1

Manchmal wollen Sie Eigentum an so etwas wie ein std::vector groß nehmen, aber Sie eine Kopie von Unfall vermeiden machen.

von nur ein r-Referenzwert Bereitstellung eines Anrufers überlasten, die eine Kopie übergeben will, muss dies ausdrücklich tun:

class DataHolder { 
    std::vector<double> a; 
    std::vector<int> b; 
public: 
    DataHolder(std::vector<double>&& a, std::vector<int>&& b) : a(a), b(b) {} 
}; 

auto a1 = makeLotsDoubles(); 
auto b1 = makeLotsInts(); 
DataHolder holder(std::move(a1), std::move(b1)); // No copies. Good. 

auto a2 = makeLotsDoubles(); 
auto b2 = makeLotsInts(); 
DataHolder holder(a2, b2) // Forgot to move, compiler error. 

Wenn stattdessen hatten Sie Pass-by-Wert verwendet, dann, wenn Sie vergessen, Verwenden Sie std::move auf einem Lvalue, dann wird eine Kopie erstellt.

0

Es ist nützlich, ein Objekt nicht kopierbar in die Klasse zu übertragen. Einige Objekte haben keinen Kopierkonstruktor, was bedeutet, dass sie nicht als Wert übergeben werden können. Man sieht dies mit RAII, wo das Erstellen einer Kopie eine neue Menge von Ressourcen erhalten würde. Oder im Allgemeinen, wenn der Kopierkonstruktor und/oder Destruktor Nebenwirkungen außerhalb des (de) konstruierten Objekts haben. Das Erstellen einer Kopie ist möglicherweise nicht nur ineffizient, wie das Kopieren einer std :: string, aber ziemlich unmöglich.

Eine Möglichkeit, ein Objekt wie dieses zu übergeben, besteht darin, es als Zeiger zu übergeben. Zum Beispiel:

// Creates a file on construction, deletes on destruction 
class File; 

{  
    // Create files 
    File* logfile1 = new File("name1"); 
    File logfile2("name2"); 

    // Give files to logger to use 
    logger.add_output(logfile1); // OK 
    logger.add_output(&logfile2); // BAD! 
} 

File hat keinen Copykonstruktor, als eine Kopie würde erstellen und die gleiche Datei wie das Original löschen, die keinen Sinn macht. Daher übergeben wir einen Zeiger auf die Datei an das Objekt logger, um das Kopieren zu vermeiden. Mit logfile1 funktioniert es in Ordnung, vorausgesetzt natürlich, dass logger die Datei löscht, wenn es damit fertig ist. Aber logfile2 hat zwei große Probleme. Einer ist, dass logger es nicht löschen kann, da es nicht mit new zugewiesen wurde. Der andere ist, dass logfile2 zerstört wird, die Datei gelöscht wird und der Zeiger auf ihn ungültig wird, gespeichert in logger, wenn logfile2 den Gültigkeitsbereich am Ende des Blocks verlässt.

Wir könnten File einen Move-Konstruktor geben, der den Besitz der Datei auf die Zieldatei überträgt und die Quelldatei "leer" macht. Jetzt können wir den obigen Code so schreiben, dass er funktioniert.

{  
    // Create files 
    File* logfile1 = new File("name1"); 
    File logfile2("name2"); 

    // Give files to logger to use 
    logger.add_output(std::move(*logfile1)); 
    logger.add_output(std::move(logfile2)); 
    logger.add_output(File("name3")); 
    delete logfile1; 
} 

Wir können nun eine File mit neuen oder als lokales Objekt oder sogar als unbenannte pr-Wert schaffen. Die Verwendung von std::move macht auch auf der Abrufseite deutlich, dass das Eigentum an der File auf logger übertragen wird. Wenn die Besitzübertragung des Zeigers nicht klar angezeigt wird, hängt es von logger.add_output() ab, die Semantik zu dokumentieren und diese Dokumentation zu überprüfen.

+0

Ein Objekt ohne Kopierkonstruktor kann nach Wert übergeben werden, wenn es einen Move-Konstruktor hat: http://melpon.org/wandbox/permlink/YYmU43P1ZLKlLJKx –

Verwandte Themen