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:
Voi sapete che il metodo swap
(tra cui la copia costruzione obbligatoria quando le RHS è un lvalue) non sarà terribilmente inefficiente .
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:
- Uno scambio non eccedente.
- 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 :
- Implicitamente, che equivale a chiamare l'operatore di assegnazione copia
vector
.
- 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:
- lhs ha una capacità sufficiente tutto il tempo.
- 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.
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
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. –
Un caso in cui è assolutamente necessario il costruttore copia/sposta, per ovvi motivi. – celtschk