2013-08-18 14 views
21

ho visto detto che un operator= scritto per prendere un parametro dello stesso tipo per valore serve sia come copia operatore di assegnazione e spostare operatore di assegnamento in C++ 11:Quando il sovraccarico passa per riferimento (valore l e valore r) preferito al valore pass-by?

Foo& operator=(Foo f) 
{ 
    swap(f); 
    return *this; 
} 

Dove l'alternativa sarebbe più del doppio dei linee con un sacco di ripetizioni codice e potenziale di errore:

Foo& operator=(const Foo& f) 
{ 
    Foo f2(f); 
    swap(f2); 
    return *this; 
} 

Foo& operator=(Foo&& f) 
{ 
    Foo f2(std::move(f)); 
    swap(f2); 
    return *this; 
} 

In quali circostanze è il ref-a-const e sovraccarichi valore r preferibile passaggio per valore, o quando è è necessario? Sto pensando di std::vector::push_back, ad esempio che è definita come due overload:

void push_back (const value_type& val); 
void push_back (value_type&& val); 

a seguito del primo esempio in cui passaggio per valore serve come assegnazione copia operatore e spostare operatore di assegnazione, non poteva essere push_back definito in lo standard per essere una singola funzione?

void push_back (value_type val); 
+0

La forma di valore per-può essere più costosa (due mosse invece di una) quando l'oggetto è pesante da spostare (ad esempio contiene molti membri di dati). – Angew

+0

Questo potrebbe essere in parte un motivo storico, dato che 'std :: vector' aveva già il' T const & 'overload in C++ 03. Potrebbe esserci del codice là fuori che dipende da quel sovraccarico esistente (ad esempio qualcuno ha preso l'indirizzo della funzione membro). Si noti inoltre che nello standard non è necessario ottimizzare le linee di codice che dovranno essere implementate, poiché è scritto una volta dall'implementatore del compilatore ma usato quasi ovunque. Il costo aggiuntivo di sviluppo è trascurabile. –

+0

Un caso in cui è assolutamente necessario il costruttore copia/sposta, per ovvi motivi. – celtschk

risposta

26

Per i tipi la cui copia assegnazione operatore può riciclare le risorse, scambiando con una copia non è quasi mai il modo migliore per attuare l'operatore di assegnamento copia. Per esempio cerca std::vector:

Questa classe gestisce un buffer di dimensioni dinamicamente e mantiene sia (lunghezza massima del buffer può contenere) capacity, e una (la lunghezza attuale) size. Se l'operatore di assegnazione copia vector è implementato swap, quindi non importa quale, un nuovo buffer viene sempre assegnato se il rhs.size() != 0.

Tuttavia, se lhs.capacity() >= rhs.size(), non è necessario assegnare alcun nuovo buffer. Si può semplicemente assegnare/costruire gli elementi da rhs a lhs. Quando il tipo di elemento è banalmente riproducibile, questo può ridursi a nient'altro che a memcpy. Questo può essere molto, molto più veloce di allocare e deallocare un buffer.

Stesso problema per std::string.

Stesso problema per MyType quando MyType ha membri di dati che sono std::vector e/o std::string.

Ci sono solo 2 volte si vuole considerare l'implementazione di assegnamento per copia con swap:

  1. Voi sapete che il metodo swap (tra cui la copia costruzione obbligatoria quando le RHS è un lvalue) non sarà terribilmente inefficiente .

  2. Si sa che sarà sempre necessario che l'operatore di assegnazione delle copie abbia la garanzia di sicurezza eccezionale.

Se non siete sicuri circa 2, in altre parole, si pensa l'operatore di assegnazione di copia potrebbe volte bisogno della garanzia forte sicurezza rispetto alle eccezioni, non implementare l'assegnazione in termini di swap. Per i tuoi clienti è facile ottenere la stessa garanzia se fornisci uno di:

  1. Uno scambio non eccedente.
  2. Un operatore di assegnazione di spostamento no.

Ad esempio:

template <class T> 
T& 
strong_assign(T& x, T y) 
{ 
    using std::swap; 
    swap(x, y); 
    return x; 
} 

o:

template <class T> 
T& 
strong_assign(T& x, T y) 
{ 
    x = std::move(y); 
    return x; 
} 

Ora ci saranno alcuni tipi cui attuazione assegnamento per copia con lo scambio avrà senso. Tuttavia questi tipi costituiranno l'eccezione, non la regola.

On:

void push_back(const value_type& val); 
void push_back(value_type&& val); 

Immaginate vector<big_legacy_type> dove:

class big_legacy_type 
{ 
public: 
     big_legacy_type(const big_legacy_type&); // expensive 
     // no move members ... 
}; 

Se solo avessimo:

void push_back(value_type val); 

Poi push_back ing un lvalue big_legacy_type in un vector richiederebbe 2 copie invece di 1, anche se capacity era sufficiente. Sarebbe un disastro, per quanto riguarda le prestazioni.

Aggiornamento

Ecco un HelloWorld che si dovrebbe essere in grado di funzionare su qualsiasi piattaforma C++ 11 conforme:

#include <vector> 
#include <random> 
#include <chrono> 
#include <iostream> 

class X 
{ 
    std::vector<int> v_; 
public: 
    explicit X(unsigned s) : v_(s) {} 

#if SLOW_DOWN 
    X(const X&) = default; 
    X(X&&) = default; 
    X& operator=(X x) 
    { 
     v_.swap(x.v_); 
     return *this; 
    } 
#endif 
}; 

std::mt19937_64 eng; 
std::uniform_int_distribution<unsigned> size(0, 1000); 

std::chrono::high_resolution_clock::duration 
test(X& x, const X& y) 
{ 
    auto t0 = std::chrono::high_resolution_clock::now(); 
    x = y; 
    auto t1 = std::chrono::high_resolution_clock::now(); 
    return t1-t0; 
} 

int 
main() 
{ 
    const int N = 1000000; 
    typedef std::chrono::duration<double, std::nano> nano; 
    nano ns(0); 
    for (int i = 0; i < N; ++i) 
    { 
     X x1(size(eng)); 
     X x2(size(eng)); 
     ns += test(x1, x2); 
    } 
    ns /= N; 
    std::cout << ns.count() << "ns\n"; 
} 

ho codificato X 's copia assegnazione dell'operatore due modi :

  1. Implicitamente, che equivale a chiamare l'operatore di assegnazione copia vector.
  2. Con l'idioma di copia/scambio, suggestivamente sotto la macro SLOW_DOWN. Ho pensato di nominarlo SLEEP_FOR_AWHILE, ma in questo modo è in realtà molto peggio delle dichiarazioni di sospensione se si utilizza un dispositivo alimentato a batteria.

Il test costruisce alcuni numeri da vector<int> s in modo casuale tra 0 e 1000 e li assegna un milione di volte. Ogni volta, somma i tempi, quindi trova il tempo medio in nanosecondi in virgola mobile e lo stampa. Se due chiamate consecutive al tuo orologio ad alta risoluzione non restituiscono qualcosa in meno di 100 nanosecondi, potresti voler aumentare la lunghezza dei vettori.

Qui sono i miei risultati:

$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp 
$ a.out 
428.348ns 
$ a.out 
438.5ns 
$ a.out 
431.465ns 
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp 
$ a.out 
617.045ns 
$ a.out 
616.964ns 
$ a.out 
618.808ns 

sto vedendo un calo di prestazioni del 43% per l'idioma di copia/swap con questo semplice test. YMMV.

Il test sopra riportato, in media, ha una capacità sufficiente per l'altra metà del tempo. Se prendiamo questo a uno dei due estremi:

  1. lhs ha una capacità sufficiente tutto il tempo.
  2. lhs non ha capacità sufficiente in qualsiasi momento.

quindi il vantaggio prestazionale dell'assegnazione di copia predefinita rispetto all'idioma di copia/scambio varia da circa 560% a 0%. L'idioma di copia/swap non è mai più veloce e può essere notevolmente più lento (per questo test).

Vuoi velocità? Misurare.

+0

Copierà l'aiuto di elision? Rimuoverebbe la * altra * copia nel secondo caso? Sto cercando di concordare con http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ Grazie – cdmh

+8

Non aiuta quando il rhs è un lvalue. Tecnicamente non c'è niente di sbagliato in quell'articolo. Tuttavia lascia un'impressione sbagliata: copiare sempre in base al valore è un consiglio terribile. Del resto, sempre "qualsiasi cosa" è un consiglio terribile. L'articolo dovrebbe lasciarti con l'impressione che ** a volte ** un parametro per valore può essere ok, o anche la cosa migliore. Ma troppe persone hanno trascurato il "a volte" perché l'articolo non lo chiarisce. Mantieni il pass-by-value nella tua casella degli strumenti. Ma usarlo per impostazione predefinita, senza progettazione, richiede problemi di prestazioni. –

+0

@HowardHinnant Sono uno dei *** "a volte" *** * significa * *** "sempre" *** pazzi. Grazie per il punto. – Manu343726

Problemi correlati