Wow, c'è così tanto da pulire qui ...
In primo luogo, la copia e scambio non è sempre il modo corretto per implementare l'assegnazione di copia. Quasi certamente nel caso di dumb_array
, questa è una soluzione subottimale.
L'uso di Copia e scambio è dumb_array
un classico esempio di come mettere l'operazione più costosa con le funzionalità più complete al livello inferiore. È perfetto per i clienti che desiderano la funzionalità più completa e sono disposti a pagare la penalizzazione delle prestazioni. Ottengono esattamente quello che vogliono.
Ma è disastroso per i clienti che non hanno bisogno della funzionalità più completa e cercano invece le massime prestazioni. Per loro dumb_array
è solo un altro pezzo di software che devono riscrivere perché è troppo lento. Se fosse dumb_array
stato progettato in modo diverso, avrebbe potuto soddisfare entrambi i clienti senza compromessi per nessuno dei due.
La chiave per soddisfare entrambi i client è creare le operazioni più veloci al livello più basso, quindi aggiungere API oltre a quella per funzionalità più complete a un costo maggiore. Cioè hai bisogno della forte garanzia di eccezione, bene, paghi per questo. Non ne hai bisogno? Ecco una soluzione più veloce.
Cerchiamo di concretizzare: ecco l'operatore di copia dell'assegnazione di garanzia di eccezione di base veloce per dumb_array
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
Spiegazione:
Una delle cose più costose che puoi fare sull'hardware moderno è fare un viaggio nel mucchio. Tutto ciò che puoi fare per evitare un viaggio al mucchio è tempo e impegno ben spesi. I clienti di dumb_array
potrebbero voler assegnare spesso array della stessa dimensione. E quando lo fanno, tutto ciò che devi fare è un memcpy
(nascosto sotto std::copy
). Non vuoi allocare un nuovo array della stessa dimensione e poi deallocare quello vecchio della stessa dimensione!
Ora per i tuoi clienti che desiderano effettivamente una forte sicurezza dalle eccezioni:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
O forse, se vuoi trarre vantaggio dall'assegnazione degli spostamenti in C ++ 11, dovrebbe essere:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Se dumb_array
i clienti di s apprezzano la velocità, dovrebbero chiamare il file operator=
. Se hanno bisogno di una forte sicurezza dalle eccezioni, ci sono algoritmi generici che possono chiamare che funzioneranno su un'ampia varietà di oggetti e devono essere implementati solo una volta.
Ora torniamo alla domanda originale (che ha un tipo o in questo momento):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
Questa è in realtà una domanda controversa. Alcuni diranno di sì, assolutamente, alcuni diranno di no.
La mia opinione personale è no, non hai bisogno di questo controllo.
Fondamento logico:
Quando un oggetto si lega a un riferimento rvalue è una delle due cose:
- Un temporaneo.
- Un oggetto che il chiamante vuole farti credere è temporaneo.
Se hai un riferimento a un oggetto che è un effettivo temporaneo, per definizione hai un riferimento univoco a quell'oggetto. Non può essere referenziato da nessun'altra parte dell'intero programma. Cioè this == &temporary
non è possibile .
Ora, se il tuo cliente ti ha mentito e ti ha promesso che stai ricevendo un temporaneo quando non lo sei, allora è responsabilità del cliente essere sicuro che non devi preoccupartene. Se vuoi stare davvero attento, credo che questa sarebbe un'implementazione migliore:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
Ad esempio, se ti viene passato un riferimento personale, questo è un bug da parte del client che dovrebbe essere corretto.
Per completezza, ecco un operatore di assegnazione di spostamento per dumb_array
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Nel tipico caso d'uso dell'assegnazione di mosse, *this
sarà un oggetto spostato da e quindi delete [] mArray;
dovrebbe essere un no-op. È fondamentale che le implementazioni rendano l'eliminazione su un nullptr il più velocemente possibile.
Avvertimento:
Alcuni sosterranno che swap(x, x)
è una buona idea o solo un male necessario. E questo, se lo scambio va allo scambio predefinito, può causare un'auto-assegnazione dello spostamento.
Non sono d'accordo che swap(x, x)
sia sempre una buona idea. Se trovato nel mio codice, lo considererò un bug di prestazioni e lo risolverò. Ma nel caso tu voglia consentirlo, renditi conto che swap(x, x)
auto-move-assignemnet solo su un valore spostato da. E nel nostro dumb_array
esempio questo sarà perfettamente innocuo se omettiamo semplicemente l'asserzione o la vincoliamo al caso spostato da:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Se si autoassegnano due mosse da (vuote) dumb_array
, non si fa nulla di sbagliato a parte l'inserimento di istruzioni inutili nel programma. Questa stessa osservazione può essere fatta per la stragrande maggioranza degli oggetti.
<
Aggiornare>
Ho riflettuto ancora su questo problema e ho cambiato leggermente la mia posizione. Ora credo che l'assegnazione dovrebbe essere tollerante rispetto all'assegnazione personale, ma che le condizioni del post sull'assegnazione della copia e sull'assegnazione del trasferimento siano diverse:
Per l'assegnazione di copie:
x = y;
si dovrebbe avere una post-condizione in cui il valore di y
non deve essere alterato. Quando &x == &y
quindi questa postcondizione si traduce in: l'assegnazione di una copia personale non dovrebbe avere alcun impatto sul valore di x
.
Per l'assegnazione del trasloco:
x = std::move(y);
si dovrebbe avere una post-condizione che y
ha uno stato valido ma non specificato. Quando &x == &y
poi questa postcondizione si traduce in: x
ha uno stato valido ma non specificato. Cioè, l'assegnazione di auto mosse non deve essere una no-op. Ma non dovrebbe bloccarsi. Questa post-condizione è coerente con il consentire swap(x, x)
di lavorare solo:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
Quanto sopra funziona, purché x = std::move(x)
non si blocchi. Può partire x
in qualsiasi stato valido ma non specificato.
Vedo tre modi per programmare l'operatore di assegnazione dello spostamento per dumb_array
ottenere ciò:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Quanto sopra attuazione tollera assegnazione sé, ma *this
e other
finiscono per essere una matrice di dimensioni zero dopo l'assegnazione auto-move, a prescindere dal valore originale *this
è. Questo va bene.
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
L'implementazione di cui sopra tollera l'autoassegnazione allo stesso modo dell'operatore di assegnazione della copia, rendendola una no-op. Anche questo va bene.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
Quanto sopra va bene solo se dumb_array
non contiene risorse che dovrebbero essere distrutte "immediatamente". Ad esempio, se l'unica risorsa è la memoria, quanto sopra va bene. Se dumb_array
fosse possibile mantenere i blocchi mutex o lo stato aperto dei file, il client potrebbe ragionevolmente aspettarsi che quelle risorse sul lhs dell'assegnazione di spostamento vengano rilasciate immediatamente e quindi questa implementazione potrebbe essere problematica.
Il costo del primo è di due negozi extra. Il costo del secondo è un test-and-branch. Entrambi funzionano. Entrambi soddisfano tutti i requisiti della Tabella 22 Requisiti MoveAssignable nello standard C ++ 11. Il terzo funziona anche modulo la preoccupazione non-risorsa-memoria.
Tutte e tre le implementazioni possono avere costi diversi a seconda dell'hardware: quanto costa una filiale? Ci sono molti registri o pochissimi?
Il punto è che l'assegnazione di auto-spostamento, a differenza dell'assegnazione di auto-copia, non deve conservare il valore corrente.
<
/Aggiornare>
Un'ultima (si spera) modifica ispirata al commento di Luc Danton:
Se stai scrivendo una classe di alto livello che non gestisce direttamente la memoria (ma potrebbe avere basi o membri che lo fanno), la migliore implementazione dell'assegnazione di mosse è spesso:
Class& operator=(Class&&) = default;
Questo sposterà ogni base e ogni membro a turno e non includerà un this != &other
assegno. Questo ti darà le massime prestazioni e la sicurezza delle eccezioni di base, assumendo che non sia necessario mantenere invarianti tra le tue basi e membri. Per i tuoi clienti che richiedono una forte sicurezza dalle eccezioni, indirizzali verso strong_assign
.