Warum ist RVO bei der Rückgabe eines Parameters nicht zulässig?

Lesezeit: 7 Minuten

Warum ist RVO bei der Ruckgabe eines Parameters nicht zulassig
Valentin Milea

Es steht in [C++11: 12.8/31] :

Diese Elision von Kopier-/Verschiebeoperationen, die als Copy-Eliminierung bezeichnet wird, ist zulässig […] :

— in einer return-Anweisung in einer Funktion mit einem Klassenrückgabetyp, wenn der Ausdruck der Name eines nicht flüchtigen automatischen Objekts ist (anders als ein Funktions- oder Catch-Klausel-Parameter) mit demselben cv-unqualifizierten Typ wie der Rückgabetyp der Funktion, kann der Kopier-/Verschiebevorgang weggelassen werden, indem das automatische Objekt direkt in den Rückgabewert der Funktion eingebaut wird

Dies impliziert

#include <iostream>

using namespace std;

struct X
{
    X() { }
    X(const X& other) { cout << "X(const X& other)" << endl; }
};

X no_rvo(X x) {
    cout << "no_rvo" << endl;
    return x;
}

int main() {
    X x_orig;
    X x_copy = no_rvo(x_orig);

    return 0;
}

wird drucken

X(const X& other)
no_rvo
X(const X& other)

Warum ist der zweite Kopierkonstruktor erforderlich? Kann ein Compiler nicht einfach die Lebensdauer von verlängern x?

  • Es gibt tatsächlich drei Objekte, die in diesem Beispiel kopiert werden (können) (das Argument to no_rvo, der Rückgabewert von no_rvo und x_copy). Die Konstruktion von x_copy kann eliminiert werden (indem der Rückgabewert von konstruiert wird no_rvo direkt hinein x_copy).

    – Mankarse

    25. Februar 12 um 14:37 Uhr


  • @Mankarse: Ich denke, genau das fragt OP – Warum die Konstruktion von x_copy ist nicht ausgeschlossen?

    – jweyrich

    25. Februar 2012 um 14:40 Uhr


  • @jweyrich: Nein – die Frage ist, warum die Konstruktion des Rückgabewerts nicht eliminiert wird. Mein Punkt ist nur, dass es ungenau ist zu sagen, dass der Code Wille drucken X(const X& other) no_rvo X(const X& other), weil der Code auch gedruckt werden könnte X(const X& other) no_rvo X(const X& other) X(const X& other) wenn der Bau von x_copy ist nicht ausgeschlossen.

    – Mankarse

    25. Februar 12 um 14:42 Uhr


  • @Mankarse: Ops, genau. Ich verstehe jetzt deinen Punkt. Es s/wird/darf/ drucken.

    – jweyrich

    25. Februar 2012 um 14:50 Uhr


Sich vorstellen no_rvo ist in einer anderen Datei als definiert main damit beim Kompilieren main der Compiler sieht nur die Deklaration

X no_rvo(X x);

und wird keine Ahnung haben, ob das Objekt vom Typ ist X zurückgegeben hat beliebig Beziehung zum Argument. Von dem, was es zu diesem Zeitpunkt weiß, die Umsetzung von no_rvo könnte genauso gut sein

X no_rvo(X x) { X other; return other; }

Also wenn es zB die Zeile kompiliert

X const& x = no_rvo(X());

es wird Folgendes tun, wenn es maximal optimiert wird.

  • Generieren Sie das temporäre X, an das übergeben werden soll no_rvo als argument
  • Anruf no_rvo, und binden Sie seinen Rückgabewert an x
  • zerstören das temporäre Objekt, an das es übergeben wurde no_rvo.

Wenn nun der Rückgabewert von no_rvo dasselbe Objekt wäre wie das ihm übergebene Objekt, dann würde die Zerstörung des temporären Objekts die Zerstörung des zurückgegebenen Objekts bedeuten. Aber das wäre falsch, weil das zurückgegebene Objekt an eine Referenz gebunden ist und daher seine Lebensdauer über diese Anweisung hinaus verlängert. Das Argument einfach nicht zu zerstören ist aber auch keine Lösung, denn das wäre falsch, wenn die Definition von no_rvo ist die alternative Implementierung, die ich oben gezeigt habe. Wenn also die Funktion ein Argument als Rückgabewert wiederverwenden darf, kann es zu Situationen kommen, in denen der Compiler das korrekte Verhalten nicht feststellen konnte.

Beachten Sie, dass der Compiler bei gängigen Implementierungen sowieso nicht in der Lage wäre, dies wegzuoptimieren, daher ist es kein so großer Verlust, dass es formal nicht erlaubt ist. Beachten Sie auch, dass der Compiler ist darf die Kopie trotzdem wegoptimieren, wenn er nachweisen kann, dass dies nicht zu einer Änderung des beobachtbaren Verhaltens führt (die sogenannte Als-ob-Regel).

  • Im Grunde wäre es also inkompatibel mit der (zusammengenommenen) Elision von Kopierkonstruktionen von Argumenten und separater Kompilierung (denn wenn die Elision des Kopierkonstruktors eines Arguments durchgeführt wird, muss dies vom Aufrufer durchgeführt werden (weil zur (separaten) Kompilierungszeit der Aufgerufene kann nicht wissen, ob sein Argument aus einem temporären konstruiert wird oder nicht)).

    – Mankarse

    25. Februar 12 um 15:24 Uhr

  • Danke für die Erklärung! Wäre eine Berufungskonvention, in der die Funktion übernimmt die Verantwortung für die Zerstörung seiner Pass-by-Value-Argumente? Dann kümmert sich der Aufrufer nicht mehr darum, wie no_rvo definiert ist.

    – Valentin Milea

    25. Februar 12 um 19:43 Uhr

Die übliche Implementierung von RVO besteht darin, dass der aufrufende Code die Adresse eines Speicherabschnitts übergibt, in dem die Funktion ihr Ergebnisobjekt erstellen soll.

Wenn das Funktionsergebnis direkt eine automatische Variable ist, die kein formales Argument ist, kann diese lokale Variable einfach in den vom Aufrufer bereitgestellten Speicherabschnitt gestellt werden, und die return-Anweisung kopiert dann überhaupt nicht.

Für ein als Wert übergebenes Argument muss der aufrufende Maschinencode sein tatsächliches Argument in die Position des formalen Arguments kopieren und initialisieren, bevor er zur Funktion springt. Damit die Funktion ihr Ergebnis dort platzieren kann, müsste sie zuerst das formale Argumentobjekt zerstören, was einige knifflige Sonderfälle hat (zB wenn diese Konstruktion direkt oder indirekt auf das formale Argumentobjekt verweist). Anstatt also den Ergebnisspeicherort mit dem formalen Argumentspeicherort zu identifizieren, muss eine Optimierung hier logischerweise einen separaten, vom Aufruf bereitgestellten Speicherabschnitt für das Funktionsergebnis verwenden.

Allerdings ein Funktionsergebnis, das nicht in einem Register übergeben wird ist normalerweise vom Anrufer bereitgestellt. Dh, was man vernünftigerweise als RVO bezeichnen könnte, eine Art vermindertes RVO, für den Fall von a return Ausdruck, der ein formales Argument bezeichnet, würde sowieso passieren. Und es passt nicht zu dem Text „indem das automatische Objekt direkt in den Rückgabewert der Funktion eingebaut wird“.

Zusammenfassend bedeutet der Datenfluss, der erfordert, dass der Aufrufer einen Wert übergibt, dass es notwendigerweise der Aufrufer ist, der den Speicher eines formalen Arguments initialisiert, und nicht die Funktion. Daher kann das Zurückkopieren von einem formalen Argument nicht vermieden werden im Allgemeinen (Dieser Wieselbegriff deckt die Sonderfälle ab, in denen der Compiler sehr spezielle Dinge tun kann, insbesondere für eingebetteten Maschinencode). Es ist jedoch die Funktion, die den Speicher jedes anderen lokalen automatischen Objekts initialisiert, und dann ist es kein Problem, RVO auszuführen.

  • Es stimmt zwar, dass die gängige Implementierung diese Optimierung sowieso nicht unterstützen würde, aber das allein erklärt nicht, warum der Standard dies verbietet. Schließlich handelt es sich um eine Optimierung und somit nicht erforderlich getan werden.

    – Celtschk

    25. Februar 12 um 15:04 Uhr

  • @celtschk es ist nicht nur die gemeinsame Implementierung, sondern alle bestehende Implementierungen. Beachten Sie auch, dass diese Optimierung nicht innerhalb der Funktion erfolgen kann, sondern vom Aufrufer mit Unterstützung der Aufrufkonventionen durchgeführt werden müsste. Beachten Sie, dass nur der Aufrufer weiß, dass das Argument ein ist rwert und dass der Raum wiederverwendet werden kann. Beachten Sie auch, dass Anrufer und Angerufener sich bei jedem Anruf darauf einigen müssten, ob das Argument so wäre semantisch während des Funktionsaufrufs zerstört wird oder später zerstört werden soll.

    – David Rodríguez – Dribeas

    25. Februar 12 um 15:48 Uhr

  • … Das Problem ist, dass das Zulassen dieser Optimierung dem Compiler keine Freiheit gibt, sondern eine ganze Reihe von Anforderungen auferlegt, um die Möglichkeit zu verwalten, dass die Optimierung stattgefunden hat, ohne undefiniertes Verhalten zu verursachen. Auch wenn es Sache des Compilers wäre, dieses Gift zu wählen, wurde die Möglichkeit, eine Modulschnittstelle zu definieren, im Standardisierungsprozess diskutiert und einer späteren Version des Standards vorbehalten. Wenn diese Optimierung erlaubt wäre und ein einzelner Compiler sich dafür entscheiden würde, müssten in C++ mit Modulen alle Compiler in den sauren Apfel beißen.

    – David Rodríguez – Dribeas

    25. Februar 12 um 15:51 Uhr


  • @DavidRodríguez-dribeas: Es spielt keine Rolle, ob Nein aktuellen Compiler ist so implementiert, dass diese Optimierung möglich ist. Ein Compiler-Schreiber kann ignorieren, dass diese Optimierung zulässig ist. Der Compiler-Schreiber weiß auch, dass diese Optimierung in diesem Fall nicht stattgefunden hat, weil es der Compiler gewesen wäre, der es getan hätte, und der Compiler nicht.

    – Celtschk

    25. Februar 12 um 15:57 Uhr


  • @DavidRodríguez-dribeas: Wenn es unmöglich wäre, es zu implementieren, gäbe es sogar weniger Grund für den Standard, dies explizit(!) zu verbieten. Dies wäre nicht erforderlich, und die zusätzliche Klammer wäre nur Platz- und Zeitverschwendung. Wenn du etwas ausdrücklich verbietest, dann tust du es, weil es so ist möchten möglich, hätte aber Folgen, die Sie nicht wollen.

    – Celtschk

    25. Februar 12 um 17:07 Uhr


.

503360cookie-checkWarum ist RVO bei der Rückgabe eines Parameters nicht zulässig?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy