Effiziente Möglichkeit, einen std :: vector in c ++ zurückzugeben


106

Wie viele Daten werden kopiert, wenn ein std :: vector in einer Funktion zurückgegeben wird und wie groß die Optimierung sein wird, um den std :: vector im Free-Store (auf dem Heap) zu platzieren und stattdessen einen Zeiger zurückzugeben, dh:

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

effizienter als:

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 

?


3
Wie wäre es, wenn Sie den Vektor als Referenz übergeben und dann ausfüllen f?
Kiril Kirov

4
RVO ist eine ziemlich grundlegende Optimierung, die die meisten Compiler jeden Moment ausführen können.
Remus Rusanu

Wenn Antworten eingehen, können Sie möglicherweise klären, ob Sie C ++ 03 oder C ++ 11 verwenden. Die Best Practices zwischen den beiden Versionen variieren erheblich.
Drew Dormann


@Kiril Kirov, kann ich das tun, ohne es in die Argumentliste der Funktion zu setzen, dh. void f (std :: vector & result)?
Morten

Antworten:


140

In C ++ 11 ist dies der bevorzugte Weg:

std::vector<X> f();

Das heißt, Rückgabe nach Wert.

Hat in C ++ 11 std::vectoreine Verschiebungssemantik, was bedeutet, dass der in Ihrer Funktion deklarierte lokale Vektor bei der Rückgabe verschoben wird und in einigen Fällen sogar die Verschiebung vom Compiler entfernt werden kann.


13
@LeonidVolnitsky: Ja, wenn es lokal ist . In der Tat return std::move(v);wird die Bewegungselision deaktiviert, selbst wenn dies nur mit möglich war return v;. Letzteres ist also bevorzugt.
Nawaz

1
@juanchopanza: Das glaube ich nicht. Vor C ++ 11 konnten Sie dagegen argumentieren, da der Vektor nicht verschoben wird. und RVO ist eine compilerabhängige Sache! Sprechen Sie über die Dinge aus den 80ern und 90ern.
Nawaz

2
Mein Verständnis des Rückgabewerts (nach Wert) lautet: Anstelle von "verschoben" wird der Rückgabewert im Angerufenen auf dem Stapel des Aufrufers erstellt, sodass alle Vorgänge im Angerufenen vorhanden sind und in RVO nichts verschoben werden kann . Ist das korrekt?
r0ng

2
@ r0ng: Ja, das stimmt. So implementieren die Compiler normalerweise RVO.
Nawaz

1
@ Nawaz ist es nicht. Es gibt nicht einmal mehr eine Bewegung.
Leichtigkeitsrennen im Orbit

70

Sie sollten nach Wert zurückkehren.

Der Standard verfügt über eine spezielle Funktion zur Verbesserung der Effizienz der Wertrendite. Es heißt "Copy Elision" und in diesem Fall "Named Return Value Optimization (NRVO)".

Compiler müssen es nicht implementieren, aber dann wieder Compiler nicht haben Funktion zu implementieren inlining (oder jede Optimierung überhaupt durchführen). Die Leistung der Standardbibliotheken kann jedoch ziemlich schlecht sein, wenn Compiler nicht optimieren und alle seriösen Compiler Inlining und NRVO (und andere Optimierungen) implementieren.

Wenn NRVO angewendet wird, wird der folgende Code nicht kopiert:

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

Der Benutzer möchte dies jedoch möglicherweise tun:

std::vector<int> myvec;
... some time later ...
myvec = f();

Die Kopierelision verhindert hier keine Kopie, da es sich eher um eine Zuweisung als um eine Initialisierung handelt. Sie sollten jedoch weiterhin nach Wert zurückkehren. In C ++ 11 wird die Zuweisung durch etwas anderes optimiert, das als "Verschiebungssemantik" bezeichnet wird. In C ++ 03 verursacht der obige Code eine Kopie, und obwohl theoretisch ein Optimierer dies möglicherweise vermeiden kann, ist es in der Praxis zu schwierig. myvec = f()In C ++ 03 sollten Sie stattdessen Folgendes schreiben:

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

Es gibt eine weitere Option, die dem Benutzer eine flexiblere Benutzeroberfläche bietet:

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

Darüber hinaus können Sie die vorhandene vektorbasierte Schnittstelle unterstützen:

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

Dies ist möglicherweise weniger effizient als Ihr vorhandener Code, wenn Ihr vorhandener Code reserve()komplexer ist als nur ein fester Betrag im Voraus. Wenn Ihr vorhandener Code push_backden Vektor jedoch grundsätzlich wiederholt aufruft , sollte dieser vorlagenbasierte Code genauso gut sein.


Upvoted die wirklich beste und detaillierteste Antwort. In Ihrer swap () -Variante ( für C ++ 03 ohne NRVO ) wird jedoch immer noch eine Kopie-Konstruktor-Kopie in f () erstellt: vom variablen Ergebnis zu einem versteckten temporären Objekt, das zuletzt gegen myvec ausgetauscht wird .
JenyaKh

@ JenyaKh: Sicher, das ist ein Problem mit der Qualität der Implementierung. Der Standard erforderte nicht, dass die C ++ 03-Implementierungen NRVO implementierten, genauso wie es kein Inlining von Funktionen erforderte. Der Unterschied zum Inlining von Funktionen besteht darin, dass das Inlining weder die Semantik noch Ihr Programm ändert, während dies bei NRVO der Fall ist. Portabler Code muss mit oder ohne NRVO funktionieren. Optimierter Code für eine bestimmte Implementierung (und bestimmte Compiler-Flags) kann Garantien in Bezug auf NRVO in der eigenen Dokumentation der Implementierung suchen.
Steve Jessop

3

Es ist Zeit, dass ich eine Antwort über RVO poste , ich auch ...

Wenn Sie ein Objekt nach Wert zurückgeben, optimiert der Compiler dies häufig, damit es nicht zweimal erstellt wird, da es überflüssig ist, es in der Funktion als temporär zu erstellen und dann zu kopieren. Dies wird als Rückgabewertoptimierung bezeichnet: Das erstellte Objekt wird verschoben, anstatt kopiert zu werden.


1

Eine gängige Sprache vor C ++ 11 besteht darin, einen Verweis auf das zu füllende Objekt zu übergeben.

Dann erfolgt keine Kopie des Vektors.

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 

3
Das ist in C ++ 11 keine Redewendung mehr.
Nawaz

1
@Nawaz Ich stimme zu. Ich bin mir nicht sicher, was die beste Vorgehensweise für SO in Bezug auf Fragen zu C ++ ist, aber nicht speziell für C ++ 11. Ich vermute, ich sollte geneigt sein, einem Schüler C ++ 11-Antworten zu geben, C ++ 03-Antworten jemandem, der im Produktionscode hüfthoch ist. Hast du eine meinung
Drew Dormann

7
Nach der Veröffentlichung von C ++ 11 (das 19 Monate alt ist) betrachte ich jede Frage als C ++ 11-Frage, es sei denn, es wird ausdrücklich angegeben, dass es sich um eine C ++ 03-Frage handelt.
Nawaz

1

Wenn der Compiler die Optimierung des benannten Rückgabewerts unterstützt ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ), können Sie den Vektor direkt zurückgeben, sofern keine vorhanden ist:

  1. Unterschiedliche Pfade, die unterschiedliche benannte Objekte zurückgeben
  2. Mehrere Rückgabepfade (auch wenn auf allen Pfaden dasselbe benannte Objekt zurückgegeben wird) mit eingeführten EH-Zuständen.
  3. Das zurückgegebene benannte Objekt wird in einem Inline-ASM-Block referenziert.

NRVO optimiert die redundanten Kopierkonstruktor- und Destruktoraufrufe und verbessert somit die Gesamtleistung.

In Ihrem Beispiel sollte es keinen wirklichen Unterschied geben.


0
vector<string> getseq(char * db_file)

Und wenn Sie es auf main () drucken möchten, sollten Sie es in einer Schleife tun.

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}

-2

So schön "return by value" auch sein mag, es ist die Art von Code, die einen in einen Fehler führen kann. Betrachten Sie das folgende Programm:

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • F: Was passiert, wenn das oben genannte ausgeführt wird? A: Ein Coredump.
  • F: Warum hat der Compiler den Fehler nicht erkannt? A: Weil das Programm syntaktisch, wenn auch nicht semantisch, korrekt ist.
  • F: Was passiert, wenn Sie vecFunc () so ändern, dass eine Referenz zurückgegeben wird? A: Das Programm wird vollständig ausgeführt und liefert das erwartete Ergebnis.
  • F: Was ist der Unterschied? A: Der Compiler muss keine anonymen Objekte erstellen und verwalten. Der Programmierer hat den Compiler angewiesen, genau ein Objekt für den Iterator und für die Endpunktbestimmung zu verwenden, anstatt zwei verschiedene Objekte wie im fehlerhaften Beispiel.

Das obige fehlerhafte Programm zeigt keine Fehler an, selbst wenn man die GNU g ++ - Berichtsoptionen -Wall -Wextra -Weffc ++ verwendet

Wenn Sie einen Wert erzeugen müssen, funktioniert Folgendes, anstatt vecFunc () zweimal aufzurufen:

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

Das Obige erzeugt auch keine anonymen Objekte während der Iteration der Schleife, erfordert jedoch einen möglichen Kopiervorgang (der, wie einige Hinweise erwähnen, unter bestimmten Umständen wegoptimiert werden kann. Die Referenzmethode garantiert jedoch, dass keine Kopie erzeugt wird RVO ausführen ist kein Ersatz für den Versuch, den effizientesten Code zu erstellen, den Sie können. Wenn Sie die Notwendigkeit für den Compiler, RVO auszuführen, in Frage stellen können, sind Sie dem Spiel voraus.


3
Dies ist eher ein Beispiel dafür, was schief gehen kann, wenn ein Benutzer mit C ++ im Allgemeinen nicht vertraut ist. Jemand, der mit objektbasierten Sprachen wie .net oder Javascript vertraut ist, würde wahrscheinlich annehmen, dass der Zeichenfolgenvektor immer als Zeiger übergeben wird und daher in Ihrem Beispiel immer auf dasselbe Objekt verweist. vecfunc (). begin () und vecfunc (). end () stimmen in Ihrem Beispiel nicht unbedingt überein, da sie Kopien des Zeichenfolgenvektors sein sollten.
Medran

-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.