2013-09-05 2 views
9

Mein Verständnis der Rückgabewertoptimierung besteht darin, dass der Compiler die Adresse des Objekts, in dem der Rückgabewert gespeichert wird, geheim übergibt und die Änderungen an diesem Objekt statt an einer lokalen Variablen vornimmt.Wie weiß der Aufrufer einer Funktion, ob die Rückgabewertoptimierung verwendet wurde?

Zum Beispiel kann der Code

std::string s = f(); 

std::string f() 
{ 
    std::string x = "hi"; 
    return x; 
} 

ähnlich wird

std::string s; 
f(s); 

void f(std::string& x) 
{ 
    x = "hi"; 
} 

Wenn RVO verwendet wird. Dies bedeutet, dass sich die Schnittstelle der Funktion geändert hat, da ein zusätzlicher versteckter Parameter vorhanden ist.

Betrachten wir nun die folgenden Fall, den ich aus Wikipedia stahl

std::string f(bool cond) 
{ 
    std::string first("first"); 
    std::string second("second"); 
    // the function may return one of two named objects 
    // depending on its argument. RVO might not be applied 
    return cond ? first : second; 
} 

Nehmen wir an, dass ein Compiler RVO auf den ersten Fall gelten, aber nicht in diesem zweiten Fall. Aber ändert sich nicht die Schnittstelle der Funktion, je nachdem, ob RVO angewendet wurde? Wenn der Rumpf der Funktion f für den Compiler nicht sichtbar ist, wie weiß der Compiler, ob RVO angewendet wurde und ob der Aufrufer den Parameter der ausgeblendeten Adresse übergeben muss?

+3

Jeder Compiler kann wählen, was er will, und wenn es ein Problem für sie in dem von Ihnen beschriebenen Fall ist, werden sie wahrscheinlich immer RVO verwenden. Wenn Sie daran interessiert sind, wie verschiedene Compiler es unter die Haube tun, empfehle ich Ihnen, den generierten Assembler-Code zu lesen. Verwenden Sie den GCC-Explorer für den einfachen Zugriff auf die von clang/gcc generierte Baugruppe. – PlasmaHH

+0

@PlasmaHH Aber RVO ist nicht immer möglich. –

+1

Das bedeutet nicht, dass die Aufrufkonvention sich ändert, wenn sie verwendet wird oder nicht. Sehen Sie sich einen Assembler an, wie er es macht. – PlasmaHH

Antwort

7

Die Schnittstelle ändert sich nicht. In allen Fällen müssen die Ergebnisse der Funktion im Bereich des Aufrufers erscheinen; In der Regel verwendet der Compiler einen versteckten Zeiger. Der einzige Unterschied ist, dass, wenn RVO verwendet wird, wie in Ihrem ersten Fall der Compiler x und diesen Rückgabewert "verschmelzen", x an der Adresse durch den Zeiger; Wenn es nicht verwendet wird, generiert der Compiler einen Aufruf an den Copy-Konstruktor in der return-Anweisung, um alles in diesen Rückgabewert zu kopieren.

Ich möchte hinzufügen, dass Ihr zweites Beispiel ist nicht sehr nahe, was passiert. Am Aufrufort, erhalten Sie fast immer etwas wie:

<raw memory for string> s; 
f(&s); 

Und die aufgerufene Funktion wird entweder eine lokale Variable oder vorübergehend direkt an die Adresse konstruieren sie übergeben wurde, oder kopieren einige othe Wert bei diesem Konstrukt Adresse. So dass in Ihrem letzten Beispiel würde die Rückkehr Aussage mehr oder weniger die Äquivalent:

if (cont) { 
    std::string::string(s, first); 
} else { 
    std::string::string(s, second); 
} 

(. Anzeigen der impliziten this Zeiger auf die Kopie Konstruktor übergeben) Im ersten Fall, wenn RVO gilt

std::string::string(s, "hi"); 

und dann x mit *s überall sonst in der Funktion ersetzt: der spezielle Code im Konstruktor von x wäre(und bei der Rückkehr nichts tun).

+0

Also fügt es einen versteckten Parameter hinzu und macht die Funktion ungültig? (deutlich unter der Haube) – xanatos

+2

@xanatos: es gibt keine "void" -Funktionen auf der Assembler-Ebene, es gibt Konventionen dafür, an welchen Stellen Rückgabewerte gesetzt werden und wo der Aufrufer sie erwartet, "void" ist genau dann der Fall, wenn die Nummer der Rückgabewerte ist 0. – PlasmaHH

+1

@PlasmaHH Und es gibt keine Parameter auf der Assembler-Ebene ... Nur Dinge, die jemand auf den Stapel geschoben oder in ein Register gesetzt. Wir können das Spiel den ganzen Tag spielen. Nehmen wir an, es wird kein Rückgabewert auf den Stapel geschoben oder in ein Register geschrieben, so dass es ähnlich einer Leerfunktion ist. – xanatos

2

Lets spielen mit NRVO, RVO und Kopie Elision!Hier

ist ein Typ:

#include <iostream> 
struct Verbose { 
    Verbose(Verbose const&){ std::cout << "copy ctor\n"; } 
    Verbose(Verbose &&){ std::cout << "move ctor\n"; } 
    Verbose& operator=(Verbose const&){ std::cout << "copy asgn\n"; } 
    Verbose& operator=(Verbose &&){ std::cout << "move asgn\n"; } 
}; 

, die ziemlich ausführlich ist. Hier

ist eine Funktion:

Verbose simple() { return {}; } 

, die ziemlich einfach ist, und verwendet die direkte Konstruktion seines Rückgabewert. Wenn Verbose ein Kopie- oder Move-Konstruktor fehlte, würde die obige Funktion funktionieren!

Hier ist eine Funktion, die RVO verwendet:

Verbose simple_RVO() { return Verbose(); } 

hier die unbenannte Verbose() temporäre Objekt erzählt wird sich auf den Rückgabewert zu kopieren. RVO bedeutet, dass der Compiler diese Kopie überspringen und direkt Verbose() in den Rückgabewert konstruieren kann, genau dann, wenn es einen Copy- oder Move-Konstruktor gibt. Der Copy- oder Move-Konstruktor wird nicht aufgerufen, sondern eher weggelassen.

Hier ist eine Funktion, die NRVO verwendet:

Verbose simple_NRVO() { 
    Verbose retval; 
    return retval; 
} 

Für NRVO jeder Pfad zu kommen, muss genau das gleiche Objekt zurück, und Sie können darüber nicht hinterhältig sein (wenn Sie den Rückgabewert zu gieße eine Referenz, dann die Referenz zurückgeben, die NRVO blockiert). In diesem Fall konstruiert der Compiler das benannte Objekt retval direkt in die Rückgabewertposition. Ähnlich wie bei RVO muss ein Copy- oder Move-Konstruktor existieren, wird aber nicht aufgerufen.

Hier ist eine Funktion, die NRVO zu verwenden, schlägt fehl:

Verbose simple_no_NRVO(bool b) { 
    Verbose retval1; 
    Verbose retval2; 
    if (b) 
    return retval1; 
    else 
    return retval2; 
} 

, da es nicht beide von ihnen in der Rückgabewert Lage sind zwei es möglich, benannte Objekte könnten zurückkehren konstruieren können, so dass es tun muss eine tatsächliche Kopie. In C++ 11 wird das zurückgegebene Objekt implizit move d statt kopiert, da es sich um eine lokale Variable handelt, die von einer Funktion in einer einfachen return-Anweisung zurückgegeben wird. So ist es zumindest.

Schließlich gibt es Kopie elision am anderen Ende:

Verbose v = simple(); // or simple_RVO, or simple_NRVO, or... 

Wenn Sie eine Funktion aufrufen, Sie bieten es mit seinen Argumenten, und Sie es informieren, wo sie ihren Rückgabewert setzen sollte. Der Aufrufer ist dafür verantwortlich, den Rückgabewert zu bereinigen und den Speicher (auf dem Stapel) dafür zuzuweisen.

Diese Kommunikation erfolgt in gewisser Weise über die Aufrufkonvention, oft implizit (dh über den Stack-Pointer).

Unter vielen Aufrufkonventionen kann die Position, an der der Rückgabewert gespeichert werden kann, als lokale Variable verwendet werden.

Im Allgemeinen, wenn Sie eine Variable des Formulars haben:

Verbose v = Verbose(); 

die implizite Kopie elided werden kann - Verbose() direkt in v konstruiert, anstatt eine temporäre dann auf v kopiert erstellt werden.Auf die gleiche Weise kann der Rückgabewert von simple (oder simple_NRVO oder was auch immer) entfernt werden, wenn das Laufzeitmodell des Compilers dies unterstützt (und dies normalerweise tut).

Grundsätzlich kann der aufrufende Standort simple_* anweisen, den Rückgabewert an einer bestimmten Stelle zu setzen und diesen Punkt einfach als lokale Variable v zu behandeln.

Beachten Sie, dass NRVO und RVO und implizite Verschiebung sind alle innerhalb der Funktion getan, und der Anrufer muss nichts davon wissen.

In ähnlicher Weise ist die eliding an der aufrufenden Website außerhalb der Funktion getan, und wenn die Aufrufkonvention es unterstützt, benötigen Sie keine Unterstützung aus dem Körper der Funktion.

Dies muss nicht in jeder Aufrufkonvention und in jedem Laufzeitmodell zutreffen, sodass der C++ - Standard diese Optimierungen optional macht.

+0

Wenn wir das Beispiel von "simple_no_NRVO" ändern, um die Variablen in den verschiedenen if-else-Fällen zu konstruieren, wird nrvo angewendet? Verbose simple_no_NRVO (bool b) { \t \t if (b) \t \t { \t \t \t Verbose retval1; \t \t \t Rückkehr retval1; \t \t} \t \t sonst \t \t { \t \t \t Verbose retval2; \t \t \t Rückkehr retval2; \t \t} } –

Verwandte Themen