2015-07-24 33 views
7

Sono un nuovo arrivato in C++ e mi sono imbattuto in un problema che recentemente ha restituito un riferimento a una variabile locale. L'ho risolto modificando il valore di ritorno da std::string& a std::string. Tuttavia, a mio modo di vedere questo può essere molto inefficiente. Si consideri il seguente codice:Come evitare di copiare un valore di ritorno

string hello() 
{ 
    string result = "hello"; 
    return result; 
} 

int main() 
{ 
    string greeting = hello(); 
} 

Per la mia comprensione, ciò che accade è:

  • hello() si chiama.
  • La variabile locale result viene assegnata un valore di "hello".
  • Il valore di result viene copiato nella variabile greeting.

Questo probabilmente non importa più di tanto per std::string, ma può sicuramente ottenere costosi se si dispone, ad esempio, una tabella hash con centinaia di voci.

Come si evita la copiatura di un temporaneo restituito e si restituisce invece una copia del puntatore all'oggetto (in sostanza, una copia della variabile locale)?


Sidenote:. Ho heard che il compilatore a volte eseguire l'ottimizzazione ritorno di valore per evitare di chiamare il costruttore di copia, ma penso che sia meglio non fare affidamento su ottimizzazioni del compilatore per rendere efficiente la corsa del codice)

+1

Non ottimizzare fino a quando non hai confermato che la soluzione non soddisfa i requisiti aziendali e quindi ottimizzare solo ciò che il profiler ti dice che è troppo lento. Lascia che sia il compilatore a gestirlo. Sarà più facile da leggere e mantenere. –

+2

In C++ 11 questo quasi certamente utilizzerà un costruttore di mosse se per qualche ragione il RVO fallisce. Se si vuole essere assolutamente sicuri, passare la stringa di output in base al riferimento anziché restituirlo. RVO fa parte dello standard ora, anche se fino a quando si dispone di un compilatore moderno in genere si può fare affidamento su di esso. –

+0

@PeteBaughman e Jonathan Potter: Mi rendo conto che il compilatore può ottimizzare questo aspetto, e questo non può causare un sovraccarico enorme nella mia applicazione, ma non è in grado di farlo in qualche modo un requisito di base? –

risposta

9

La descrizione nella tua domanda è praticamente corretta. Ma è importante capire che questo è il comportamento della macchina C++ astratta .Infatti, la descrizione del comportamento canonica astratto ritorno è ancor meno ottimale

  1. result viene copiato in un oggetto temporaneo intermedio anonimo del tipo std::string. Quel temporaneo persiste dopo il ritorno della funzione.
  2. L'oggetto temporaneo intermedio senza nome viene quindi copiato in greeting dopo il ritorno della funzione.

La maggior parte dei compilatori è sempre stata abbastanza intelligente da eliminare quel temporaneo intermedio in piena conformità con le regole di elision copia classica. Ma anche senza questo temporaneo intermedio il comportamento è sempre stato visto come grossolanamente sub-ottimale. Ecco perché è stata data molta libertà ai compilatori per fornire loro opportunità di ottimizzazione in contesti di ritorno per valore. Originariamente era Return Value Optimization (RVO). Ad essa è stata aggiunta l'Ottimizzazione del valore di ritorno con nome (NRVO). E infine, in C++ 11, spostare la semantica è diventato un ulteriore modo per ottimizzare il comportamento di ritorno in questi casi.

Nota che sotto NRVO nel tuo esempio l'inizializzazione di result con "hello" in realtà luoghi che "hello" direttamente nel greeting fin dall'inizio.

Quindi nel C++ moderno il consiglio migliore è: lasciarlo così com'è e non evitarlo. Restituiscalo per valore. (E preferisce utilizzare immediato inizializzazione nel punto della dichiarazione ogni volta che è possibile, invece di optare per l'inizializzazione di default seguito da assegnazione.)

In primo luogo, RVO del compilatore/capacità NRVO possibile (e volontà) di eliminare la copia. In qualsiasi compilatore che si rispetti RVO/NRVO non è qualcosa di oscuro o secondario. È qualcosa che gli scrittori di compilatori si sforzano attivamente di implementare e implementare correttamente.

In secondo luogo, si sposta sempre la semantica come soluzione di riserva se RVO/NRVO non riesce o non è applicabile. Lo spostamento è naturalmente applicabile nei contesti di ritorno per valore ed è molto meno costoso della copia completa per oggetti non banali. E std::string è un tipo mobile.

+1

Per spiegare la parte di questa risposta sono più d'accordo; non è d'accordo con il sidenote nella domanda, è infatti opportuno fare affidamento su qualche ottimizzazione del compilatore. – kasterma

+0

si si si si un milione di volte si –

3

ci sono molti modi per raggiungere tale:

1) Ritorna alcuni dati dal riferimento

void SomeFunc(std::string& sResult) 
{ 
    sResult = "Hello world!"; 
} 

2) Re gira il puntatore all'oggetto

CSomeHugeClass* SomeFunc() 
{ 
    CSomeHugeClass* pPtr = new CSomeHugeClass(); 
    //... 
    return(pPtr); 
} 

3) C++ 11 potrebbe utilizzare un costruttore di mosse in tali casi. Vedi thisthis e this per le informazioni aggiuntive.

4

Non sono d'accordo con la frase "Penso che sia meglio non fare affidamento sulle ottimizzazioni del compilatore per far funzionare il codice in modo efficiente." Questo è fondamentalmente l'intero lavoro del compilatore. Il tuo compito è scrivere un codice sorgente chiaro, corretto e mantenibile. Per ogni problema di prestazioni che ho dovuto risolvere, ho dovuto risolvere un centinaio di problemi causati da uno sviluppatore che cercava di essere intelligente invece di fare qualcosa di semplice, corretto e manutenibile.

Diamo un'occhiata ad alcune delle cose che potresti fare per cercare di "aiutare" il compilatore e vedere come influiscono sulla manutenibilità del codice sorgente.

  • Si potrebbe restituire i dati tramite riferimento

Ad esempio:

void hello(std::string& outString) 

Restituzione di dati utilizzando un riferimento rende il codice alla chiamata posto difficile da leggere.È quasi impossibile dire quale funzione chiama lo stato mutato come un effetto collaterale e quale no. Anche se stai molto attento a qualificare i riferimenti, sarà difficile da leggere sul sito di chiamata. Considera il seguente esempio:

void hello(std::string& outString); //<-This one could modify outString 
void out(const std::string& toWrite); //<-This one definitely doesn't. 

. . . 

std::string myString; 
hello(myString); //<-This one maybe mutates myString - hard to tell. 
out(myString); //<-This one certainly doesn't, but it looks identical to the one above 

Anche la dichiarazione di saluto non è chiara. Modifica outString, o l'autore è semplicemente sciatto e si è dimenticato di const qualificare il riferimento? Il codice scritto in un functional style è più facile da leggere e comprendere e più difficile da interrompere accidentalmente.

Evitare

  • Si potrebbe restituire un puntatore all'oggetto invece di restituire l'oggetto.

Restituire un puntatore all'oggetto rende difficile accertarsi che il codice sia corretto. A meno che non usi un unique_ptr devi avere fiducia che chiunque usi il tuo metodo sia completo e si assicuri di eliminare il puntatore quando ha finito, ma non è molto RAII. std :: string è già un tipo di wrapper RAII per un char * che astrae i problemi di durata dei dati associati alla restituzione di un puntatore. Restituire un puntatore a una stringa: std :: string semplicemente reintroduce i problemi che std :: string è stato progettato per risolvere. Affidarsi a un essere umano per essere diligente e leggere attentamente la documentazione per la propria funzione e sapere quando cancellare il puntatore e quando non cancellare il puntatore è improbabile che abbia un esito positivo.

Evitare

  • che ci porta a spostare costruttori.

Un costruttore di spostamenti trasferirà semplicemente la proprietà dei dati puntati da "risultato" alla destinazione finale. Successivamente, l'accesso all'oggetto 'risultato' non è valido ma non importa: il metodo è terminato e l'oggetto 'risultato' è andato fuori dal campo di applicazione. Nessuna copia, solo un trasferimento di proprietà del puntatore con semantica chiara.

Normalmente il compilatore chiamerà il costruttore di spostamenti per te. Se sei davvero paranoico (o hai una conoscenza specifica che il compilatore non ti aiuterà) puoi usare std::move.

fare questo uno se possibile

Infine compilatori moderni sono sorprendenti. Con un moderno compilatore C++, il 99% delle volte il compilatore eseguirà una sorta di ottimizzazione per eliminare la copia. L'altro 1% delle volte probabilmente non ha importanza per le prestazioni. In determinate circostanze il compilatore può riscrivere un metodo come std :: string GetString(); annullare GetString (std :: string & outVar); automaticamente. Il codice è ancora facile da leggere, ma nell'assemblaggio finale si ottengono tutti i vantaggi di velocità reali o immaginari di ritorno per riferimento. Non sacrificare la leggibilità e la manutenibilità per le prestazioni, a meno che tu non abbia una conoscenza specifica che la soluzione non soddisfa i requisiti aziendali.