Copia costruttore e = sovraccarico dell'operatore in C ++: è possibile una funzione comune?


87

Poiché un costruttore di copie

MyClass(const MyClass&);

e un sovraccarico dell'operatore =

MyClass& operator = (const MyClass&);

hanno più o meno lo stesso codice, lo stesso parametro e differiscono solo al ritorno, è possibile avere una funzione comune da utilizzare per entrambi?


6
"... hanno più o meno lo stesso codice ..."? Hmm ... Stai facendo qualcosa di sbagliato. Cerca di ridurre al minimo la necessità di utilizzare funzioni definite dall'utente per questo e lascia che il compilatore faccia tutto il lavoro sporco. Questo spesso significa incapsulare le risorse nel proprio oggetto membro. Potresti mostrarci un codice. Forse abbiamo dei buoni suggerimenti per il design.
sellibitze

2
Possibile duplicato di Ridurre la
mpromonet

Risposte:


121

Sì. Ci sono due opzioni comuni. Uno - che è generalmente sconsigliato - è chiamare operator=esplicitamente il dal costruttore di copie:

MyClass(const MyClass& other)
{
    operator=(other);
}

Tuttavia, fornire un bene operator=è una sfida quando si tratta di affrontare il vecchio stato e le questioni derivanti dall'assegnazione di sé. Inoltre, tutti i membri e le basi vengono inizializzati per impostazione predefinita per primi anche se devono essere assegnati a da other. Questo potrebbe non essere nemmeno valido per tutti i membri e le basi e anche dove è valido è semanticamente ridondante e può essere praticamente costoso.

Una soluzione sempre più diffusa consiste nell'implementazione operator=utilizzando il costruttore di copie e un metodo di scambio.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

o anche:

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

Una swapfunzione è tipicamente semplice da scrivere in quanto si limita a scambiare la proprietà degli interni e non deve ripulire lo stato esistente o allocare nuove risorse.

I vantaggi dell'idioma di copia e scambio sono che è automaticamente sicuro per l'autoassegnazione e, a condizione che l'operazione di scambio non avvenga, è anche fortemente sicuro rispetto alle eccezioni.

Per essere assolutamente sicuro rispetto alle eccezioni, un operatore di assegnazione scritta "manuale" deve in genere allocare una copia delle nuove risorse prima di disallocare le vecchie risorse dell'assegnatario in modo che se si verifica un'eccezione nell'allocazione delle nuove risorse, il vecchio stato può ancora essere restituito a . Tutto questo viene fornito gratuitamente con il copy-and-swap, ma in genere è più complesso, e quindi soggetto a errori, da fare da zero.

L'unica cosa a cui fare attenzione è assicurarsi che il metodo di scambio sia un vero scambio, e non quello predefinito std::swapche utilizza il costruttore di copia e l'operatore di assegnazione stesso.

In genere swapviene utilizzato un memberwise . std::swapfunziona ed è garantito "no-throw" con tutti i tipi di base e i tipi di puntatore. La maggior parte dei puntatori intelligenti può anche essere scambiata con una garanzia di non lancio.


3
In realtà, non sono operazioni comuni. Mentre il copy ctor inizializza per la prima volta i membri dell'oggetto, l'operatore di assegnazione sovrascrive i valori esistenti. Considerando questo, l'allineamento operator=dal copyctor è in effetti piuttosto brutto, perché prima inizializza tutti i valori su un valore predefinito solo per sovrascriverli con i valori dell'altro oggetto subito dopo.
sbi

14
Forse a "Non consiglio", aggiungi "e nemmeno un esperto di C ++". Qualcuno potrebbe arrivare e non rendersi conto che non stai solo esprimendo una preferenza personale di minoranza, ma l'opinione consolidata di coloro che ci hanno effettivamente pensato. E, OK, forse mi sbaglio e qualche esperto di C ++ lo consiglia, ma personalmente vorrei ancora lanciare il guanto di sfida a qualcuno per trovare un riferimento per quella raccomandazione.
Steve Jessop

4
Giusto, ti ho già votato comunque :-). Immagino che se qualcosa è ampiamente considerato la migliore pratica, allora è meglio dirlo (e guardarlo di nuovo se qualcuno dice che non è davvero la migliore, dopotutto). Allo stesso modo se qualcuno chiedesse "è possibile usare i mutex in C ++", non direi "un'opzione abbastanza comune è ignorare completamente RAII e scrivere codice non protetto dalle eccezioni che si blocca in produzione, ma è sempre più popolare scrivere codice decente e funzionante ";-)
Steve Jessop

4
+1. E penso che ci sia sempre bisogno di analisi. Penso che sia ragionevole avere una assignfunzione membro utilizzata sia dal copy ctor che dall'operatore di assegnazione in alcuni casi (per classi leggere). In altri casi (uso intensivo di risorse / casi, handle / body) una copia / scambio è la strada da percorrere ovviamente.
Johannes Schaub - litb

2
@litb: sono rimasto sorpreso da questo, quindi ho cercato l'articolo 41 in Exception C ++ (in cui questo gotw si è trasformato) e questa particolare raccomandazione è andata e lui raccomanda il copy-and-swap al suo posto. Piuttosto subdolamente ha lasciato cadere "Problema # 4: è inefficiente per l'assegnazione" allo stesso tempo.
CB Bailey

13

Il costruttore di copie esegue la prima inizializzazione degli oggetti che erano la memoria non elaborata. L'operatore di assegnazione, OTOH, sovrascrive i valori esistenti con quelli nuovi. Più spesso che mai, ciò comporta l'eliminazione delle vecchie risorse (ad esempio la memoria) e l'allocazione di nuove.

Se c'è una somiglianza tra i due, è che l'operatore di assegnazione esegue la distruzione e la costruzione di copie. Alcuni sviluppatori erano soliti implementare effettivamente l'assegnazione mediante distruzione sul posto seguita dalla costruzione di copie di posizionamento. Tuttavia, questa è una molto cattiva idea. (E se questo è l'operatore di assegnazione di una classe base che ha chiamato durante l'assegnazione di una classe derivata?)

Quello che di solito è considerato l'idioma canonico al giorno d'oggi sta usando swapcome suggerito da Charles:

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

Questo utilizza la costruzione della copia (nota che otherviene copiato) e la distruzione (viene distrutto alla fine della funzione) - e li usa anche nel giusto ordine: costruzione (potrebbe fallire) prima della distruzione (non deve fallire).


Dovrebbe swapessere dichiarato virtual?

1
@Johannes: le funzioni virtuali vengono utilizzate nelle gerarchie di classi polimorfiche. Gli operatori di assegnazione vengono utilizzati per i tipi di valore. I due difficilmente si mescolano.
sbi

-3

Qualcosa mi dà fastidio:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Primo, leggere la parola "scambia" quando la mia mente pensa "copia" irrita il mio buon senso. Inoltre, metto in dubbio l'obiettivo di questo trucco stravagante. Sì, eventuali eccezioni nella costruzione delle nuove risorse (copiate) dovrebbero verificarsi prima dello scambio, il che sembra un modo sicuro per assicurarsi che tutti i nuovi dati siano riempiti prima di renderli disponibili.

Va bene. Allora, che dire delle eccezioni che si verificano dopo lo scambio? (quando le vecchie risorse vengono distrutte quando l'oggetto temporaneo esce dall'ambito) Dal punto di vista dell'utente dell'assegnazione, l'operazione è fallita, tranne che non l'ha fatto. Ha un enorme effetto collaterale: la copia è realmente avvenuta. Solo una pulizia delle risorse non è riuscita. Lo stato dell'oggetto di destinazione è stato alterato anche se l'operazione sembra dall'esterno non riuscita.

Quindi, propongo invece di "scambiare" per fare un "trasferimento" più naturale:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

C'è ancora la costruzione dell'oggetto temporaneo, ma la prossima azione immediata è liberare tutte le risorse correnti della destinazione prima di spostare (e annullare in modo che non vengano liberate due volte) le risorse della sorgente su di esso.

Invece di {costruisci, sposta, distruggi}, propongo {costruisci, distruggi, sposta}. La mossa, che è l'azione più pericolosa, è quella presa per ultima dopo che tutto il resto è stato risolto.

Sì, il fallimento della distruzione è un problema in entrambi gli schemi. I dati sono danneggiati (copiati quando non pensavi che fossero) o persi (liberati quando non pensavi che lo fossero). Perso è meglio che danneggiato. Nessun dato è meglio di dati errati.

Trasferisci invece di scambiare. Questo è comunque il mio suggerimento.


2
Un distruttore non deve fallire, quindi non sono previste eccezioni alla distruzione. E non capisco quale sarebbe il vantaggio di spostare la mossa dietro la distruzione, se la mossa è l'operazione più pericolosa? Cioè, nello schema standard, un errore di spostamento non corromperà il vecchio stato, mentre il tuo nuovo schema lo fa. Allora perché? Inoltre, First, reading the word "swap" when my mind is thinking "copy" irritates-> Come scrittore di librerie, di solito conosci le pratiche comuni (copia + scambio), e il punto cruciale è my mind. La tua mente è effettivamente nascosta dietro l'interfaccia pubblica. Questo è il significato del codice riutilizzabile.
Sebastian Mach
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.