Sposta l’operatore di assegnazione e `if (this! = & Rhs)`

Nell’operatore di assegnazione di una class, di solito è necessario verificare se l’object da assegnare è l’object di richiamo in modo da non rovinare tutto:

Class& Class::operator=(const Class& rhs) { if (this != &rhs) { // do the assignment } return *this; } 

Hai bisogno della stessa cosa per l’operatore di assegnazione del movimento? C’è mai una situazione in cui this == &rhs sarebbe vero?

 ? Class::operator=(Class&& rhs) { ? } 

Wow, c’è così tanto da pulire qui …

Innanzitutto, Copy and Swap non è sempre il modo corretto di implementare Copy Assignment. Quasi certamente nel caso di dumb_array , questa è una soluzione non ottimale.

L’uso di Copy and Swap è per dumb_array è un classico esempio di mettere l’operazione più costosa con le funzionalità più complete nel livello inferiore. È perfetto per i clienti che desiderano la funzionalità più completa e sono disposti a pagare la sanzione 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 software che devono riscrivere perché troppo lento. Se dumb_array fosse stato progettato diversamente, avrebbe potuto soddisfare entrambi i clienti senza compromessi con nessuno dei due client.

La chiave per soddisfare entrambi i clienti è quella di creare le operazioni più veloci al livello più basso, e quindi di aggiungere l’API in aggiunta a quelle più complete con maggiori costi. Vale a dire la forte garanzia di eccezione, va bene, si paga per questo. Non ne hai bisogno? Ecco una soluzione più veloce.

Diamo concretezza: ecco l’operatore di Assegnazione copia a garanzia di eccezioni rapida e di base 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 con l’hardware moderno è fare un viaggio nell’heap. Qualunque cosa tu possa fare per evitare un viaggio nel mucchio è tempo e sforzo ben spesi. I clienti di dumb_array potrebbero voler spesso assegnare array della stessa dimensione. E quando lo fanno, tutto ciò che devi fare è una memcpy (nascosta sotto std::copy ). Non vuoi assegnare un nuovo array della stessa dimensione e quindi rilasciarne uno vecchio della stessa dimensione!

Ora per i tuoi clienti che vogliono davvero una forte sicurezza delle eccezioni:

 template  C& strong_assign(C& lhs, C rhs) { swap(lhs, rhs); return lhs; } 

O forse se vuoi sfruttare l’assegnazione di spostamento in C ++ 11 che dovrebbe essere:

 template  C& strong_assign(C& lhs, C rhs) { lhs = std::move(rhs); return lhs; } 

Se i client di dumb_array valutano la velocità, dovrebbero chiamare l’ operator= . Se hanno bisogno di una forte sicurezza di eccezione, 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 object si lega a un riferimento di rvalue è una delle due cose:

  1. Un temporaneo.
  2. Un object che il chiamante vuole che tu creda sia temporaneo.

Se si dispone di un riferimento a un object che è un temporaneo effettivo, quindi per definizione, si dispone di un riferimento univoco a tale object. Non può essere referenziato da nessun’altra parte dell’intero programma. Cioè this == &temporary non è ansible .

Ora se il tuo cliente ti ha mentito e ti ha promesso che stai diventando temporaneo quando non lo sei, allora è responsabilità del cliente essere sicuro che non ti debba importare. Se vuoi essere molto attento, credo che sarebbe un’implementazione migliore:

 Class& Class::operator=(Class&& other) { assert(this != &other); // ... return *this; } 

Ad esempio, se si è passati ad un riferimento personale, si tratta di un bug da parte del client che dovrebbe essere corretto.

Per completezza, ecco un operatore di assegnazione del movimento 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 dello spostamento, *this sarà un object spostato da e quindi delete [] mArray; dovrebbe essere un no-op. È fondamentale che le implementazioni effettuino l’eliminazione su nullptr il più velocemente ansible.

Avvertimento:

Alcuni sosterranno che swap(x, x) è una buona idea, o solo un male necessario. E questo, se lo scambio passa allo scambio predefinito, può causare un’assegnazione auto-spostamento.

Non sono d’accordo che lo swap(x, x) sia sempre una buona idea. Se trovato nel mio codice, lo considererò un bug delle prestazioni e lo risolverò. Ma nel caso in cui si voglia permetterlo, rendersi conto che lo swap(x, x) fa solo auto-move-assignemnet su un valore spostato da. E nel nostro esempio dumb_array questo sarà perfettamente innocuo se semplicemente omettiamo l’asserzione o la dumb_array 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 assegni autonomamente due dumb_array di dumb_array spostati da (vuoti), non fai nulla di sbagliato oltre a inserire istruzioni inutili nel tuo programma. Questa stessa osservazione può essere fatta per la stragrande maggioranza degli oggetti.

< Aggiornamento >

Ho dato un po 'più di riflessione al problema e ho cambiato la mia posizione. Ora ritengo che l'incarico debba essere tollerante rispetto all'autocertificazione, ma che le condizioni di post sull'assegnazione delle copie e sull'assegnazione delle mosse siano diverse:

Per l'assegnazione della copia:

 x = y; 

si dovrebbe avere una post-condizione che il valore di y non dovrebbe essere alterato. Quando &x == &y allora questa postazione si traduce in: l'assegnazione del self copy non dovrebbe avere alcun impatto sul valore di x .

Per l'assegnazione del movimento:

 x = std::move(y); 

si dovrebbe avere una post-condizione che abbia uno stato valido ma non specificato. Quando &x == &y allora questa postazione si traduce in: x ha uno stato valido ma non specificato. Cioè l'assegnazione del movimento personale non deve essere un no-op. Ma non dovrebbe bloccarsi. Questa post-condizione è coerente con consentire a swap(x, x) di funzionare solo:

 template  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, a condizione che x = std::move(x) non si blocchi. Può lasciare x in qualsiasi stato valido ma non specificato.

Vedo tre modi per programmare l'operatore di assegnazione del movimento per dumb_array per 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; } 

L'implementazione di cui sopra tollera l'auto assegnazione, ma *this e other finiscono per essere un array di dimensioni zero dopo l'assegnazione del movimento autonomo, indipendentemente dal valore originale di *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 delle copie, rendendolo un no-op. Anche questo va bene.

 dumb_array& operator=(dumb_array&& other) { swap(other); return *this; } 

Quanto sopra è ok 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 potesse contenere blocchi mutex o lo stato di apertura dei file, il client potrebbe ragionevolmente aspettarsi che le risorse sul lhs dell'assegnazione dello spostamento vengano immediatamente rilasciate 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 dei requisiti MoveAssignable di Table 22 nello standard C ++ 11. Il terzo funziona anche modulo non-memoria-preoccupazione-preoccupazione.

Tutte e tre le implementazioni possono avere costi diversi a seconda dell'hardware: quanto è costoso un ramo? Ci sono molti registri o pochissimi?

Il take-away è che l'auto-spostamento-assegnazione, a differenza dell'assegnazione auto-copia, non deve conservare il valore corrente.

< / Update >

Una modifica finale (si spera) ispirata al commento di Luc Danton:

Se stai scrivendo una class di alto livello che non gestisce direttamente la memoria (ma potrebbe avere basi o membri che lo fanno), la migliore implementazione dell'assegnazione delle mosse è spesso:

 Class& operator=(Class&&) = default; 

Questo si sposterà a turno ogni base e ogni membro, e non includerà this != &other controllo. Questo ti darà le massime prestazioni e la sicurezza di base delle eccezioni, supponendo che non sia necessario mantenere invarianti tra le tue basi e membri. Per i tuoi clienti che richiedono una forte sicurezza delle eccezioni, indirizzali verso strong_assign .

Innanzitutto, hai sbagliato la firma dell’operatore di assegnazione del movimento. Poiché le mosse rubano risorse dall’object sorgente, l’origine deve essere un riferimento a valore non const .

 Class &Class::operator=( Class &&rhs ) { //... return *this; } 

Si noti che si ritorna ancora tramite un riferimento di valore l (non costante).

Per entrambi i tipi di assegnazione diretta, lo standard non è quello di verificare l’autoassegnazione, ma per assicurarsi che un autoassegnazione non causi un crash-and-burn. Generalmente, nessuno fa esplicitamente x = x o y = std::move(y) chiama, ma l’aliasing, specialmente attraverso più funzioni, può portare a = b o c = std::move(d) in essere auto-assegnazioni. Un controllo esplicito per l’autoassegnazione, ovvero this == &rhs , che salta la carne della funzione quando vero è un modo per garantire la sicurezza di autoassegnazione. Ma è uno dei peggiori modi, dal momento che ottimizza un caso raro (si spera) mentre si tratta di un anti-ottimizzazione per il caso più comune (a causa di ramificazioni e forse di mancanze della cache).

Ora quando (almeno) uno degli operandi è un object direttamente temporaneo, non puoi mai avere uno scenario di autoassegnazione. Alcune persone sostengono di assumere quel caso e ottimizzano il codice al punto tale che il codice diventa stupido suicida quando l’assunto è sbagliato. Dico che scaricare lo stesso object di controllo sugli utenti è irresponsabile. Non facciamo questo argomento per l’assegnazione delle copie; perché invertire la posizione per l’assegnazione del movimento?

Facciamo un esempio, modificato da un altro rispondente:

 dumb_array& dumb_array::operator=(const dumb_array& other) { if (mSize != other.mSize) { delete [] mArray; mArray = nullptr; // clear this... mSize = 0u; // ...and this in case the next line throws mArray = other.mSize ? new int[other.mSize] : nullptr; mSize = other.mSize; } std::copy(other.mArray, other.mArray + mSize, mArray); return *this; } 

Questo incarico di copia gestisce l’autoassegnazione con garbo senza un controllo esplicito. Se le dimensioni di origine e di destinazione sono diverse, la deallocazione e la riallocazione precedono la copia. Altrimenti, solo la copia è fatta. L’autoassegnazione non ottiene un percorso ottimizzato, viene scaricata nello stesso percorso di quando le dimensioni di origine e destinazione iniziano uguali. La copia è tecnicamente inutile quando i due oggetti sono equivalenti (anche quando sono lo stesso object), ma questo è il prezzo quando non si fa un controllo di uguaglianza (saggio o saggio sull’indirizzo) dal momento che detto controllo sarebbe di per sé un rifiuto del tempo. Nota che l’autoassegnazione degli oggetti qui causerà una serie di auto-assegnamenti a livello di elemento; il tipo di elemento deve essere sicuro per fare questo.

Come il suo esempio di origine, questa assegnazione di copia fornisce la garanzia di sicurezza di eccezione di base. Se si desidera la garanzia forte, utilizzare l’operatore di assegnazione unificata dalla query Copia e Scambio originale, che gestisce sia l’assegnazione di copia che quella di spostamento. Ma il punto di questo esempio è ridurre la sicurezza di un grado per guadagnare velocità. (A proposito, stiamo assumendo che i valori dei singoli elementi siano indipendenti, che non ci sia alcun vincolo invariante che limiti alcuni valori rispetto agli altri.)

Esaminiamo un’assegnazione del movimento per lo stesso tipo:

 class dumb_array { //... void swap(dumb_array& other) noexcept { // Just in case we add UDT members later using std::swap; // both members are built-in types -> never throw swap( this->mArray, other.mArray ); swap( this->mSize, other.mSize ); } dumb_array& operator=(dumb_array&& other) noexcept { this->swap( other ); return *this; } //... }; void swap( dumb_array &l, dumb_array &r ) noexcept { l.swap( r ); } 

Un tipo sostituibile che necessita di personalizzazione dovrebbe avere una funzione libera a due argomenti denominata swap nello stesso spazio dei nomi del tipo. (La restrizione dello spazio dei nomi consente alle chiamate non qualificate di scambiare per funzionare.) Un tipo di contenitore deve anche aggiungere una funzione membro di swap pubblico per far corrispondere i contenitori standard. Se uno swap membri non viene fornito, probabilmente lo swap funzioni libere deve essere contrassegnato come amico del tipo scambiabile. Se personalizzi le mosse per utilizzare lo swap , devi fornire il tuo codice di scambio; il codice standard chiama il codice di movimento del tipo, che si tradurrà in una ricorsione reciproca infinita per i tipi di spostamento personalizzati.

Come i distruttori, le funzioni di scambio e le operazioni di spostamento dovrebbero essere mai lanciate se ansible, e probabilmente contrassegnate come tali (in C ++ 11). I tipi di libreria standard e le routine hanno ottimizzazioni per i tipi di movimento non gettare.

Questa prima versione dell’assegnazione del movimento soddisfa il contratto di base. I marcatori delle risorse della fonte vengono trasferiti all’object di destinazione. Le vecchie risorse non verranno divulgate poiché l’object di origine ora le gestisce. E l’object di origine viene lasciato in uno stato utilizzabile in cui è ansible applicare ulteriori operazioni, tra cui assegnazione e distruzione.

Nota che questo spostamento è automaticamente sicuro per l’autoassegnazione, poiché la chiamata di swap è. Inoltre è fortemente sicuro. Il problema è la non necessaria conservazione delle risorse. Le vecchie risorse per la destinazione non sono più concettualmente necessarie, ma qui sono ancora in giro solo così l’object sorgente può rimanere valido. Se la distruzione programmata dell’object sorgente è molto lontana, stiamo sprecando spazio per le risorse, o peggio se lo spazio totale delle risorse è limitato e altre petizioni di risorse si verificheranno prima che il (nuovo) object sorgente decada ufficialmente.

Questo problema è ciò che ha causato il controverso consiglio del guru attuale riguardante l’auto-targeting durante l’assegnazione del movimento. Il modo di scrivere l’assegnazione del movimento senza indugiare sulle risorse è qualcosa come:

 class dumb_array { //... dumb_array& operator=(dumb_array&& other) noexcept { delete [] this->mArray; // kill old resources this->mArray = other.mArray; this->mSize = other.mSize; other.mArray = nullptr; // reset source other.mSize = 0u; return *this; } //... }; 

L’origine viene ripristinata alle condizioni predefinite, mentre le vecchie risorse di destinazione vengono distrutte. Nel caso di autoassegnazione, il tuo object attuale finisce con il suicidio. Il modo principale per aggirarlo è circondare il codice di azione con un blocco if(this != &other) , o avvitarlo e lasciare che i clienti mangiano una linea di base assert(this != &other) (se ti senti bene).

Un’alternativa è studiare come rendere l’assegnazione delle copie fortemente sicura, senza assegnazione unificata, e applicarla all’assegnazione dello spostamento:

 class dumb_array { //... dumb_array& operator=(dumb_array&& other) noexcept { dumb_array temp{ std::move(other) }; this->swap( temp ); return *this; } //... }; 

Quando gli other e this sono distinti, l’ other viene svuotato dal passaggio alla temp e rimane così. Poi this perde le sue vecchie risorse per temp mentre le risorse originariamente possedute da other . Quindi le vecchie risorse di this vengono uccise quando fa temp .

Quando avviene l’autoassegnazione, lo svuotamento other per la temp svuota anche this . Quindi l’object target recupera le sue risorse quando temp e this scambio. La morte di temp reclama un object vuoto, che dovrebbe essere praticamente un no-op. this / other object conserva le sue risorse.

L’incarico di movimento non dovrebbe mai essere lanciato fintanto che lo sono anche la costruzione del movimento e lo scambio. Il costo di essere sicuri anche durante l’autoassegnazione è un po ‘più di istruzioni su tipi di basso livello, che dovrebbero essere sommersi dalla chiamata di deallocazione.

Sono nel campo di coloro che vogliono operatori sicuri di autoassegnazione, ma non vogliono scrivere assegni di auto-assegnazione nelle implementazioni di operator= . E infatti non voglio nemmeno implementare operator= affatto, voglio che il comportamento predefinito funzioni subito “fuori dagli schemi”. I migliori membri speciali sono quelli che vengono gratuitamente.

Detto questo, i requisiti MoveAssignable presenti nello Standard sono descritti come segue (da 17.6.3.1 Requisiti argomento template [utility.arg.requirements], n3290):

 Espressione Tipo di reso Valore restituito Post-condizione
 t = rv T & tt è equivalente al valore di rv prima dell'assegnazione

dove i segnaposto sono descritti come: ” t [è un] lvalue modificabile di tipo T;” e ” rv è un valore di tipo T;”. Si noti che questi sono requisiti inseriti nei tipi utilizzati come argomenti per i modelli della libreria Standard, ma guardando altrove nello Standard noto che ogni requisito sull’assegnazione del movimento è simile a questo.

Ciò significa che a = std::move(a) deve essere ‘sicuro’. Se quello di cui hai bisogno è un test di id quadro (es. this != &other Altro), allora vai a prenderlo, altrimenti non sarai nemmeno in grado di mettere i tuoi oggetti in std::vector ! (A meno che tu non usi quei membri / operazioni che richiedono MoveAssignable, ma non importa che.) Si noti che con l’esempio precedente a = std::move(a) , quindi this == &other sarà effettivamente valido.

Siccome la tua attuale operator= funzione è scritta, dal momento che hai fatto l’argomento di riferimento rval const , non c’è modo di “rubare” i puntatori e cambiare i valori del riferimento del valore in entrata … non puoi cambiare questo, puoi solo leggerlo. Vedrei solo un problema se dovessi iniziare a chiamare delete su puntatori, ecc. In this object come faresti in un normale metodo lvaue-reference operator= , ma questo tipo di sconfigge il punto della versione -valore … cioè, sembrerebbe ridondante usare la versione rvalue per fare fondamentalmente le stesse operazioni normalmente lasciate a un operator= const -val operator= metodo.

Ora, se hai definito il tuo operator= per prendere un riferimento al valore non costante, allora l’unico modo in cui potevo vedere un assegno era se avessi passato this object a una funzione che restituiva intenzionalmente un riferimento rvalue piuttosto che un temporaneo.

Ad esempio, supponiamo che qualcuno abbia provato a scrivere una funzione operator+ e utilizzi una combinazione di riferimenti rvalue e riferimenti lvalue per “impedire” la creazione di temporanee extra durante un’operazione di aggiunta in pila sul tipo di object:

 struct A; //defines operator=(A&& rhs) where it will "steal" the pointers //of rhs and set the original pointers of rhs to NULL A&& operator+(A& rhs, A&& lhs) { //...code return std::move(rhs); } A&& operator+(A&& rhs, A&&lhs) { //...code return std::move(rhs); } int main() { A a; a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a //...rest of code } 

Ora, da quello che capisco sui riferimenti di valore, fare ciò che precede è scoraggiato (cioè, dovresti solo restituire un riferimento temporaneo, non di valore), ma, se qualcuno dovesse farlo ancora, allora dovresti controllare per fare certo che il riferimento di valore in ingresso non stava facendo riferimento allo stesso object di this puntatore.

La mia risposta è ancora che l’assegnazione delle mosse non deve essere salvata contro l’autoapprendimento, ma ha una spiegazione diversa. Considera std :: unique_ptr. Se dovessi implementarne uno, farei qualcosa del genere:

 unique_ptr& operator=(unique_ptr&& x) { delete ptr_; ptr_ = x.ptr_; x.ptr_ = nullptr; return *this; } 

Se guardi Scott Meyers che lo spiega , fa qualcosa di simile. (Se ti muovi perché non fare lo scambio, ha una scrittura in più). E questo non è sicuro per l’auto-assegnazione.

A volte questo è sfortunato. Considera di spostare dal vettore tutti i numeri pari:

 src.erase( std::partition_copy(src.begin(), src.end(), src.begin(), std::back_inserter(even), [](int num) { return num % 2; } ).first, src.end()); 

Questo va bene per i numeri interi, ma non credo che tu possa fare qualcosa del genere con la semantica del movimento.

Per concludere: spostare l’assegnazione all’object stesso non è ok e devi fare attenzione.

Piccolo aggiornamento

  1. Non sono d’accordo con Howard, che è una ctriggers idea, ma ancora – penso che l’auto-spostamento dell’assegnazione di oggetti “spostati” dovrebbe funzionare, perché lo swap(x, x) dovrebbe funzionare. Algoritmi amano queste cose! È sempre bello quando un caso d’angolo funziona. (E devo ancora vedere un caso in cui non è gratis, non significa che non esista però).
  2. In questo modo l’assegnazione di unique_ptrs è implementata in libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} È sicuro per l’assegnazione del movimento personale.
  3. Le linee guida fondamentali pensano che dovrebbe essere OK per l’assegnazione di auto-spostamento.

C’è una situazione a cui (questo == rhs) posso pensare. Per questa affermazione: Myclass obj; std :: move (obj) = std :: move (obj)