9

Diese Frage wurde durch Verwirrung über RVO in C++ 11 ausgelöst.Welche "Return" -Methode ist besser für große Daten in C++/C++ 11?

Ich habe zwei Möglichkeiten, um "Return" value: Rückkehr nach Wert und Rückkehr über Referenzparameter. Wenn ich nicht betrachte die Leistung, bevorzuge ich die erste. Da die Rückgabe von Wert ist natürlicher und ich kann leicht die Eingabe und die Ausgabe unterscheiden. Aber, wenn ich die Effizienz bei der Rückgabe großer Daten betrachte. Ich kann mich nicht entscheiden, denn in C++ 11 gibt es RVO.

Hier mein Beispiel-Code, sind diese beiden Codes die gleiche Arbeit:

Rückkehr nach Wert

struct SolutionType 
{ 
    vector<double> X; 
    vector<double> Y; 
    SolutionType(int N) : X(N),Y(N) { } 
}; 

SolutionType firstReturnMethod(const double input1, 
           const double input2); 
{ 
    // Some work is here 

    SolutionType tmp_solution(N); 
    // since the name is too long, I make alias. 
    vector<double> &x = tmp_solution.X; 
    vector<double> &y = tmp_solution.Y; 

    for (...) 
    { 
    // some operation about x and y 
    // after that these two vectors become very large 
    } 

    return tmp_solution; 
} 

Rückkehr über Referenzparameter

void secondReturnMethod(SolutionType& solution, 
         const double input1, 
         const double input2); 
{ 
    // Some work is here   

    // since the name is too long, I make alias. 
    vector<double> &x = solution.X; 
    vector<double> &y = solution.Y; 

    for (...) 
    { 
    // some operation about x and y 
    // after that these two vectors become very large 
    } 
} 

Hier sind meine Fragen:

  1. Wie kann ich sicherstellen, dass RVO in C++ 11 passiert ist?
  2. Wenn wir sicher sind, dass RVO passiert ist, in der heutigen C++ - Programmierung, welche "Return" -Methode empfehlen Sie ? Warum?
  3. Warum gibt es eine Bibliothek, die die Rückgabe über Referenzparameter, Codestil oder historischen Grund verwendet?

UPDATE Dank dieser Antworten, ich weiß, die erste Methode in den meisten Art und Weise besser ist.

Hier einige nützlichen Links zum Thema, die mir dieses Problem verstehen helfen:

  1. How to return large data efficiently in C++11
  2. In C++, is it still bad practice to return a vector from a function?
  3. Want Speed? Pass by Value.

Antwort

16

Vor allem den richtigen technischen Begriff für das, was Sie sind Tun ist NRVO. RVO bezieht sich auf Provisorien zurückgegeben wird:

X foo() { 
    return make_x(); 
} 

NRVO bezieht sich auf benannte Objekte zurückgegeben werden:

X foo() { 
    X x = make_x(); 
    x.do_stuff(); 
    return x; 
} 

Zweitens (N) RVO ist Compiler-Optimierung und ist nicht vorgeschrieben. Sie können jedoch ziemlich sicher sein, dass (N) RVOs ziemlich aggressiv verwendet werden, wenn Sie einen modernen Compiler verwenden.

Drittel aller, (N) RVO ist nicht C++ 11 Features - hier lange vor 2011

Forth von allem ist, was Sie haben in C++ 11 ist ein bewegen Konstrukteur.Wenn also Ihre Klasse die Bewegungssemantik unterstützt, wird sie verschoben, nicht kopiert, auch wenn (N) RVO nicht passiert. Leider kann nicht alles semantisch effizient bewegt werden.

Fünftens ist die Rückkehr durch Verweis eine schreckliche Antipattern. Es stellt sicher, dass das Objekt zweimal effektiv erstellt wird - das erste Mal als "leeres" Objekt, das zweite Mal, wenn es mit Daten gefüllt wird - und es verhindert, dass Objekte verwendet werden, für die der Status 'leer' keine gültige Invariante ist.

+0

Wird es auch dann, wenn in 'return x verschoben werden;' 'x' ist ein L-Wert und' std :: move' fehlt? – Zereges

+7

Kein großer Fan von * viertem *, wie? – IInspectable

+4

@Zereges Niemals mit 'return std :: move (something) zurückkehren;' Es ist eine Pessimierung. – NathanOliver

1

Es gibt keine Möglichkeit sicherzustellen, dass RVO (oder NVRO) in C++ 11 auftritt. Ob es auftritt oder nicht, hängt mit der Qualität der Implementierung (z. B. des Compilers) zusammen, anstatt etwas zu sein, das vom Programmierer grundlegend gesteuert werden kann.

Verschiebungssemantik kann unter bestimmten Umständen verwendet werden, um einen ähnlichen Effekt zu erzielen, unterscheidet sich jedoch von RVO.

Im Allgemeinen empfehle ich die Verwendung der Methode, die für die Daten zur Verfügung steht, was für den Programmierer verständlich ist. Code, den der Programmierer verstehen kann, ist einfacher, richtig zu funktionieren. Das Wackeln mit arkanen Techniken zum Optimieren der Leistung (z. B. in einem Versuch, NVRO zu erzwingen) neigt dazu, Code schwieriger zu verstehen, als daher wahrscheinlicher Fehler zu haben (z. B. erhöhtes Potenzial für undefiniertes Verhalten). Wenn der Code korrekt funktioniert, aber MESSUNGEN zeigen, dass die erforderliche Leistung fehlt, können mehr geheimnisvolle Techniken zur Leistungssteigerung untersucht werden. Aber der Versuch, Code von Hand liebevoll zu optimieren (d. H. Bevor irgendwelche Messungen einen Bedarf gezeigt haben), wird aus einem Grund "vorzeitige Optimierung" genannt.

Die Rückgabe als Referenz ermöglicht die Vermeidung des Kopierens großer Daten bei der Rückgabe durch eine Funktion. Wenn also die Funktion eine große Datenstruktur zurückgibt, kann die Rückgabe als Referenz effizienter sein (durch verschiedene Maßnahmen) als die Rückgabe als Wert. Es gibt jedoch Kompromisse - die Rückgabe eines Verweises auf etwas ist gefährlich (führt zu undefiniertem Verhalten), wenn die zugrunde liegenden Daten nicht mehr existieren, während ein anderer Code einen Verweis darauf hat. Hingegen macht die Rückgabe eines Wertes es für einen Code schwierig, einen Verweis auf (zum Beispiel) eine Datenstruktur zu halten, die möglicherweise nicht mehr existiert.

BEARBEITEN: Hinzufügen eines Beispiels, bei dem die Rückgabe über Referenz gefährlich ist, wie im Kommentar gefordert.

AnyType &func() 
    { 
     Anytype x; 
     // initialise x in some way 

     return x; 
    }; 

    int main() 
    { 
     // assume AnyType can be sent to an ostream this wah 

     std::cout << func() << '\n';  // undefined behaviour here 
    } 

In diesem Fall func() gibt einen Verweis auf etwas, das nicht mehr vorhanden ist, nachdem es gibt - häufig eine baumelnden Referenz bezeichnet. Jede Verwendung dieser Referenz (in diesem Fall zum Drucken des Referenzwertes) hat ein undefiniertes Verhalten. Die Rückgabe nach Wert (d. H. Einfaches Entfernen der &) gibt eine Kopie der Variablen zurück, die existiert, wenn der Aufrufer versucht, sie zu verwenden.

Die Ursache für undefiniertes Verhalten ist, wie func() zurückgibt. Das undefinierte Verhalten tritt jedoch beim Aufrufer (der die Referenz verwendet) nicht innerhalb von func() selbst auf. Diese Trennung zwischen Ursache und Wirkung kann Fehler verursachen, die sehr schwer zu finden sind.

+0

Danke für deine Antwort. Ich weiß das, "verfrühte Optimierung ist die Wurzel allen Übels." Und ich versuche nicht, es zu optimieren. Ich bin ein Neuling von C++, wenn ich Code schreibe, möchte ich es nur richtig schreiben. Kannst du mir ein Beispiel geben, dass "ein Hinweis auf etwas gefährlich ist"? Ich denke, die Referenz ist in der Parameterliste, es ** ** ** existiert. – Regis

+0

Okay. Ich habe ein Beispiel für eine Funktion gegeben, die eine hängende Referenz zurückgibt.Es ist ein wenig komplizierter, aber immer noch einfach, ähnliche Beispiele zu konstruieren, bei denen ein Argument als Referenz übergeben wird und diese Referenz zurückgegeben wird, aber das Objekt, auf das verwiesen wird, aufhört zu existieren, bevor es verwendet wird. – Peter

+0

Sorry, vielleicht habe ich die Frage nicht klar beschrieben. In meiner Frage ist es keine Rückkehr durch Referenz, es ist "Rückkehr" über Referenzparameter, und die reale Rendite ist ungültig. Meine Bitte um Irreführung. – Regis

3

SergyA's Antwort ist perfekt. Wenn Sie diesem Rat folgen, werden Sie fast immer nichts falsch machen.

Es gibt jedoch eine Art von "Ergebnis", bei der es besser ist, einen Verweis auf das Ergebnis der Aufrufstelle zu übergeben.

Dies ist in dem Fall, in dem Sie einen std Container als Ergebnispuffer in einer Schleife verwenden.

Wenn Sie sich die Funktion std::getline ansehen, sehen Sie ein Beispiel.

std::getline wurde entwickelt, um einen std::string Puffer aus dem Eingangsstrom zu füllen.

Jedes Mal, wenn getline mit derselben String-Referenz aufgerufen wird, werden die Daten der Zeichenfolge überschrieben. Beachten Sie, dass im Laufe der Zeit (vorausgesetzt zufällige Zeilenlängen) manchmal eine implizite reserve der Zeichenfolge sein muss, um neue lange Zeilen zu berücksichtigen. Kürzere Linien als die bisher längsten benötigen jedoch keine reserve, da es bereits genug capacity geben wird.

Stellen Sie sich eine Version von getline mit der folgenden Signatur:

std::string fictional_getline(std::istream&); 

Dies bedeutet, dass eine neue Zeichenfolge jedes Mal wieder die Funktion aufgerufen wird. Unabhängig davon, ob RVO oder NRVO aufgetreten ist, muss diese Zeichenfolge erstellt werden, und wenn sie länger als die kurze Zeichenfolgeoptimierungsgrenze ist, erfordert dies eine Speicherzuweisung. Darüber hinaus wird der Speicher der Zeichenfolge jedes Mal freigegeben, wenn sie den Gültigkeitsbereich verlässt.

In diesem Fall und anderen wie es ist es viel effizienter, Ihren Ergebniscontainer als Referenz zu übergeben.

Beispiele:

void do_processing(const std::string& s) 
{ 
    // ... 
} 

/// @post: in the case of an error, os.bad() == true 
/// @post: in the case of no error, os.bad() == false 
std::string fictional_getline(std::istream& stream) 
{ 
    std::string result; 
    if (not std::getline(stream, result)) 
    { 
     // what to do here? 
    } 
    return result; 
} 

// note that buf is re-used which will require fewer and fewer 
// reallocations the more the loop progresses 
void fast_process(std::istream& stream) 
{ 
    std::string buf; 
    while(std::getline(std::cin, buf)) 
    { 
     do_processing(buf); 
    } 
} 

// note that buf is re-created and destroyed each time around the loop  
void not_so_fast_process(std::istream& stream) 
{ 
    for(;;) 
    { 
     auto buf = fictional_getline(stream); 
     if (!stream) break; 
     do_processing(buf); 
    } 
} 
+0

Danke für Ihre Antwort. Wenn ich ein Objekt in der Schleife erstelle, weiß ich, dass es jedes Mal erstellt und verrechnet wird, aber was ist, wenn ich das Objekt aus der Schleife dekleariere? In 'not_so_fast_process' mache ich es so:' std :: string buf; for (;;) {buf = fiktive_getline (stream); if (! stream) break; do_processing (buf);} ', verwendet ite' buf' wie den buf in 'fast_process'? – Regis

+0

@Reigs Der Compiler darf einen Kopierkonstruktor unabhängig von Nebeneffekten verlassen, aber es ist nicht erlaubt, einen Zuweisungsoperator zu verwenden. Sie hätten in diesem Fall immer noch einen redundanten Konstruktor/Destruktor-Zyklus pro Schleife. –

+0

Ich verstehe. Wenn ich den buf out of loop dekliniere, wird in dieser Zeile 'buf = fiktional_getline (stream);' 'die ganze Zeit kopiert, weil es eine Zuweisung ist, die nicht entfernt werden kann. Also, es wird weniger effizient sein. Vielen Dank. – Regis