Domanda Qual è la regola del tre?


  • Cosa fa copiare un oggetto significare?
  • Quali sono le copia costruttore e il copia l'operatore di assegnazione?
  • Quando devo dichiararli da solo?
  • Come posso impedire che i miei oggetti vengano copiati?

1841
2017-11-13 13:27


origine


risposte:


introduzione

C ++ tratta le variabili dei tipi definiti dall'utente con semantica del valore. Ciò significa che gli oggetti vengono copiati in modo implicito in vari contesti, e dovremmo capire cosa significa "copiare un oggetto" in realtà.

Consideriamo un semplice esempio:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(Se sei perplesso dal name(name), age(age) parte, questo è chiamato a elenco di inizializzazione dei membri.)

Funzioni membro speciali

Cosa significa copiare un person oggetto? Il main la funzione mostra due scenari di copia distinti. L'inizializzazione person b(a); è eseguito dal copia costruttore. Il suo compito è costruire un nuovo oggetto basato sullo stato di un oggetto esistente. L'incarico b = a è eseguito dal copia l'operatore di assegnazione. Il suo lavoro è generalmente un po 'più complicato, perché l'oggetto di destinazione è già in uno stato valido che deve essere gestito.

Dal momento che non abbiamo dichiarato né il costruttore di copie né l'operatore di assegnazione (né il distruttore), questi sono implicitamente definiti per noi. Citazione dallo standard:

Il costruttore [...] copia e l'operatore di assegnazione [...] copia e il distruttore sono funzioni membro speciali.   [ Nota: L'implementazione dichiarerà implicitamente queste funzioni membro   per alcuni tipi di classi quando il programma non li dichiara esplicitamente.   L'implementazione li definirà implicitamente se vengono utilizzati. [...] nota finale ]   [n3126.pdf sezione 12 §1]

Per impostazione predefinita, copiare un oggetto significa copiare i suoi membri:

Il costruttore di copie implicitamente definito per una classe non unione X esegue una copia membro dei suoi sottooggetti.   [n3126.pdf sezione 12.8 §16]

L'operatore di assegnazione di copia implicitamente definito per una classe non unione X esegue l'assegnazione di copia membro a tempo   dei suoi sottooggetti   [n3126.pdf sezione 12.8 §30]

Definizioni implicite

Il membro speciale implicitamente definito funziona per person Assomiglia a questo:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

La copia membro è esattamente ciò che vogliamo in questo caso: name e age vengono copiati, quindi otteniamo un self-contained, indipendente person oggetto. Il distruttore implicitamente definito è sempre vuoto. Anche in questo caso va bene, poiché non abbiamo acquisito risorse nel costruttore. I distruttori dei membri sono implicitamente chiamati dopo person il distruttore è finito:

Dopo aver eseguito il corpo del distruttore e distrutto qualsiasi oggetto automatico allocato all'interno del corpo,   un distruttore per la classe X chiama i distruttori per i membri [...] diretti di X.   [n3126.pdf 12.4 §6]

Gestione delle risorse

Quindi, quando dovremmo dichiarare esplicitamente quelle funzioni dei membri speciali? Quando la nostra classe gestisce una risorsa, questo è, quando un oggetto della classe è responsabile per quella risorsa. Questo di solito significa che la risorsa è acquisita nel costruttore (o passati al costruttore) e rilasciato nel distruttore.

Torniamo indietro nel tempo al C ++ pre-standard. Non c'era niente di simile std::stringe i programmatori erano innamorati dei puntatori. Il person la classe potrebbe essere simile a questa:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

Ancora oggi, le persone scrivono ancora classi in questo stile e si mettono nei guai: "Ho spinto una persona in un vettore e ora ho degli errori di memoria pazzi!" Ricorda che per impostazione predefinita, copiare un oggetto significa copiare i suoi membri, ma copiare il name membro copia semplicemente un puntatore, non l'array di caratteri a cui punta! Questo ha diversi effetti spiacevoli:

  1. Cambiamenti via a può essere osservato tramite b.
  2. Una volta b è distrutto, a.name è un puntatore pendente.
  3. Se a viene distrutto, eliminando i rendimenti dei puntatori pendenti comportamento indefinito.
  4. Dal momento che l'incarico non tiene conto di cosa name indicato prima dell'assegnazione, prima o poi avrai perdite di memoria dappertutto.

Definizioni esplicite

Poiché la copia membrowise non ha l'effetto desiderato, dobbiamo definire esplicitamente il costruttore copia e l'operatore di assegnazione copia per creare copie profonde dell'array di caratteri:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

Nota la differenza tra inizializzazione e assegnazione: dobbiamo abbattere il vecchio stato prima di assegnarlo a name per evitare perdite di memoria. Inoltre, dobbiamo proteggere contro l'autoassegnazione del modulo x = x. Senza quel controllo, delete[] name eliminerebbe la matrice contenente il fonte stringa, perché quando scrivi x = x, entrambi this->name e that.name contengono lo stesso puntatore.

Eccezione sicurezza

Sfortunatamente, questa soluzione fallirà se new char[...] genera un'eccezione a causa dell'esaurimento della memoria. Una possibile soluzione è introdurre una variabile locale e riordinare le dichiarazioni:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

Questo si occupa anche dell'auto-assegnazione senza un controllo esplicito. Una soluzione ancora più robusta a questo problema è il idioma di copia e scambio, ma non entrerò nei dettagli della sicurezza delle eccezioni qui. Ho solo menzionato le eccezioni per fare il seguente punto: Scrivere classi che gestiscono risorse è difficile.

Risorse non copiabili

Alcune risorse non possono o non devono essere copiate, come handle di file o mutex. In tal caso, dichiara semplicemente il costruttore copia e l'operatore di assegnazione copia come private senza dare una definizione:

private:

    person(const person& that);
    person& operator=(const person& that);

In alternativa, puoi ereditare da boost::noncopyable o dichiararli come cancellati (C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

La regola del tre

A volte è necessario implementare una classe che gestisce una risorsa. (Non gestire mai più risorse in una singola classe, questo porterà solo a dolore). In tal caso, ricorda il regola del tre:

Se è necessario dichiarare esplicitamente il distruttore,   copia il costruttore o l'operatore di assegnazione delle copie da solo,   probabilmente dovrai dichiararli esplicitamente tutti e tre.

(Sfortunatamente, questa "regola" non è applicata dallo standard C ++ o da qualsiasi compilatore di cui sono a conoscenza.)

Consigli

Il più delle volte, non è necessario gestire una risorsa da soli, perché una classe esistente come std::string lo fa già per te Basta confrontare il semplice codice usando a std::string membro per l'alternativa contorta e soggetta a errori usando a char* e tu dovresti essere convinto. Fintanto che rimani lontano dai membri puntatori grezzi, è improbabile che la regola dei tre riguardi il tuo codice.


1512
2017-11-13 13:27



Il Regola del tre è una regola empirica per C ++, in pratica dicendo

Se la tua classe ha bisogno di qualcuno di

  • un copia costruttore,
  • un operatore di assegnazione,
  • o a distruttore,

definito esplicitamente, quindi è probabile che abbia bisogno tutti e tre.

Le ragioni di ciò sono che tutti e tre sono solitamente utilizzati per gestire una risorsa e, se la classe gestisce una risorsa, in genere deve gestire la copia e la liberazione.

Se non esiste una buona semantica per copiare la risorsa gestita dalla classe, allora considera di proibire la copia dichiarando (no definizione) il costruttore della copia e l'operatore di assegnazione come private.

(Si noti che l'imminente nuova versione dello standard C ++ (che è C ++ 11) aggiunge la semantica del movimento al C ++, che probabilmente cambierà la Regola del 3. Tuttavia, ne so troppo poco per scrivere una sezione C ++ 11 sulla regola del tre).


450
2017-11-13 14:22



La legge dei tre grandi è come sopra specificato.

Un semplice esempio, in inglese semplice, del tipo di problema che risolve:

Distruttore non predefinito

Hai allocato la memoria nel tuo costruttore e quindi devi scrivere un distruttore per cancellarlo. Altrimenti si causerà una perdita di memoria.

Potresti pensare che questo è un lavoro fatto.

Il problema sarà, se una copia è fatta del tuo oggetto, allora la copia punterà alla stessa memoria dell'oggetto originale.

Una volta, uno di questi cancella la memoria nel suo distruttore, l'altro avrà un puntatore alla memoria non valida (questo è chiamato un puntatore che penzola) quando tenta di usarlo, le cose diventeranno pelose.

Pertanto, si scrive un costruttore di copie in modo che assegni nuovi oggetti ai propri pezzi di memoria da distruggere.

Operatore di assegnazione e costruttore di copie

Hai assegnato memoria nel tuo costruttore a un puntatore membro della tua classe. Quando si copia un oggetto di questa classe, l'operatore di assegnazione e il costruttore di copie predefiniti copieranno il valore di questo puntatore membro sul nuovo oggetto.

Ciò significa che il nuovo oggetto e il vecchio oggetto punteranno allo stesso pezzo di memoria, quindi quando lo cambierai in un oggetto verrà modificato anche per l'altro oggetto. Se un oggetto cancella questa memoria, l'altro continuerà a provare ad usarlo - eek.

Per risolvere questo problema scrivi la tua versione del costruttore di copia e dell'operatore di assegnazione. Le tue versioni assegnano memoria separata ai nuovi oggetti e copiano i valori a cui punta il primo puntatore anziché il suo indirizzo.


134
2018-05-14 14:22



Fondamentalmente se hai un distruttore (non il distruttore predefinito) significa che la classe che hai definito ha una certa allocazione di memoria. Supponiamo che la classe sia usata al di fuori da qualche codice cliente o da voi.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

Se MyClass ha solo alcuni membri tipizzati primitivi, un operatore di assegnazione predefinito funzionerebbe, ma se ha alcuni membri del puntatore e oggetti che non hanno operatori di assegnazione il risultato sarebbe imprevedibile. Quindi possiamo dire che se c'è qualcosa da eliminare nel distruttore di una classe, potremmo aver bisogno di un operatore di copia profonda, il che significa che dovremmo fornire un costruttore di copia e un operatore di assegnazione.


37
2017-12-31 19:29



Cosa significa copiare un oggetto? Ci sono alcuni modi in cui puoi copiare oggetti - parliamo dei 2 tipi a cui probabilmente ti riferisci - copia profonda e copia superficiale.

Dato che siamo in un linguaggio orientato agli oggetti (o almeno lo stanno assumendo), diciamo che hai un pezzo di memoria allocato. Dato che si tratta di un linguaggio OO, possiamo facilmente fare riferimento a blocchi di memoria che allochiamo perché di solito sono le variabili primitive (ints, chars, bytes) o le classi che abbiamo definito fatte con i nostri tipi e primitive. Quindi diciamo che abbiamo una classe di auto come segue:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

Una copia profonda è se dichiariamo un oggetto e quindi creiamo una copia completamente separata dell'oggetto ... finiamo con 2 oggetti in 2 set di memoria completamente separati.

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

Ora facciamo qualcosa di strano. Diciamo che car2 è programmato male o intenzionalmente destinato a condividere la memoria reale di cui è fatta la macchina1. (Di solito è un errore farlo e nelle classi è di solito la coperta di cui si parla.) Fingi che ogni volta che chiedi a car2, stai davvero risolvendo un puntatore allo spazio di memoria di car1 ... è più o meno una copia superficiale è.

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

Quindi, indipendentemente dal linguaggio in cui stai scrivendo, stai molto attento a ciò che intendi quando si tratta di copiare oggetti perché la maggior parte delle volte si desidera una copia profonda.

Quali sono il costruttore di copie e l'operatore di assegnazione delle copie? Li ho già usati sopra. Il costruttore di copie viene chiamato quando si digita un codice come Car car2 = car1;  Sostanzialmente se si dichiara una variabile e la si assegna in una riga, viene chiamato il costruttore della copia. L'operatore di assegnazione è ciò che accade quando usi un segno di uguale--car2 = car1;. Avviso car2 non è dichiarato nella stessa dichiarazione. I due blocchi di codice che scrivi per queste operazioni sono probabilmente molto simili. In effetti il ​​tipico schema di progettazione ha un'altra funzione che si chiama per impostare tutto una volta che si è soddisfatti della copia / assegnazione iniziale legittima - se si guarda al codice a mano lunga che ho scritto, le funzioni sono quasi identiche.

Quando devo dichiararli da solo? Se non stai scrivendo un codice che deve essere condiviso o per produzione in qualche modo, devi solo dichiararlo quando ne hai bisogno. È necessario essere consapevoli di ciò che fa il linguaggio del programma se si sceglie di usarlo 'per caso' e non ne ha fatto uno - cioè. ottieni il valore predefinito del compilatore. Raramente utilizzo i costruttori di copie, ad esempio, ma le sostituzioni dell'operatore di assegnazione sono molto comuni. Sapevi che puoi ignorare anche quali addizioni, sottrazioni, ecc. Significano?

Come posso impedire che i miei oggetti vengano copiati? Sostituire tutti i modi in cui è consentito allocare memoria per il tuo oggetto con una funzione privata è un inizio ragionevole. Se davvero non vuoi che le persone li copiano, potresti renderlo pubblico e allertare il programmatore generando un'eccezione e non copiando l'oggetto.


27
2017-10-17 16:37



Quando devo dichiararli da solo?

La Regola dei Tre afferma che se dichiari qualcuno di a

  1. copia costruttore
  2. copia l'operatore di assegnazione
  3. distruttore

allora dovresti dichiararli tutti e tre. Nacque dall'osservazione che la necessità di assumere il significato di un'operazione di copia derivava quasi sempre dalla classe che eseguiva un qualche tipo di gestione delle risorse, e che quasi sempre implicava che

  • qualunque sia stata la gestione delle risorse eseguita in un'operazione di copia probabilmente doveva essere eseguita nell'altra operazione di copia e

  • il distruttore di classe parteciperebbe anche alla gestione della risorsa (solitamente rilasciandola). La risorsa classica da gestire era la memoria, ed è per questo che tutte le classi della libreria standard gestire la memoria (ad esempio, i contenitori STL che eseguono la gestione dinamica della memoria) dichiarano tutti "i tre grandi": sia le operazioni di copia che un distruttore.

Una conseguenza della Regola del Tre è che la presenza di un distruttore dichiarato dall'utente indica che è improbabile che una semplice copia di membro sia appropriata per le operazioni di copia nella classe. Questo, a sua volta, suggerisce che se una classe dichiara un distruttore, le operazioni di copia probabilmente non dovrebbero essere generate automaticamente, perché non farebbero la cosa giusta. Al momento dell'adozione del C ++ 98, l'importanza di questa linea di ragionamento non era pienamente apprezzata, quindi in C ++ 98 l'esistenza di un distruttore dichiarato dall'utente non aveva alcun impatto sulla volontà dei compilatori di generare operazioni di copia. Questo continua ad essere il caso in C ++ 11, ma solo perché la limitazione delle condizioni in cui vengono generate le operazioni di copia causerebbe una rottura eccessiva del codice legacy.

Come posso impedire che i miei oggetti vengano copiati?

Dichiarare il costruttore di copia e l'operatore di assegnazione copia come specificatore di accesso privato.

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

In C ++ 11 in poi puoi anche dichiarare il costruttore di copia e l'operatore di assegnazione cancellati

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

19
2018-01-12 09:54



Molte delle risposte esistenti toccano già il costruttore di copie, l'operatore di assegnazione e il distruttore. Tuttavia, nel post C ++ 11, l'introduzione della mossa semantica potrebbe espandere questo oltre 3.

Recentemente Michael Claisse ha tenuto un discorso che tocca questo argomento: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


9
2018-01-07 05:38



La regola del terzo in C ++ è un principio fondamentale della progettazione e lo sviluppo di tre requisiti che, se esiste una chiara definizione in una delle seguenti funzioni membro, il programmatore dovrebbe definire insieme le altre due funzioni dei membri. Vale a dire le seguenti tre funzioni membro sono indispensabili: distruttore, costruttore di copia, operatore di assegnazione copia.

Copia costruttore in C ++ è un costruttore speciale. È usato per costruire un nuovo oggetto, che è il nuovo oggetto equivalente a una copia di un oggetto esistente.

L'operatore di assegnazione delle copie è un operatore di assegnazione speciale che viene solitamente utilizzato per specificare un oggetto esistente ad altri dello stesso tipo di oggetto.

Ci sono esempi veloci:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

5
2017-08-12 04:27