2009-11-14 34 views
70

Poiché un costruttore di copiaCopia costruttore e = overload operatore in C++: è possibile una funzione comune?

MyClass(const MyClass&); 

e sovraccarico = operatore

MyClass& operator = (const MyClass&); 

avere praticamente lo stesso codice, lo stesso parametro, e differiscono solo sul ritorno, è possibile avere un comune funzione per entrambi da usare?

+3

"... hanno praticamente lo stesso codice ..."? Hmm ... Devi fare qualcosa di sbagliato. Cerca di minimizzare la necessità di utilizzare funzioni definite dall'utente per questo e lascia che il compilatore faccia tutto il lavoro sporco. Questo spesso significa incapsulare risorse nel proprio oggetto membro. Potresti mostrarci un codice. Forse abbiamo alcuni buoni consigli sul design. – sellibitze

+1

Possibile duplicato di [Riduzione della duplicazione del codice tra operator = e il costruttore di copie] (http://stackoverflow.com/questions/1477145/reducing-code-duplication-between-operator-and-the-copy-constructor) – mpromonet

risposta

96

Sì. Ci sono due opzioni comuni. Uno - che io non consiglio - è quello di chiamare il operator= dal costruttore di copia in modo esplicito:

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

Tuttavia, fornendo un buon operator= è una sfida quando si tratta di trattare con il vecchio Stato e le questioni derivanti da auto assegnazione. Inoltre, tutti i membri e le basi vengono inizializzati automaticamente per primi anche se devono essere assegnati 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ù popolare è implementare operator= utilizzando il costruttore di copia 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 funzione swap è in genere semplice da scrivere come solo scambia la proprietà delle parti interne e non deve ripulire stato esistente o allocare nuove risorse.

I vantaggi dell'idioma di copia e swap sono che è automaticamente autoassegnazione sicura e, a condizione che l'operazione di scambio sia azzerata, è anche eccezionalmente sicura.

Per essere eccezionalmente sicuro, un operatore di assegnazione scritto a mano deve in genere allocare una copia delle nuove risorse prima di deselezionare le vecchie risorse dell'assegnatario in modo che se si verifica un'eccezione che alloca le nuove risorse, il vecchio stato può ancora essere restituito a. Tutto questo è gratis con copy-and-swap, ma in genere è più complesso, e quindi soggetto a errori, da fare da zero.

L'unica cosa da stare attenti è quello di assicurarsi che il metodo di swap è un vero scambio, e non il default std::swap che utilizza il costruttore di copia e l'assegnazione operatore stesso.

In genere viene utilizzato un membro swap membro. std::swap funziona ed è 'no-throw' garantito con tutti i tipi di base e tipi di puntatore. La maggior parte dei puntatori intelligenti possono anche essere scambiati con una garanzia di non lancio.

+3

In realtà, non sono operazioni comuni. Mentre il codificatore copia per la prima volta inizializza i membri dell'oggetto, l'operatore di assegnazione sostituisce i valori esistenti. Considerando questo, alling 'operator =' dal copy ctor è in effetti piuttosto brutto, perché inizialmente inizializza tutti i valori ad alcuni di default solo per sovrascriverli con i valori degli altri oggetti subito dopo. – sbi

+0

Ho detto opzioni comuni, non operazioni. Sono completamente d'accordo sul fatto che chiamare 'operator =' da un costruttore di copie non è buono, ma basta vedere un ragionevole codice del mondo reale per vedere quanto è comune. –

+0

Downvoters, ti interessa spiegarlo? –

11

Il costruttore di copie esegue l'inizializzazione per la prima volta degli oggetti che erano una memoria non elaborata. L'operatore di assegnazione, OTOH, sostituisce i valori esistenti con quelli nuovi. Più spesso che mai, questo comporta il licenziamento di vecchie risorse (ad esempio, la memoria) e l'allocazione di nuove.

Se c'è una somiglianza tra i due, è che l'operatore di assegnazione esegue distruzione e copia-costruzione. Alcuni sviluppatori hanno effettivamente implementato l'assegnazione per distruzione sul posto seguita da copia-costruzione dei posizionamenti. Tuttavia, questa è una pessima idea di .(E se questo è l'operatore di assegnazione di una classe base che ha chiamato durante l'assegnazione di una classe derivata?)

Che di solito è considerato il linguaggio canonico al giorno d'oggi sta usando swap come Charles suggerito:

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

Questo utilizza copia -costruzione (nota che other viene copiata) e distruzione (è distrutta alla fine della funzione) - e li usa anche nell'ordine corretto: la costruzione (potrebbe fallire) prima della distruzione (non deve fallire).

+0

Dovremmo 'swap' essere dichiarato' virtuale'? – nonplus

+1

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

-2

Qualcosa mi dà fastidio su:

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

In primo luogo, la lettura della parola "swap" quando la mia mente sta pensando "copia" irrita il mio senso comune. Inoltre, metto in dubbio l'obiettivo di questo trucco elegante. Sì, qualsiasi eccezione nella costruzione delle nuove risorse (copiate) dovrebbe avvenire prima dello scambio, il che sembra un modo sicuro per assicurarsi che tutti i nuovi dati vengano riempiti prima di renderli disponibili.

Questo va bene. Quindi, 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 del compito, l'operazione è fallita, tranne che non lo ha fatto. Ha un enorme effetto collaterale: la copia è effettivamente avvenuta. Era solo un po 'di pulizia delle risorse che non funzionava. Lo stato dell'oggetto di destinazione è stato modificato anche se l'operazione sembra non riuscita.

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

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

C'è ancora la costruzione dell'oggetto temporanea, ma il prossimo intervento immediato è quello di liberare tutte le risorse attuali del la destinazione prima di spostarsi (e NULLing in modo che non vengano liberati in modo doppio) le risorse della sorgente.

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

Sì, l'errore di distruzione è un problema in entrambi gli schemi. I dati sono corrotti (copiati quando non pensavi che fosse) o persi (liberati quando non pensavi che fosse). Lost è meglio che danneggiato. Nessun dato è migliore dei dati cattivi.

Trasferimento invece di scambio. Questo è il mio suggerimento comunque.

+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? Ad esempio, nello schema standard, un errore di spostamento non corromperà il vecchio stato, mentre il nuovo schema lo fa. Allora perché? Inoltre, 'In primo luogo, leggendo la parola" scambio "quando la mia mente sta pensando" copia "irrita -> Come scrittore di una biblioteca, di solito conosci le pratiche comuni (copia + swap) e il punto cruciale è" la mia mente ". La tua mente è effettivamente nascosta dietro l'interfaccia pubblica. Questo è il codice riutilizzabile. –

Problemi correlati