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


126

Nell'operatore di assegnazione di una classe, di solito è necessario verificare se l'oggetto assegnato è l'oggetto invocante in modo da non rovinare le cose:

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

    return *this;
}

Hai bisogno della stessa cosa per l'operatore di assegnazione dello spostamento? C'è mai una situazione in cui this == &rhssarebbe vero?

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

12
Irrilevante per la domanda di Q, e solo così che i nuovi utenti che leggono questa Q lungo la timeline (perché so che Seth lo sa già) non ottengano idee sbagliate, Copy and Swap è il modo corretto per implementare l'operatore di assegnazione di copia in cui tu non è necessario controllare l'autoassegnazione e tutto.
Alok Save

5
@VaughnCato: A a; a = std::move(a);.
Xeo

11
@VaughnCato L'uso std::moveè normale. Quindi prendi in considerazione l'aliasing e quando sei all'interno di uno stack di chiamate e hai un riferimento a T, e un altro riferimento a T... controllerai l'identità proprio qui? Vuoi trovare la prima chiamata (o le prime chiamate) in cui documentare che non puoi passare lo stesso argomento due volte dimostrerà staticamente che quei due riferimenti non saranno alias? O farai funzionare l'autoassegnazione?
Luc Danton,

2
@ LucDanton preferirei un'asserzione nell'operatore di assegnazione. Se std :: move fosse usato in modo tale che fosse possibile finire con un'autoassegnazione rvalue, lo considererei un bug che dovrebbe essere corretto.
Vaughn Cato

4
@VaughnCato Un posto in cui lo scambio automatico è normale è all'interno di std::sorto std::shuffle- ogni volta che si scambiano gli elementi iesimo e jesimo di un array senza i != jprima controllare . ( std::swapè implementato in termini di assegnazione delle mosse.)
Quuxplusone

Risposte:


143

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_arrayun 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_arraystato 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_arraypotrebbero 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_arrayi 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:

  1. Un temporaneo.
  2. 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, *thissarà 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_arrayesempio 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 ynon deve essere alterato. Quando &x == &yquindi 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 yha uno stato valido ma non specificato. Quando &x == &ypoi questa postcondizione si traduce in: xha 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 xin qualsiasi stato valido ma non specificato.

Vedo tre modi per programmare l'operatore di assegnazione dello spostamento per dumb_arrayottenere 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 *thise otherfiniscono 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_arraynon contiene risorse che dovrebbero essere distrutte "immediatamente". Ad esempio, se l'unica risorsa è la memoria, quanto sopra va bene. Se dumb_arrayfosse 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 != &otherassegno. 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.


6
Non so come pensare a questa risposta. Sembra che l'implementazione di tali classi (che gestiscono la loro memoria in modo molto esplicito) sia una cosa comune. E 'vero che quando si fa scrittura tale classe uno deve essere molto molto attenti a garanzie di sicurezza rispetto alle eccezioni e trovando il punto dolce per l'interfaccia di essere concisa ma comoda, ma la questione sembra essere chiedendo consigli generali.
Luc Danton

Sì, sicuramente non uso mai il copy-and-swap perché è una perdita di tempo per le classi che gestiscono risorse e cose (perché andare a fare un'altra copia intera di tutti i tuoi dati?). E grazie, questo risponde alla mia domanda.
Seth Carnegie,

5
Downvoted per il suggerimento che mossa-assegnazione-da-sé dovrebbe mai affermare-fallire o produrre un risultato "non specificato". L'assegnazione da sé è letteralmente il caso più facile da risolvere. Se la tua classe si blocca std::swap(x,x), perché dovrei fidarmi di essa per gestire correttamente operazioni più complicate?
Quuxplusone

1
@Quuxplusone: sono arrivato a concordare con te sull'assert-fail, come si nota nell'aggiornamento alla mia risposta. Per quanto riguarda std::swap(x,x), funziona anche quando x = std::move(x)produce un risultato non specificato. Provalo! Non devi credermi.
Howard Hinnant

@ HowardHinnant buon punto, swapfunziona fintanto che x = move(x)lascia xin qualsiasi stato in grado di muoversi. E gli algoritmi std::copy/ std::movesono definiti in modo da produrre un comportamento indefinito già su copie non operative (ahi; il ventenne memmovecapisce bene il caso banale ma std::movenon lo fa!). Quindi immagino di non aver ancora pensato a una "schiacciata" per l'autoassegnazione. Ma ovviamente l'autoassegnazione è qualcosa che accade spesso nel codice reale, che lo Standard lo abbia benedetto o meno.
Quuxplusone

11

Innanzitutto, hai sbagliato la firma dell'operatore di assegnazione delle mosse. Poiché gli spostamenti rubano risorse dall'oggetto sorgente, la sorgente deve essere un constriferimento senza valore r.

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

Si noti che si ritorna comunque tramite un riferimento (non const) l .

Per entrambi i tipi di assegnazione diretta, lo standard non consiste nel verificare l'autoassegnazione, ma nell'assicurarsi che un'autoassegnazione non provochi un crash-and-burn. In generale, nessuno fa x = xo y = std::move(y)chiama esplicitamente , ma l'aliasing, specialmente attraverso più funzioni, può portare a = bo c = std::move(d)diventare auto-assegnazioni. Un controllo esplicito per l'autoassegnazione, cioè this == &rhs, che salta la carne della funzione quando è vero è un modo per garantire la sicurezza dell'autoassegnazione. Ma è uno dei modi peggiori, dal momento che ottimizza un caso (si spera) raro mentre è un'anti-ottimizzazione per il caso più comune (a causa di ramificazioni e possibilmente mancate cache).

Ora, quando (almeno) uno degli operandi è un oggetto direttamente temporaneo, non puoi mai avere uno scenario di autoassegnazione. Alcune persone sostengono di assumere quel caso e ottimizzano il codice così tanto che il codice diventa suicidamente stupido quando il presupposto è sbagliato. Dico che scaricare il controllo dello stesso oggetto sugli utenti è irresponsabile. Non facciamo questo argomento per l'assegnazione di copie; perché invertire la posizione per l'assegnazione del movimento?

Facciamo un esempio, modificato da un altro intervistato:

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;
}

Questa assegnazione di copia gestisce l'autoassegnazione con garbo senza un controllo esplicito. Se le dimensioni dell'origine e della destinazione sono diverse, la deallocazione e la riallocazione precedono la copia. Altrimenti, viene eseguita solo la copia. L'autoassegnazione non ottiene un percorso ottimizzato, viene scaricato nello stesso percorso di quando le dimensioni di origine e di destinazione iniziano uguali. La copia è tecnicamente non necessaria quando i due oggetti sono equivalenti (anche quando sono lo stesso oggetto), ma questo è il prezzo quando non si esegue un controllo di uguaglianza (in termini di valore o di indirizzo) poiché detto controllo stesso sarebbe uno spreco del tempo. Notare che l'autoassegnazione dell'oggetto qui causerà una serie di autoassegnazione a livello di elemento; il tipo di elemento deve essere sicuro per farlo.

Come il suo esempio sorgente, questa assegnazione di copia fornisce la garanzia di sicurezza di base dell'eccezione. Se desideri una garanzia forte, utilizza l'operatore di assegnazione unificata della query originale di copia e scambio , che gestisce sia l'assegnazione di copia che 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 esiste alcun vincolo invariante che limiti alcuni valori rispetto ad altri.)

Diamo un'occhiata a un'assegnazione di mosse per questo 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 scambiabile che necessita di personalizzazione dovrebbe avere una funzione libera a due argomenti chiamata swapnello stesso spazio dei nomi del tipo. (La restrizione dello spazio dei nomi consente alle chiamate non qualificate allo scambio di funzionare.) Un tipo di contenitore dovrebbe anche aggiungere una swapfunzione membro pubblica per abbinare i contenitori standard. Se un membro swapnon viene fornito, swapprobabilmente la funzione free deve essere contrassegnata come un amico del tipo swap. Se personalizzi le mosse da usare swap, devi fornire il tuo codice di scambio; il codice standard chiama il codice di spostamento del tipo, che risulterebbe in una ricorsione reciproca infinita per i tipi personalizzati per lo spostamento.

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

Questa prima versione dell'assegnazione di trasloco soddisfa il contratto di base. Gli indicatori di risorsa dell'origine vengono trasferiti all'oggetto di destinazione. Le vecchie risorse non verranno perse poiché l'oggetto di origine ora le gestisce. E l'oggetto sorgente viene lasciato in uno stato utilizzabile in cui possono essere applicate ulteriori operazioni, tra cui assegnazione e distruzione.

Nota che questa mossa-assegnazione è automaticamente sicura per l'auto-assegnazione, poiché la swapchiamata è. È anche fortemente sicuro dalle eccezioni. Il problema è la conservazione delle risorse non necessaria. Le vecchie risorse per la destinazione non sono più necessarie concettualmente, ma qui sono ancora in giro solo così l'oggetto sorgente può rimanere valido. Se la distruzione programmata dell'oggetto sorgente è molto lontana, stiamo sprecando spazio per le risorse, o peggio se lo spazio totale per le risorse è limitato e altre petizioni sulle risorse avverranno prima che il (nuovo) oggetto sorgente muoia ufficialmente.

Questo problema è ciò che ha causato il controverso consiglio del guru attuale sull'auto-targeting durante l'assegnazione delle mosse. Il modo per scrivere mosse-assegnazioni senza risorse persistenti è qualcosa del tipo:

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;
    }
    //...
};

La sorgente viene ripristinata alle condizioni predefinite, mentre le vecchie risorse di destinazione vengono distrutte. Nel caso dell'autoassegnazione, il tuo oggetto attuale finisce per suicidarsi. Il modo principale per aggirare il problema è circondare il codice di azione con un if(this != &other)blocco, o avvitarlo e lasciare che i clienti mangino una assert(this != &other)riga iniziale (se ti senti bene).

Un'alternativa è studiare come rendere l'assegnazione di copia fortemente sicura rispetto alle eccezioni, senza assegnazione unificata, e applicarla all'assegnazione di spostamento:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Quando othere thissono distinti, otherviene svuotato dal passaggio a tempe rimane tale. Quindi thisperde le sue vecchie risorse tempmentre ottiene le risorse originariamente detenute da other. Quindi le vecchie risorse di thisvengono uccise quando lo tempfa.

Quando auto-assegnazione avviene, lo svuotamento di otherper tempvuoti thispure. Quindi l'oggetto target recupera le sue risorse quando tempe lo thisscambia. La morte di temprivendica un oggetto vuoto, che dovrebbe essere praticamente un no-op. L' oggetto this/ othermantiene le sue risorse.

L'assegnazione della mossa dovrebbe essere mai lanciata fintanto che lo sono anche la costruzione del movimento e lo scambio. Il costo di essere al sicuro anche durante l'autoassegnazione è un paio di istruzioni in più rispetto ai tipi di basso livello, che dovrebbero essere sommersi dalla chiamata di deallocazione.


È necessario verificare se è stata allocata memoria prima di chiamare deleteil secondo blocco di codice?
user3728501

3
Il secondo esempio di codice, l'operatore di assegnazione della copia senza verifica dell'autoassegnazione, è sbagliato. std::copyprovoca un comportamento indefinito se gli intervalli di origine e di destinazione si sovrappongono (incluso il caso in cui coincidono). Vedere C ++ 14 [alg.copy] / 3.
MM

6

Sono nel campo di coloro che desiderano operatori sicuri per l'autoassegnazione, ma non vogliono scrivere controlli di autoassegnazione nelle implementazioni di operator=. E in effetti non voglio nemmeno implementarlo operator=, voglio che il comportamento predefinito funzioni "subito". 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 dell'argomento modello [utility.arg.requirements], n3290):

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

dove i segnaposto sono descritti come: " t[è un] valore modificabile di tipo T;" e " rvè un valore di tipo T;". Si noti che questi sono requisiti posti sui tipi usati come argomenti per i modelli della libreria Standard, ma guardando altrove nello Standard ho notato che ogni requisito sull'assegnazione di mosse è simile a questo.

Ciò significa che a = std::move(a)deve essere "sicuro". Se ciò di cui hai bisogno è un test di identità (ad esempio this != &other), allora fallo, altrimenti non sarai nemmeno in grado di inserire i tuoi oggetti std::vector! (A meno che tu non usi quei membri / operazioni che richiedono MoveAssignable; ma non importa.) Notare che con l'esempio precedente a = std::move(a), allora this == &othersarà effettivamente valido.


Puoi spiegare come a = std::move(a)non lavorare farebbe sì che una classe non funzioni std::vector? Esempio?
Paul J. Lucas

@ PaulJ.Lucas La chiamata std::vector<T>::erasenon è consentita a meno che non Tsia MoveAssignable. (Come un IIRC a parte alcuni requisiti MoveAssignable sono stati allentati per MoveInsertable invece in C ++ 14.)
Luc Danton

OK, quindi Tdeve essere MoveAssignable, ma perché erase()mai dipendere dallo spostamento di un elemento su se stesso ?
Paul J. Lucas

@ PaulJ.Lucas Non esiste una risposta soddisfacente a questa domanda. Tutto si riduce a "non rompere i contratti".
Luc Danton

2

Poiché la tua operator=funzione corrente è scritta, dato che hai impostato l'argomento rvalue-reference const, non c'è modo di "rubare" i puntatori e cambiare i valori del riferimento rvalue in arrivo ... semplicemente non puoi cambiarlo, tu poteva solo leggere da esso. Vedrei un problema solo se dovessi iniziare a chiamare deletepuntatori, ecc. Nel tuo thisoggetto come faresti in un normale operator=metodo di riferimento lvaue , ma questo tipo di sconfigge il punto della versione rvalue ... cioè, lo sarebbe sembra ridondante usare la versione rvalue per fare sostanzialmente le stesse operazioni normalmente lasciate al metodo const-lvalue operator=.

Ora, se hai definito il tuo operator=in modo che prenda un constriferimento non rvalue, l'unico modo in cui ho potuto vedere un controllo richiesto era se thispassavi l' oggetto a una funzione che restituiva intenzionalmente un riferimento rvalue piuttosto che un temporaneo.

Ad esempio, supponiamo che qualcuno abbia provato a scrivere una operator+funzione e utilizzi un mix di riferimenti rvalue e riferimenti lvalue per "impedire" la creazione di temporanei extra durante alcune operazioni di addizione in pila sul tipo di oggetto:

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 ho capito sui riferimenti rvalue, fare quanto sopra è sconsigliato (cioè, dovresti semplicemente restituire un riferimento temporaneo, non rvalue), ma, se qualcuno dovesse ancora farlo, allora dovresti controllare per rendere certo che il riferimento rvalue in arrivo non faceva riferimento allo stesso oggetto del thispuntatore.


Nota che "a = std :: move (a)" è un modo banale per avere questa situazione. La tua risposta è comunque valida.
Vaughn Cato

1
Totalmente d'accordo che è il modo più semplice, anche se penso che la maggior parte delle persone non lo farà intenzionalmente :-) ... Tieni presente però che se il riferimento rvalue è const, allora puoi solo leggere da esso, quindi l'unico bisogno di fare un controllo sarebbe se decidessi nel tuooperator=(const T&&) di eseguire la stessa reinizializzazione di thisquella che faresti con un operator=(const T&)metodo tipico piuttosto che con un'operazione in stile scambio (ad esempio, rubare puntatori, ecc. piuttosto che fare copie profonde).
Jason

1

La mia risposta è ancora che l'assegnazione della mossa non deve essere salvo contro l'autoassegnazione, 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 spiega questo , fa qualcosa di simile. (Se vaghi perché non fare lo scambio - ha una scrittura in più). E questo non è sicuro per l'autoassegnazione.

A volte questo è un peccato. Considera l'idea 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 far funzionare qualcosa di simile con la semantica del movimento.

Per concludere: spostare l'assegnazione sull'oggetto stesso non va bene e devi stare attento.

Piccolo aggiornamento.

  1. Non sono d'accordo con Howard, che è una cattiva idea, ma comunque - penso che l'assegnazione di auto-spostamento di oggetti "spostati" dovrebbe funzionare, perché swap(x, x) dovrebbe funzionare. Gli algoritmi adorano queste cose! È sempre bello quando un caso d'angolo funziona e basta. (E devo ancora vedere un caso in cui non è gratuito. Non significa che non esista però).
  2. Questo è il modo in cui l'assegnazione di unique_ptrs è implementata in libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} è sicuro per l'assegnazione degli spostamenti automatici.
  3. Le linee guida di base pensano che dovrebbe essere OK spostarsi da soli.

0

C'è una situazione a cui (this == rhs) posso pensare. Per questa dichiarazione: Myclass obj; std :: move (obj) = std :: move (obj)


Myclass obj; std :: move (obj) = std :: move (obj);
little_monster
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.