Modo efficiente per restituire un vettore std :: in c ++


106

Quanti dati vengono copiati, quando si restituisce uno std :: vector in una funzione e quanto grande sarà l'ottimizzazione per posizionare lo std :: vector in free-store (sull'heap) e restituire invece un puntatore, cioè è:

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

più efficiente di:

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

?


3
Che ne dici di passare il vettore per riferimento e poi riempirlo all'interno f?
Kiril Kirov

4
RVO è un'ottimizzazione piuttosto semplice che la maggior parte dei compilatori sarà in grado di eseguire in qualsiasi momento.
Remus Rusanu

Man mano che le risposte arrivano, può aiutarti a chiarire se stai usando C ++ 03 o C ++ 11. Le migliori pratiche tra le due versioni variano molto.
Drew Dormann


@ Kiril Kirov, posso farlo senza metterlo nell'elenco degli argomenti della funzione, ad es. void f (std :: vettore e risultato)?
Morten

Risposte:


140

In C ++ 11, questo è il modo preferito:

std::vector<X> f();

Cioè, ritorno per valore.

Con C ++ 11, std::vectorha la semantica dello spostamento, il che significa che il vettore locale dichiarato nella funzione verrà spostato al ritorno e in alcuni casi anche lo spostamento può essere eliso dal compilatore.


13
@LeonidVolnitsky: Sì, se è locale . In effetti, return std::move(v);disabiliterà l'elisione del movimento anche se era possibile con solo return v;. Quindi quest'ultimo è preferito.
Nawaz

1
@juanchopanza: non la penso così. Prima di C ++ 11, potevi contestarlo perché il vettore non verrà spostato; e RVO è una cosa dipendente dal compilatore! Parla delle cose degli anni '80 e '90.
Nawaz

2
La mia comprensione del valore di ritorno (in base al valore) è: invece di 'stato spostato', il valore di ritorno nel chiamato viene creato sullo stack del chiamante, quindi tutte le operazioni nel chiamato sono sul posto, non c'è niente da spostare in RVO . È corretto?
r0ng

2
@ r0ng: Sì, è vero. Questo è il modo in cui i compilatori di solito implementano RVO.
Nawaz

1
@Nawaz Non lo è. Non c'è più nemmeno una mossa.
Gare di leggerezza in orbita

70

Dovresti restituire per valore.

Lo standard ha una caratteristica specifica per migliorare l'efficienza del ritorno in valore. Si chiama "copy elision", e più specificamente in questo caso "named return value optimization (NRVO)".

I compilatori non devono implementarlo, ma anche in questo caso i compilatori non devono implementare la funzione inlining (o eseguire alcuna ottimizzazione). Ma le prestazioni delle librerie standard possono essere piuttosto scadenti se i compilatori non si ottimizzano e tutti i compilatori seri implementano inlining e NRVO (e altre ottimizzazioni).

Quando viene applicato NRVO, non ci sarà copia nel seguente codice:

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

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

Ma l'utente potrebbe voler fare questo:

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

L'elisione della copia non impedisce una copia qui perché è un'assegnazione piuttosto che un'inizializzazione. Tuttavia, dovresti comunque restituire per valore. In C ++ 11, l'assegnazione è ottimizzata da qualcosa di diverso, chiamato "semantica di spostamento". In C ++ 03, il codice sopra causa una copia e, sebbene in teoria un ottimizzatore possa essere in grado di evitarlo, in pratica è troppo difficile. Quindi, invece di myvec = f(), in C ++ 03 dovresti scrivere questo:

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

C'è un'altra opzione, che è quella di offrire un'interfaccia più flessibile all'utente:

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

È quindi possibile supportare anche l'interfaccia basata su vettori esistente oltre a ciò:

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

Questo potrebbe essere meno efficiente del codice esistente, se il codice esistente utilizza reserve()in un modo più complesso di una semplice quantità fissa in anticipo. Ma se il codice esistente fondamentalmente richiama push_backil vettore ripetutamente, allora questo codice basato su modello dovrebbe essere altrettanto buono.


Votato la risposta davvero migliore e dettagliata. Tuttavia, nella tua variante swap () ( per C ++ 03 senza NRVO ) avrai ancora una copia del costruttore di copie fatta all'interno di f (): dal risultato della variabile a un oggetto temporaneo nascosto che sarà finalmente scambiato con myvec .
JenyaKh

@JenyaKh: certo, questo è un problema di qualità dell'implementazione. Lo standard non richiedeva che le implementazioni C ++ 03 implementassero NRVO, proprio come non richiedeva l'inlining di funzioni. La differenza dalla funzione inlining, è che l'inlining non cambia la semantica o il programma mentre NRVO lo fa. Il codice portatile deve funzionare con o senza NRVO. Il codice ottimizzato per una particolare implementazione (e particolari flag del compilatore) può cercare garanzie riguardo NRVO nella propria documentazione dell'implementazione.
Steve Jessop

3

È ora che pubblichi una risposta su RVO , anche io ...

Se restituisci un oggetto per valore, il compilatore spesso lo ottimizza in modo che non venga costruito due volte, poiché è superfluo costruirlo nella funzione come temporaneo e quindi copiarlo. Questa si chiama ottimizzazione del valore di ritorno: l'oggetto creato verrà spostato invece di essere copiato.


1

Un linguaggio comune pre-C ++ 11 consiste nel passare un riferimento all'oggetto da riempire.

Quindi non c'è copia del vettore.

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

3
Questo non è più un idioma in C ++ 11.
Nawaz

1
@Nawaz sono d'accordo. Non sono sicuro di quale sia la migliore pratica ora su SO per quanto riguarda le domande su C ++ ma non specificamente C ++ 11. Sospetto che dovrei essere incline a dare risposte C ++ 11 a uno studente, risposte C ++ 03 a qualcuno immerso nel codice di produzione. Hai un'opinione?
Drew Dormann

7
In realtà, dopo il rilascio di C ++ 11 (che ha 19 mesi), considero ogni domanda come una domanda C ++ 11, a meno che non sia esplicitamente dichiarato come domanda C ++ 03.
Nawaz

1

Se il compilatore supporta l'ottimizzazione del valore di ritorno denominato ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ), puoi restituire direttamente il vettore purché non sia presente:

  1. Percorsi diversi che restituiscono oggetti con nome diverso
  2. Più percorsi di ritorno (anche se lo stesso oggetto con nome viene restituito su tutti i percorsi) con stati EH introdotti.
  3. L'oggetto denominato restituito viene referenziato in un blocco asm inline.

NRVO ottimizza le chiamate ridondanti del costruttore di copia e del distruttore e quindi migliora le prestazioni complessive.

Non dovrebbero esserci differenze reali nel tuo esempio.


0
vector<string> getseq(char * db_file)

E se vuoi stamparlo su main () dovresti farlo in un ciclo.

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

Per quanto bello possa essere il "ritorno per valore", è il tipo di codice che può portare in errore. Considera il seguente programma:

    #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;
    };
  • D: Cosa succederà quando verrà eseguito quanto sopra? A: Un coredump.
  • D: Perché il compilatore non ha rilevato l'errore? R: Perché il programma è sintatticamente, anche se non semanticamente, corretto.
  • D: Cosa succede se modifichi vecFunc () per restituire un riferimento? R: Il programma viene eseguito fino al completamento e produce il risultato atteso.
  • D: Qual è la differenza? R: Il compilatore non deve creare e gestire oggetti anonimi. Il programmatore ha indicato al compilatore di utilizzare esattamente un oggetto per l'iteratore e per la determinazione dell'endpoint, piuttosto che due oggetti diversi come fa l'esempio rotto.

Il programma errato di cui sopra non indicherà errori anche se si utilizzano le opzioni di reporting GNU g ++ -Wall -Wextra -Weffc ++

Se è necessario produrre un valore, quanto segue funzionerà al posto di chiamare due volte vecFunc ():

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

Quanto sopra, inoltre, non produce oggetti anonimi durante l'iterazione del ciclo, ma richiede una possibile operazione di copia (che, come alcuni notano, potrebbe essere ottimizzata in alcune circostanze. Ma il metodo di riferimento garantisce che nessuna copia verrà prodotta. Credendo che il compilatore lo farà eseguire RVO non è un sostituto per cercare di costruire il codice più efficiente possibile.Se puoi discutere la necessità che il compilatore esegua RVO, sei in vantaggio.


3
Questo è più un esempio di cosa può andare storto se un utente non ha familiarità con C ++ in generale. Qualcuno che abbia familiarità con linguaggi basati su oggetti come .net o javascript probabilmente presumerebbe che il vettore stringa sia sempre passato come puntatore e quindi nel tuo esempio punterebbe sempre allo stesso oggetto. vecfunc (). begin () e vecfunc (). end () non corrisponderanno necessariamente nel tuo esempio poiché dovrebbero essere copie del vettore stringa.
Medran

-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.