2014-02-06 8 views
7

Beispiel:Kann die Rückgabe eines geschweiften eingeschlossenen Initialisierers zu einer Kopie in C++ führen?

struct s { int a; }; 

s func() { return {42}; } 

int main() { 
    s new_obj = func(); // line 6 
    (void) new_obj; 
    return 0; 
} 

Dies funktioniert. Nun, was passiert, wenn wir davon ausgehen, dass unser Compiler kein RVO hat?

  1. func gibt eine Struktur von s, so {42} muss s umgewandelt werden, wird dann zurückgeführt und schließlich auf new_obj in Zeile kopiert 6.
  2. func gibt eine Initialisiererliste, so dass eine tiefe Kopie unmöglich ist.

Was sagt die Sprache? Kannst du einen Beweis geben?

Hinweis: Ich weiß, dass dies in diesem Beispiel nicht sinnvoll erscheint, aber für die Rückgabe sehr großer, konstanter Größe-s möchte ich nicht auf RVO verlassen.

+4

"Das ist vielleicht gut zu wissen" - außer dass Ihr Compiler ** tatsächlich RVO ausführt. –

+0

@KonradRudolph Was, wenn aus irgendeinem Grund RVO in einem Beispiel nicht verwendet werden kann. Und selbst wenn - ich dachte, dass die C++ - Sprache RVO nicht garantiert. – Johannes

+0

@KonradRudolph Sollte es Objekt nicht bewegen, anstatt RVO anzuwenden (oder möglicherweise zu überspringen)? –

Antwort

5

Betrachten Sie das folgende Beispiel:

#include <iostream> 

struct foo { 
    foo(int) {} 
    foo(const foo&) { std::cout << "copy\n"; } 
    foo(foo&&)  { std::cout << "move\n"; } 
}; 

foo f() { 
    //return 42; 
    return { 42 }; 
} 

int main() { 
    foo obj = f(); 
    (void) obj; 
} 

Wenn mit gcc 4.8.1 mit -fno-elide-constructors zusammengestellt RVO die Ausgabe zu verhindern, ist

move 

Wenn in f die return-Anweisung ohne geschweifte Klammern verwendet wird, dann , dann ist die Ausgabe

move 
move 

Wi th kein RVO, was passiert ist folgendes. f muss ein temporäres Objekt vom Typ foo, nennen wir es ret, erstellt werden, um zurückgegeben werden.

Wenn return { 42 }; verwendet wird, dann ret ist direkte aus dem Wert initialisiert 42. Also wurde bisher kein Konstruktor für Kopieren/Verschieben aufgerufen.

Wenn return 42; verwendet wird, dann vorübergehend eine andere, nennen wir es tmp direkt von 42 initialisiert und tmp bewegt wird ret zu erstellen. Daher wurde bisher ein Move-Konstruktor aufgerufen. (Beachten Sie, dass tmp ein R-Wert ist und foo hat einen Umzug Konstruktor. Wenn es keine Bewegung Konstruktor war, dann würde die Kopie Konstruktor aufgerufen werden.)

Jetzt ret ist ein R-Wert und verwendet wird obj zu initialisieren. Daher wird der Bewegungsbaustein aufgerufen, um von ret zu obj zu wechseln. (Wiederum könnte unter gewissen Umständen der Kopierkonstruktor stattdessen aufgerufen werden.) Also entweder eine (für return { 42 };) oder zwei (für return 42;) Bewegungen passieren.

Wie ich in meinem Kommentar zu der Frage des OP sagte, ist dieser Beitrag sehr relevant: construction helper make_XYZ allowing RVO and type deduction even if XZY has noncopy constraint. Vor allem die hervorragende answer von R. Martinho Fernandes.

+2

Im Falle von starre-init-Listen, ist "ret" _copy -list-initialisiert_! Wenn der Konvertierungskonstruktor von 'int' nach' foo' 'explizit' deklariert wird, erhalten Sie einen Fehler. Ähnlich in dem Fall 'return 42;'. Es ist immer _copy-initialization_. – MWid

+0

@MWid Du hast Recht. Wenn während der * Listen-Initialisierung * (z.B. 'return {42.0};') eine einschränkende Umwandlung erforderlich ist, ist der Code schlecht ausgebildet. Ich wollte mich nur auf den Hauptpunkt der OP-Frage konzentrieren (wenn/wann Kopien/Bewegungen gemacht werden) und diese anderen Fragen beiseite lassen. –

+0

Siehe auch [diese selbst beantwortete Frage von Johannes Schaub] (http://stackoverflow.com/q/7935639/420683) – dyp