In C ++, è ancora una cattiva pratica restituire un vettore da una funzione?


103

Versione breve: è comune restituire oggetti di grandi dimensioni, come vettori / array, in molti linguaggi di programmazione. Questo stile è ora accettabile in C ++ 0x se la classe ha un costruttore di mosse, o i programmatori C ++ lo considerano strano / brutto / abominio?

Versione lunga: in C ++ 0x è ancora considerata una cattiva forma?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

La versione tradizionale sarebbe simile a questa:

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

Nella versione più recente, il valore restituito da BuildLargeVectorè un rvalue, quindi v sarebbe stato costruito utilizzando il costruttore di spostamento di std::vector, supponendo che (N) RVO non abbia luogo.

Anche prima di C ++ 0x la prima forma sarebbe spesso "efficiente" a causa di (N) RVO. Tuttavia, (N) RVO è a discrezione del compilatore. Ora che abbiamo riferimenti rvalue, è garantito che non verrà eseguita alcuna copia completa.

Modifica : la domanda non riguarda davvero l'ottimizzazione. Entrambe le forme mostrate hanno prestazioni quasi identiche nei programmi del mondo reale. Mentre, in passato, la prima forma avrebbe potuto avere prestazioni peggiori di ordine di grandezza. Di conseguenza, il primo modulo è stato per molto tempo un importante odore di codice nella programmazione C ++. Non più, spero?


18
Chi ha mai detto che all'inizio fosse una cattiva forma?
Edward Strange

7
Certamente era un cattivo odore di codice nei "vecchi tempi", che è da dove vengo. :-)
Nate

1
Spero proprio di sì! Mi piacerebbe vedere il valore di passaggio diventare più popolare. :)
sellibitze

Risposte:


73

Dave Abrahams ha un'analisi piuttosto completa della velocità di passaggio / restituzione dei valori .

Risposta breve, se è necessario restituire un valore, restituire un valore. Non utilizzare riferimenti di output perché il compilatore lo fa comunque. Ovviamente ci sono avvertenze, quindi dovresti leggere quell'articolo.


24
"il compilatore lo fa comunque": il compilatore non è tenuto a farlo == incertezza == cattiva idea (serve il 100% di certezza). "analisi completa" C'è un grosso problema con questa analisi - si basa su caratteristiche del linguaggio non documentate / non standard in un compilatore sconosciuto ("Sebbene l'elisione della copia non sia mai richiesta dallo standard"). Quindi, anche se funziona, non è una buona idea usarlo - non c'è assolutamente alcuna garanzia che funzionerà come previsto, e non c'è alcuna garanzia che ogni compilatore funzionerà sempre in questo modo. Fare affidamento su questo documento è una cattiva pratica di codifica, IMO. Anche se perderai prestazioni.
SigTerm

5
@SigTerm: questo è un commento eccellente !!! la maggior parte dell'articolo a cui si fa riferimento è troppo vaga per essere considerata anche solo per l'uso in produzione. La gente pensa che qualsiasi cosa un autore che ha scritto un libro Red In-Depth sia gospel e dovrebbe essere rispettato senza ulteriori riflessioni o analisi. ATM non esiste un compilatore sul mercato che fornisca una copia elison così varia come gli esempi che Abrahams usa nell'articolo.
Hippicoder

13
@SigTerm, ci sono molte cose che il compilatore non è tenuto a fare, ma presumi che lo faccia comunque. I compilatori non sono "obbligati" a passare x / 2a x >> 1for ints, ma si presume che lo farà. Lo standard non dice nulla su come i compilatori sono richiesti per implementare i riferimenti, ma si presume che vengano gestiti in modo efficiente utilizzando i puntatori. Lo standard non dice nulla anche sulle v-table, quindi non puoi essere sicuro che le chiamate di funzioni virtuali siano efficienti. In sostanza, a volte è necessario riporre un po 'di fiducia nel compilatore.
Peter Alexander

16
@Sig: In realtà è garantito molto poco tranne l'effettivo output del programma. Se vuoi la certezza del 100% su ciò che accadrà il 100% delle volte, allora è meglio passare a una lingua diversa.
Dennis Zickefoose

6
@SigTerm: lavoro sullo "scenario reale". Provo cosa fa il compilatore e ci lavoro. Non esiste "può funzionare più lentamente". Semplicemente non funziona più lentamente perché il compilatore implementa RVO, indipendentemente dal fatto che lo standard lo richieda o meno. Non ci sono se, ma o forse, è solo un semplice fatto.
Peter Alexander

37

Almeno IMO, di solito è una cattiva idea, ma non per motivi di efficienza. È una cattiva idea perché la funzione in questione di solito dovrebbe essere scritta come un algoritmo generico che produce il suo output tramite un iteratore. Quasi qualsiasi codice che accetta o restituisce un contenitore invece di operare su iteratori dovrebbe essere considerato sospetto.

Non fraintendetemi: ci sono volte in cui ha senso passare in giro oggetti simili a raccolte (ad esempio stringhe) ma per l'esempio citato, considererei il passaggio o la restituzione del vettore una cattiva idea.


6
Il problema con l'approccio iteratore è che richiede di creare funzioni e metodi basati su modelli, anche quando il tipo di elemento della raccolta è noto. Questo è irritante e, quando il metodo in questione è virtuale, impossibile. Nota, non sono in disaccordo con la tua risposta di per sé, ma in pratica diventa un po 'macchinosa in C ++.
jon-hanson

22
Non sono d'accordo L'uso di iteratori per l'output a volte è appropriato, ma se non stai scrivendo un algoritmo generico, le soluzioni generiche spesso forniscono un overhead inevitabile che è difficile da giustificare. Sia in termini di complessità del codice che di prestazioni effettive.
Dennis Zickefoose

1
@ Dennis: devo dire che la mia esperienza è stata esattamente l'opposto: scrivo un buon numero di cose come modelli anche quando conosco i tipi coinvolti in anticipo, perché farlo è più semplice e migliora le prestazioni.
Jerry Coffin

9
Personalmente restituisco un container. L'intento è chiaro, il codice è più semplice, non mi preoccupo molto delle prestazioni quando lo scrivo (evito solo la pessimizzazione precoce). Non sono sicuro se l'uso di un iteratore di output renderebbe più chiaro il mio intento ... e ho bisogno di codice non modello il più possibile, perché in un progetto di grandi dimensioni le dipendenze uccidono lo sviluppo.
Matthieu M.

1
@ Dennis: ipotizzerò che concettualmente, non dovresti mai "costruire un contenitore piuttosto che scrivere su un intervallo". Un contenitore è proprio questo: un contenitore. La tua preoccupazione (e la preoccupazione del tuo codice) dovrebbe riguardare il contenuto, non il contenitore.
Jerry Coffin

18

Il succo è:

Copy Elision e RVO possono evitare le "copie spaventose" (il compilatore non è necessario per implementare queste ottimizzazioni e in alcune situazioni non può essere applicato)

I riferimenti C ++ 0x RValue consentono implementazioni di stringhe / vettori che lo garantiscono .

Se puoi abbandonare i vecchi compilatori / implementazioni STL, restituisci i vettori liberamente (e assicurati che anche i tuoi oggetti lo supportino). Se la tua base di codice deve supportare compilatori "minori", attieniti al vecchio stile.

Sfortunatamente, questo ha una grande influenza sulle tue interfacce. Se C ++ 0x non è un'opzione e sono necessarie garanzie, è possibile utilizzare invece oggetti con conteggio dei riferimenti o copia su scrittura in alcuni scenari. Tuttavia, hanno degli svantaggi con il multithreading.

(Vorrei che solo una risposta in C ++ fosse semplice, diretta e senza condizioni).


11

Infatti, dal momento che C ++ 11, il costo di copiare l' std::vectorè andato nella maggior parte dei casi.

Tuttavia, si dovrebbe tenere presente che il costo della costruzione del nuovo vettore (quindi della sua distruzione ) esiste ancora e l'uso dei parametri di output invece di restituire per valore è ancora utile quando si desidera riutilizzare la capacità del vettore. Ciò è documentato come un'eccezione in F.20 delle linee guida di base C ++.

Confrontiamo:

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

con:

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

Supponiamo ora di dover chiamare questi metodi numItervolte in un ciclo stretto ed eseguire alcune azioni. Ad esempio, calcoliamo la somma di tutti gli elementi.

Usando BuildLargeVector1, faresti:

size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
    std::vector<int> v = BuildLargeVector1(vecSize);
    sum1 = std::accumulate(v.begin(), v.end(), sum1);
}

Usando BuildLargeVector2, faresti:

size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
    BuildLargeVector2(/*out*/ v, vecSize);
    sum2 = std::accumulate(v.begin(), v.end(), sum2);
}

Nel primo esempio, si verificano molte allocazioni / deallocazioni dinamiche non necessarie, che vengono evitate nel secondo esempio utilizzando un parametro di output alla vecchia maniera, riutilizzando la memoria già allocata. La validità o meno di questa ottimizzazione dipende dal costo relativo dell'allocazione / deallocazione rispetto al costo del calcolo / della modifica dei valori.

Prova delle prestazioni

Giochiamo con i valori di vecSizee numIter. Manterremo vecSize * numIter costante in modo che "in teoria", dovrebbe richiedere lo stesso tempo (= c'è lo stesso numero di assegnazioni e aggiunte, con gli stessi identici valori), e la differenza di tempo può venire solo dal costo di allocazioni, deallocazioni e migliore utilizzo della cache.

Più specificamente, usiamo vecSize * numIter = 2 ^ 31 = 2147483648, perché ho 16 GB di RAM e questo numero garantisce che non vengano allocati più di 8 GB (sizeof (int) = 4), assicurandomi che non sto scambiando su disco ( tutti gli altri programmi erano chiusi, avevo ~ 15 GB disponibili durante l'esecuzione del test).

Ecco il codice:

#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

class Timer {
    using clock = std::chrono::steady_clock;
    using seconds = std::chrono::duration<double>;
    clock::time_point t_;

public:
    void tic() { t_ = clock::now(); }
    double toc() const { return seconds(clock::now() - t_).count(); }
};

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

int main() {
    Timer t;

    size_t vecSize = size_t(1) << 31;
    size_t numIter = 1;

    std::cout << std::setw(10) << "vecSize" << ", "
              << std::setw(10) << "numIter" << ", "
              << std::setw(10) << "time1" << ", "
              << std::setw(10) << "time2" << ", "
              << std::setw(10) << "sum1" << ", "
              << std::setw(10) << "sum2" << "\n";

    while (vecSize > 0) {

        t.tic();
        size_t sum1 = 0;
        {
            for (int i = 0; i < numIter; ++i) {
                std::vector<int> v = BuildLargeVector1(vecSize);
                sum1 = std::accumulate(v.begin(), v.end(), sum1);
            }
        }
        double time1 = t.toc();

        t.tic();
        size_t sum2 = 0;
        {
            std::vector<int> v;
            for (int i = 0; i < numIter; ++i) {
                BuildLargeVector2(/*out*/ v, vecSize);
                sum2 = std::accumulate(v.begin(), v.end(), sum2);
            }
        } // deallocate v
        double time2 = t.toc();

        std::cout << std::setw(10) << vecSize << ", "
                  << std::setw(10) << numIter << ", "
                  << std::setw(10) << std::fixed << time1 << ", "
                  << std::setw(10) << std::fixed << time2 << ", "
                  << std::setw(10) << sum1 << ", "
                  << std::setw(10) << sum2 << "\n";

        vecSize /= 2;
        numIter *= 2;
    }

    return 0;
}

E questo è il risultato:

$ g++ -std=c++11 -O3 main.cpp && ./a.out
   vecSize,    numIter,      time1,      time2,       sum1,       sum2
2147483648,          1,   2.360384,   2.356355, 2147483648, 2147483648
1073741824,          2,   2.365807,   1.732609, 2147483648, 2147483648
 536870912,          4,   2.373231,   1.420104, 2147483648, 2147483648
 268435456,          8,   2.383480,   1.261789, 2147483648, 2147483648
 134217728,         16,   2.395904,   1.179340, 2147483648, 2147483648
  67108864,         32,   2.408513,   1.131662, 2147483648, 2147483648
  33554432,         64,   2.416114,   1.097719, 2147483648, 2147483648
  16777216,        128,   2.431061,   1.060238, 2147483648, 2147483648
   8388608,        256,   2.448200,   0.998743, 2147483648, 2147483648
   4194304,        512,   0.884540,   0.875196, 2147483648, 2147483648
   2097152,       1024,   0.712911,   0.716124, 2147483648, 2147483648
   1048576,       2048,   0.552157,   0.603028, 2147483648, 2147483648
    524288,       4096,   0.549749,   0.602881, 2147483648, 2147483648
    262144,       8192,   0.547767,   0.604248, 2147483648, 2147483648
    131072,      16384,   0.537548,   0.603802, 2147483648, 2147483648
     65536,      32768,   0.524037,   0.600768, 2147483648, 2147483648
     32768,      65536,   0.526727,   0.598521, 2147483648, 2147483648
     16384,     131072,   0.515227,   0.599254, 2147483648, 2147483648
      8192,     262144,   0.540541,   0.600642, 2147483648, 2147483648
      4096,     524288,   0.495638,   0.603396, 2147483648, 2147483648
      2048,    1048576,   0.512905,   0.609594, 2147483648, 2147483648
      1024,    2097152,   0.548257,   0.622393, 2147483648, 2147483648
       512,    4194304,   0.616906,   0.647442, 2147483648, 2147483648
       256,    8388608,   0.571628,   0.629563, 2147483648, 2147483648
       128,   16777216,   0.846666,   0.657051, 2147483648, 2147483648
        64,   33554432,   0.853286,   0.724897, 2147483648, 2147483648
        32,   67108864,   1.232520,   0.851337, 2147483648, 2147483648
        16,  134217728,   1.982755,   1.079628, 2147483648, 2147483648
         8,  268435456,   3.483588,   1.673199, 2147483648, 2147483648
         4,  536870912,   5.724022,   2.150334, 2147483648, 2147483648
         2, 1073741824,  10.285453,   3.583777, 2147483648, 2147483648
         1, 2147483648,  20.552860,   6.214054, 2147483648, 2147483648

Risultati benchmark

(Intel i7-7700K @ 4,20 GHz; 16 GB DDR4 2400 Mhz; Kubuntu 18.04)

Notazione: mem (v) = v.size () * sizeof (int) = v.size () * 4 sulla mia piattaforma.

Non sorprende che quando numIter = 1(cioè, mem (v) = 8GB), i tempi siano perfettamente identici. Infatti, in entrambi i casi stiamo allocando solo una volta un enorme vettore di 8 GB in memoria. Ciò dimostra anche che non è avvenuta alcuna copia durante l'utilizzo di BuildLargeVector1 (): non avrei abbastanza RAM per fare la copia!

Quando numIter = 2, riutilizzare la capacità del vettore invece di riallocare un secondo vettore è 1,37 volte più veloce.

Quando numIter = 256, riutilizzare la capacità del vettore (invece di allocare / deallocare un vettore più e più volte 256 volte ...) è 2.45 volte più veloce :)

Possiamo notare che time1 è praticamente costante da numIter = 1a numIter = 256, il che significa che allocare un enorme vettore di 8 GB è più o meno costoso quanto allocare 256 vettori di 32 MB. Tuttavia, l'allocazione di un enorme vettore di 8 GB è decisamente più costoso rispetto all'allocazione di un vettore di 32 MB, quindi il riutilizzo della capacità del vettore fornisce miglioramenti delle prestazioni.

Da numIter = 512(mem (v) = 16MB) a numIter = 8M(mem (v) = 1kB) è il punto debole: entrambi i metodi sono esattamente altrettanto veloci e più veloci di tutte le altre combinazioni di numIter e vecSize. Ciò probabilmente ha a che fare con il fatto che la dimensione della cache L3 del mio processore è di 8 MB, quindi il vettore si adatta perfettamente alla cache. Non spiego davvero perché il salto improvviso di time1è per mem (v) = 16 MB, sembrerebbe più logico accadere subito dopo, quando mem (v) = 8 MB. Notare che sorprendentemente, in questo punto debole, non riutilizzare la capacità è in effetti leggermente più veloce! Non lo spiego davvero.

Quando le numIter > 8Mcose iniziano a diventare brutte. Entrambi i metodi diventano più lenti, ma la restituzione del vettore per valore diventa ancora più lenta. Nel peggiore dei casi, con un vettore contenente un solo singolo int, riutilizzare la capacità invece di restituire per valore è 3,3 volte più veloce. Presumibilmente, ciò è dovuto ai costi fissi di malloc () che iniziano a dominare.

Nota come la curva per il tempo2 è più liscia della curva per il tempo1: non solo il riutilizzo della capacità del vettore è generalmente più veloce, ma forse ancora più importante, è più prevedibile .

Si noti inoltre che nel punto debole, siamo stati in grado di eseguire 2 miliardi di aggiunte di interi a 64 bit in ~ 0,5 secondi, il che è abbastanza ottimale su un processore a 64 bit da 4,2 Ghz. Potremmo fare di meglio parallelizzando il calcolo per utilizzare tutti gli 8 core (il test sopra utilizza solo un core alla volta, cosa che ho verificato rieseguendo il test mentre monitorava l'utilizzo della CPU). Le migliori prestazioni si ottengono quando mem (v) = 16kB, che è l'ordine di grandezza della cache L1 (la cache dati L1 per l'i7-7700K è 4x32kB).

Ovviamente, le differenze diventano sempre meno rilevanti quanto più calcoli devi effettivamente fare sui dati. Di seguito sono riportati i risultati se sostituiamo sum = std::accumulate(v.begin(), v.end(), sum);con for (int k : v) sum += std::sqrt(2.0*k);:

Benchmark 2

conclusioni

  1. L'utilizzo dei parametri di output anziché la restituzione in base al valore può fornire miglioramenti delle prestazioni riutilizzando la capacità.
  2. Su un moderno computer desktop, questo sembra applicabile solo a vettori grandi (> 16 MB) e piccoli vettori (<1kB).
  3. Evita di allocare milioni / miliardi di piccoli vettori (<1kB). Se possibile, riutilizza la capacità o, meglio ancora, progetta la tua architettura in modo diverso.

I risultati possono differire su altre piattaforme. Come al solito, se le prestazioni sono importanti, scrivi benchmark per il tuo caso d'uso specifico.


6

Continuo a pensare che sia una cattiva pratica, ma vale la pena notare che il mio team utilizza MSVC 2008 e GCC 4.1, quindi non stiamo usando i compilatori più recenti.

In precedenza, molti degli hotspot mostrati in vtune con MSVC 2008 si riducevano alla copia di stringhe. Avevamo un codice come questo:

String Something::id() const
{
    return valid() ? m_id: "";
}

... nota che abbiamo utilizzato il nostro tipo String (questo era richiesto perché forniamo un kit di sviluppo software in cui gli autori di plugin potrebbero utilizzare compilatori diversi e quindi implementazioni diverse e incompatibili di std :: string / std :: wstring).

Ho apportato una semplice modifica in risposta alla sessione di profiling del campionamento del grafico delle chiamate che mostra che String :: String (const String &) impiega una quantità di tempo significativa. I metodi come nell'esempio precedente sono stati i maggiori contributori (in realtà la sessione di profiling ha mostrato che l'allocazione e la deallocazione della memoria sono uno dei più grandi hotspot, con il costruttore di copie String che è il contributore principale per le allocazioni).

Il cambiamento che ho fatto è stato semplice:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

Eppure questo ha fatto la differenza! L'hotspot è scomparso nelle successive sessioni di profiler e in aggiunta a questo eseguiamo molti test approfonditi per tenere traccia delle prestazioni delle nostre applicazioni. Tutti i tipi di tempi dei test delle prestazioni sono diminuiti in modo significativo dopo queste semplici modifiche.

Conclusione: non stiamo usando i compilatori più recenti in assoluto, ma non possiamo ancora dipendere dal fatto che il compilatore ottimizzi la copia per restituire in base al valore in modo affidabile (almeno non in tutti i casi). Questo potrebbe non essere il caso per coloro che utilizzano compilatori più recenti come MSVC 2010. Non vedo l'ora di usare C ++ 0x e usare semplicemente riferimenti rvalue e non dovrò mai preoccuparmi che stiamo pessimizzando il nostro codice restituendo complesso classi per valore.

[Modifica] Come ha sottolineato Nate, RVO si applica alla restituzione di provvisori creati all'interno di una funzione. Nel mio caso, non c'erano tali temporanei (ad eccezione del ramo non valido in cui costruiamo una stringa vuota) e quindi RVO non sarebbe stato applicabile.


3
Questo è il punto: RVO dipende dal compilatore, ma un compilatore C ++ 0x deve usare la semantica di spostamento se decide di non utilizzare RVO (supponendo che ci sia un costruttore di spostamento). L'utilizzo dell'operatore trigraph sconfigge RVO. Vedi cpp-next.com/archive/2009/09/move-it-with-rvalue-references a cui si riferiva Peter. Ma il tuo esempio non è comunque idoneo per la semantica di spostamento perché non stai restituendo un temporaneo.
Nate

@ Stinky472: la restituzione di un membro per valore sarebbe stata sempre più lenta del riferimento. I riferimenti Rvalue sarebbero ancora più lenti rispetto alla restituzione di un riferimento al membro originale (se il chiamante può accettare un riferimento invece di aver bisogno di una copia). Inoltre, ci sono ancora molte volte che puoi salvare, sopra i riferimenti rvalue, perché hai contesto. Ad esempio, puoi fare String newstring; newstring.resize (string1.size () + string2.size () + ...); nuova stringa + = stringa1; nuova stringa + = stringa2; ecc. Questo è ancora un risparmio sostanziale rispetto ai valori.
Puppy

@DeadMG un risparmio sostanziale rispetto all'operatore binario + anche con compilatori C ++ 0x che implementano RVO? Se è così, è un peccato. Poi di nuovo questo ha senso dal momento che dobbiamo ancora creare un temporaneo per calcolare la stringa concatenata mentre + = può concatenarsi direttamente a newstring.
stinky472

Che ne dici di un caso come: string newstr = str1 + str2; Su un compilatore che implementa la semantica di spostamento, sembra che dovrebbe essere veloce quanto o addirittura più veloce di: string newstr; newstr + = str1; newstr + = str2; Nessuna riserva, per così dire (presumo che tu intendessi prenotare invece di ridimensionare).
stinky472

5
@Nate: Penso che tu stia confondendo trigrafi come <::o ??!con l' operatore condizionale ?: (a volte chiamato operatore ternario ).
fredoverflow

3

Solo per fare un pò di nitidezza: non è comune in molti linguaggi di programmazione restituire array dalle funzioni. Nella maggior parte di essi, viene restituito un riferimento all'array. In C ++, tornerebbe l'analogia più vicinaboost::shared_array


4
@Billy: std :: vector è un tipo di valore con semantica di copia. L'attuale standard C ++ non offre garanzie che (N) RVO venga mai applicato, e in pratica ci sono molti scenari di vita reale in cui non lo è.
Nemanja Trifunovic

3
@Billy: Ancora una volta, ci sono alcuni scenari molto reali in cui anche gli ultimi compilatori non applicano NRVO: efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic

3
@Billy ONeal: il 99% non è abbastanza, hai bisogno del 100%. Legge di Murphy - "se qualcosa può andare storto, lo farà". L'incertezza va bene se hai a che fare con una sorta di logica confusa, ma non è una buona idea per scrivere software tradizionale. Se c'è anche l'1% di possibilità che il codice non funzioni come pensi, allora dovresti aspettarti che questo codice introduca un bug critico che ti farà licenziare. Inoltre non è una caratteristica standard. Usare funzionalità non documentate è una cattiva idea: se in un anno dalla conoscenza del compilatore abbandonerà la funzionalità (non è richiesta dallo standard, giusto?), Sarai tu quello nei guai.
SigTerm

4
@SigTerm: Se stessimo parlando di correttezza del comportamento, sarei d'accordo con te. Tuttavia, stiamo parlando di un'ottimizzazione delle prestazioni. Cose del genere vanno bene con una certezza inferiore al 100%.
Billy ONeal

2
@Nemanja: non vedo su cosa si "invoca" qui. La tua app funziona allo stesso modo, indipendentemente dall'utilizzo di RVO o NRVO. Se vengono utilizzati, tuttavia, funzionerà più velocemente. Se la tua app è troppo lenta su una particolare piattaforma e l'hai rintracciata per restituire la copia del valore, cambiarla con tutti i mezzi, ma ciò non cambia il fatto che la best practice è ancora quella di utilizzare il valore restituito. Se è assolutamente necessario assicurarsi che non venga eseguita alcuna copia, avvolgere il vettore in a shared_ptre chiamarlo un giorno.
Billy ONeal

2

Se le prestazioni sono un vero problema, dovresti renderti conto che la semantica dello spostamento non è sempre più veloce della copia. Ad esempio, se si dispone di una stringa che utilizza l' ottimizzazione delle stringhe piccole, per le stringhe piccole un costruttore di spostamenti deve eseguire esattamente la stessa quantità di lavoro di un normale costruttore di copie.


1
NRVO non scompare solo perché sono stati aggiunti i costruttori di mosse.
Billy ONeal

1
@Billy, vero ma irrilevante, la domanda era: C ++ 0x ha cambiato le migliori pratiche e NRVO non è cambiato a causa di C ++ 0x
Motti
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.