Quando dobbiamo usare i costruttori di copia?


87

So che il compilatore C ++ crea un costruttore di copia per una classe. In quale caso dobbiamo scrivere un costruttore di copie definito dall'utente? Puoi fare qualche esempio?



1
Uno dei casi per scrivere il tuo copy-ctor: quando devi fare un deep copy. Si noti inoltre che non appena si crea un ctor, non viene creato alcun ctor predefinito (a meno che non si utilizzi la parola chiave predefinita).
harshvchawla

Risposte:


75

Il costruttore di copia generato dal compilatore esegue la copia in termini di membri. A volte questo non è sufficiente. Per esempio:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

in questo caso la copia del storedmembro per membro non duplicherà il buffer (verrà copiato solo il puntatore), quindi il primo ad essere distrutto con la condivisione della copia del buffer chiamerà delete[]correttamente e il secondo eseguirà un comportamento indefinito. È necessario un costruttore di copia per la copia profonda (e anche un operatore di assegnazione).

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}

10
Non esegue una copia in termini di bit, ma di membri che in particolare richiama il copy-ctor per i membri di tipo classe.
Georg Fritzsche

7
Non scrivere l'operatore di valutazione in questo modo. Non è sicuro dall'eccezione. (se il nuovo genera un'eccezione l'oggetto viene lasciato in uno stato indefinito con lo store che punta a una parte di memoria deallocata (dealloca la memoria SOLO dopo che tutte le operazioni che possono lanciare sono state completate con successo)). Una soluzione semplice è usare il copy swap idium.
Martin York,

@sharptooth 3a linea dal basso che hai delete stored[];e credo che dovrebbe esseredelete [] stored;
Peter Ajtai

4
So che è solo un esempio, ma dovresti sottolineare che la soluzione migliore è usare std::string. L'idea generale è che solo le classi di utilità che gestiscono le risorse devono sovraccaricare i Tre Grandi e che tutte le altre classi dovrebbero usare solo quelle classi di utilità, eliminando la necessità di definire uno qualsiasi dei Tre Grandi.
GManNickG

2
@ Martin: volevo assicurarmi che fosse scolpito nella pietra. : P
GManNickG

46

Sono un po 'irritato che la regola del Rule of Five non sia stata citata.

Questa regola è molto semplice:

La regola del cinque :
ogni volta che stai scrivendo uno di Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor o Move Assignment Operator, probabilmente dovrai scrivere gli altri quattro.

Ma c'è una linea guida più generale che dovresti seguire, che deriva dalla necessità di scrivere codice protetto dalle eccezioni:

Ogni risorsa dovrebbe essere gestita da un oggetto dedicato

Qui @sharptooth's codice è ancora (per lo più) bene, però se dovesse aggiungere un secondo attributo alla sua classe che non sarebbe stato. Considera la seguente classe:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

Cosa succede se new Barlancia? Come si cancella l'oggetto puntato da mFoo? Ci sono soluzioni (prova / cattura a livello di funzione ...), semplicemente non scalano.

Il modo corretto per affrontare la situazione è usare classi appropriate invece di puntatori non elaborati.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

Con la stessa implementazione del costruttore (o effettivamente, usando make_unique), ora ho la sicurezza delle eccezioni gratuitamente !!! Non è eccitante? E soprattutto, non devo più preoccuparmi di un distruttore adeguato! Ho bisogno di scrivere il mio Copy Constructore Assignment Operator, però, perchéunique_ptr non definisce queste operazioni ... ma non importa qui;)

E quindi, sharptoothla classe di s rivisitata:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

Non so voi, ma trovo il mio più facile;)


Per C ++ 11 - la regola del cinque che aggiunge alla regola del tre Move Constructer e Move Assignment Operator.
Robert Andrzejuk

1
@ Robb: Nota che in realtà, come dimostrato nell'ultimo esempio, dovresti generalmente mirare alla regola dello zero . Solo le classi tecniche specializzate (generiche) dovrebbero preoccuparsi di gestire una risorsa, tutte le altre classi dovrebbero usare quei puntatori / contenitori intelligenti e non preoccuparsene.
Matthieu M.

@MatthieuM. D'accordo :-) Ho menzionato la regola del cinque, poiché questa risposta è precedente a C ++ 11 e inizia con "Big Three", ma va detto che ora i "Big Five" sono rilevanti. Non voglio votare a favore di questa risposta poiché è corretta nel contesto richiesto.
Robert Andrzejuk

@ Robb: Buon punto, ho aggiornato la risposta per menzionare la regola del cinque invece del tre grande. Si spera che la maggior parte delle persone sia passata ai compilatori compatibili con C ++ 11 (e ho pietà di quelli che ancora non l'hanno fatto).
Matthieu M.

31

Posso ricordare dalla mia pratica e pensare ai seguenti casi in cui si ha a che fare con la dichiarazione / definizione esplicita del costruttore di copia. Ho raggruppato i casi in due categorie

  • Correttezza / semantica : se non si fornisce un costruttore di copie definito dall'utente, i programmi che utilizzano quel tipo potrebbero non riuscire a compilarsi o funzionare in modo errato.
  • Ottimizzazione : fornire una buona alternativa al costruttore di copie generato dal compilatore consente di rendere il programma più veloce.


Correttezza / semantica

Inserisco in questa sezione i casi in cui la dichiarazione / definizione del costruttore di copia è necessaria per il corretto funzionamento dei programmi che utilizzano quella tipologia.

Dopo aver letto questa sezione, imparerai a conoscere diverse insidie ​​nel consentire al compilatore di generare il costruttore di copia da solo. Pertanto, come seand ha notato nella sua risposta , è sempre sicuro disattivare la copiabilità per una nuova classe e deliberatamente abilitarlo in seguito quando realmente necessario.

Come rendere una classe non copiabile in C ++ 03

Dichiarare un costruttore di copie privato e non fornire un'implementazione per esso (in modo che la compilazione fallisca nella fase di collegamento anche se gli oggetti di quel tipo vengono copiati nello scope della classe o dai suoi amici).

Come rendere una classe non copiabile in C ++ 11 o più recente

Dichiara il costruttore di copie con =deletealla fine.


Copia superficiale e profonda

Questo è il caso meglio compreso e in realtà l'unico menzionato nelle altre risposte. shaprtooth l' ha coperto abbastanza bene. Voglio solo aggiungere che la copia profonda delle risorse che dovrebbero essere di proprietà esclusiva dell'oggetto può essere applicata a qualsiasi tipo di risorsa, di cui la memoria allocata dinamicamente è solo un tipo. Se necessario, potrebbe anche essere necessario copiare in profondità un oggetto

  • copia dei file temporanei sul disco
  • apertura di una connessione di rete separata
  • creando un thread di lavoro separato
  • allocare un framebuffer OpenGL separato
  • eccetera

Oggetti autoregistranti

Considera una classe in cui tutti gli oggetti, indipendentemente da come sono stati costruiti, DEVONO essere registrati in qualche modo. Qualche esempio:

  • L'esempio più semplice: mantenere il conteggio totale degli oggetti attualmente esistenti. La registrazione degli oggetti consiste solo nell'incrementare il contatore statico.

  • Un esempio più complesso è avere un registro singleton, in cui vengono memorizzati i riferimenti a tutti gli oggetti esistenti di quel tipo (in modo che le notifiche possano essere recapitate a tutti loro).

  • Gli smart-pointer con conteggio dei riferimenti possono essere considerati solo un caso speciale in questa categoria: il nuovo puntatore si "registra" con la risorsa condivisa piuttosto che in un registro globale.

Tale operazione di autoregistrazione deve essere eseguita da QUALSIASI costruttore del tipo e il costruttore di copia non fa eccezione.


Oggetti con riferimenti incrociati interni

Alcuni oggetti possono avere una struttura interna non banale con riferimenti incrociati diretti tra i loro diversi sotto-oggetti (infatti, un solo riferimento incrociato interno è sufficiente per attivare questo caso). Il costruttore di copie fornito dal compilatore interromperà le associazioni interne tra oggetti , convertendole in associazioni tra oggetti .

Un esempio:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Solo gli oggetti che soddisfano determinati criteri possono essere copiati

Potrebbero esserci classi in cui gli oggetti possono essere copiati in sicurezza mentre si trovano in uno stato (ad esempio, stato costruito per impostazione predefinita) e non possono essere copiati in modo sicuro altrimenti. Se vogliamo consentire la copia di oggetti sicuri da copiare, allora, se programmiamo in modo difensivo, abbiamo bisogno di un controllo in fase di esecuzione nel costruttore di copie definito dall'utente.


Oggetti secondari non copiabili

A volte, una classe che dovrebbe essere copiabile aggrega sotto-oggetti non copiabili. Di solito, questo accade per oggetti con uno stato non osservabile (questo caso è discusso più dettagliatamente nella sezione "Ottimizzazione" di seguito). Il compilatore aiuta semplicemente a riconoscere quel caso.


Sottooggetti quasi copiabili

Una classe, che dovrebbe essere copiabile, può aggregare un sottooggetto di tipo quasi copiabile. Un tipo quasi copiabile non fornisce un costruttore di copia in senso stretto, ma ha un altro costruttore che permette di creare una copia concettuale dell'oggetto. La ragione per rendere un tipo quasi copiabile è quando non c'è un pieno accordo sulla semantica della copia del tipo.

Ad esempio, rivisitando il caso di autoregistrazione dell'oggetto, possiamo sostenere che potrebbero esserci situazioni in cui un oggetto deve essere registrato con il gestore oggetti globale solo se è un oggetto autonomo completo. Se è un oggetto secondario di un altro oggetto, la responsabilità di gestirlo è con il suo oggetto contenitore.

Oppure, è necessario supportare sia la copia superficiale che quella profonda (nessuna di esse è l'impostazione predefinita).

Quindi la decisione finale è lasciata agli utenti di quel tipo: quando copiano oggetti, devono specificare esplicitamente (attraverso argomenti aggiuntivi) il metodo di copia previsto.

In caso di un approccio non difensivo alla programmazione, è anche possibile che siano presenti sia un normale costruttore di copie che un costruttore quasi-copia. Ciò può essere giustificato quando nella stragrande maggioranza dei casi dovrebbe essere applicato un unico metodo di copia, mentre in situazioni rare ma ben comprese dovrebbero essere usati metodi di copia alternativi. Quindi il compilatore non si lamenterà di non essere in grado di definire implicitamente il costruttore di copia; sarà esclusiva responsabilità degli utenti ricordare e verificare se un sottooggetto di quel tipo debba essere copiato tramite un costruttore quasi-copia.


Non copiare lo stato fortemente associato all'identità dell'oggetto

In rari casi un sottoinsieme dello stato osservabile dell'oggetto può costituire (o essere considerato) una parte inseparabile dell'identità dell'oggetto e non dovrebbe essere trasferibile ad altri oggetti (sebbene ciò possa essere alquanto controverso).

Esempi:

  • L'UID dell'oggetto (ma anche questo appartiene al caso di "autoregistrazione" dall'alto, poiché l'id deve essere ottenuto in un atto di autoregistrazione).

  • Cronologia dell'oggetto (es. Lo stack Annulla / Ripristina) nel caso in cui il nuovo oggetto non deve ereditare la cronologia dell'oggetto sorgente, ma iniziare invece con un singolo elemento della cronologia " Copiato alle <TIME> da <OTHER_OBJECT_ID> ".

In questi casi, il costruttore della copia deve saltare la copia dei sotto-oggetti corrispondenti.


Applicazione della firma corretta del costruttore della copia

La firma del costruttore di copia fornito dal compilatore dipende da quali costruttori di copia sono disponibili per i sottooggetti. Se almeno un oggetto secondario non ha un vero costruttore di copia (prendendo l'oggetto sorgente per riferimento costante) ma ha invece un costruttore di copia mutante (prendendo l'oggetto sorgente con riferimento non costante) allora il compilatore non avrà scelta ma per dichiarare implicitamente e quindi definire un costruttore di copia mutante.

Ora, cosa succede se il costruttore di copia "mutante" del tipo di oggetto secondario non muta effettivamente l'oggetto sorgente (ed è stato semplicemente scritto da un programmatore che non conosce la constparola chiave)? Se non possiamo constcorreggere il codice aggiungendo il codice mancante , l'altra opzione è dichiarare il nostro costruttore di copie definito dall'utente con una firma corretta e commettere il peccato di passare a un file const_cast.


Copia su scrittura (COW)

Un contenitore COW che ha fornito riferimenti diretti ai suoi dati interni DEVE essere copiato in profondità al momento della costruzione, altrimenti potrebbe comportarsi come un handle di conteggio dei riferimenti.

Sebbene COW sia una tecnica di ottimizzazione, questa logica nel costruttore di copie è cruciale per la sua corretta implementazione. Ecco perché ho inserito questo caso qui invece che nella sezione "Ottimizzazione", dove andremo dopo.



Ottimizzazione

Nei seguenti casi potresti voler / aver bisogno di definire il tuo costruttore di copie per motivi di ottimizzazione:


Ottimizzazione della struttura durante la copia

Si consideri un contenitore che supporta le operazioni di rimozione degli elementi, ma può farlo semplicemente contrassegnando l'elemento rimosso come eliminato e riciclando il suo slot in un secondo momento. Quando viene creata una copia di un tale contenitore, può avere senso compattare i dati sopravvissuti piuttosto che conservare gli slot "cancellati" così come sono.


Salta la copia dello stato non osservabile

Un oggetto può contenere dati che non fanno parte del suo stato osservabile. Di solito, si tratta di dati memorizzati nella cache / memorizzati nella cache durante la durata dell'oggetto per velocizzare determinate operazioni di query lente eseguite dall'oggetto. È possibile saltare la copia di tali dati poiché verranno ricalcolati quando (e se!) Verranno eseguite le operazioni pertinenti. La copia di questi dati potrebbe essere ingiustificata, in quanto potrebbe essere rapidamente invalidata se lo stato osservabile dell'oggetto (da cui derivano i dati memorizzati nella cache) viene modificato da operazioni di mutazione (e se non abbiamo intenzione di modificare l'oggetto, perché stiamo creando un profondo copia quindi?)

Questa ottimizzazione è giustificata solo se i dati ausiliari sono grandi rispetto ai dati che rappresentano lo stato osservabile.


Disabilita la copia implicita

C ++ consente di disabilitare la copia implicita dichiarando il costruttore della copia explicit. Quindi gli oggetti di quella classe non possono essere passati alle funzioni e / o restituiti dalle funzioni per valore. Questo trucco può essere utilizzato per un tipo che sembra essere leggero ma in effetti è molto costoso da copiare (anche se renderlo quasi copiabile potrebbe essere una scelta migliore).

In C ++ 03 la dichiarazione di un costruttore di copie richiedeva di definirlo anche (ovviamente, se si intendeva usarlo). Quindi, andare per un tale costruttore di copie semplicemente per la preoccupazione in discussione significava che dovevi scrivere lo stesso codice che il compilatore avrebbe generato automaticamente per te.

Gli standard C ++ 11 e più recenti consentono di dichiarare funzioni membro speciali (i costruttori predefiniti e di copia, l'operatore di assegnazione della copia e il distruttore) con una richiesta esplicita di utilizzare l'implementazione predefinita (basta terminare la dichiarazione con =default).



TODO

Questa risposta può essere migliorata come segue:

  • Aggiungi altro codice di esempio
  • Illustrare il caso "Oggetti con riferimenti incrociati interni"
  • Aggiungi alcuni link

6

Se hai una classe che ha contenuto allocato dinamicamente. Ad esempio, memorizzi il titolo di un libro come carattere * e imposti il ​​titolo con nuovo, la copia non funzionerà.

Dovresti scrivere un costruttore di copie che lo faccia title = new char[length+1]e poi strcpy(title, titleIn). Il costruttore di copie farebbe solo una copia "superficiale".


2

Copy Constructor viene chiamato quando un oggetto viene passato per valore, restituito per valore o copiato in modo esplicito. Se non esiste un costruttore di copia, c ++ crea un costruttore di copia predefinito che crea una copia superficiale. Se l'oggetto non ha puntatori alla memoria allocata dinamicamente, lo farà la copia superficiale.


0

Spesso è una buona idea disabilitare copy ctor e operator = a meno che la classe non ne abbia espressamente bisogno. Ciò può prevenire inefficienze come il passaggio di un argomento per valore quando si intende fare riferimento. Anche i metodi generati dal compilatore potrebbero non essere validi.


-1

Consideriamo lo snippet di codice di seguito:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();fornisce un output spazzatura perché esiste un costruttore di copie definito dall'utente creato senza codice scritto per copiare i dati in modo esplicito. Quindi il compilatore non crea lo stesso.

Ho solo pensato di condividere questa conoscenza con tutti voi, anche se la maggior parte di voi lo sa già.

Saluti ... Buona programmazione !!!

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.